逆操作コマンド
操作のヒストリーをコマンドという単位でまとめ、時系列で保存しておく
UndoもしくはUndoのUndo(=Redo)をするために、各コマンドは「そのコマンドで行った変更を打ち消す変更」を実装する
例)
オブジェクトを(100, 100)から(200, 200)の位置に移動した <-> (100, 100)の位置に移動させる
設定の値をfalseにした <-> 設定の値をtrueにする
など
Cons
各操作について常に逆操作を実装しないといけないのでスナップショットに比べると面倒 100操作を戻すのに100回逆操作を適用しないといけない
操作前のアプリケーションの状態を得るために、操作後の(現在の)アプリケーションの状態を必要とする
スナップショットに比べて、状態の不整合が起きやすい(これはスナップショット側が純粋に状態を上書きするため) Pros
/icons/hr.icon
使用するメモリが少なく、形式によってはファイルに永続化させることで無制限のUndoを実装することも出来る
Gitは各commit時点におけるファイルのスナップショットを直接保存していたような気がします igrep.icon
そうですね。全部かはわからないけどmergeとかの時にdiffにしたりして変更適用してるshokai.icon
全部diffで保存してると、1万コミットしたら1万回変更適用しないと最新状態が得られない(容量少ないかわりに計算コストが高い)
スナップショットだと容量がでかくなる問題については、ハッシュ値のテーブル持つことで節約等をしてる
git commit object (とtree object) 概念上はファイルシステムのスナップショットです jokester.icon
でも一回のcommitで数個のファイルだけ変わると、多くのtree objectが再利用できるため容量を無駄に食わない。いわば一種のcopy on writeである
複雑になると逆操作コマンドの実装が大変になる。
新しいオブジェクトの生成とかやると辛い
コンストラクタと同じパラメータ持ったCreateObjectCommand再実装するか、生成したオブジェクトのインスタンス直接持たせるか、みたいになる
オブジェクトのインスタンス持たせると後で書き換えるとつらい
CreateObjectCommandでだけインスタンスを持たせてそれ以降の書き換えはmodと逆modにしました
後からUndo機能をつけようとすると大変辛いmiyanokomiya.icon
既存実装がコマンド単位になってない(1操作で複数回データを編集してる、副作用がめちゃくちゃあるetc)のを直すところから始まるのでしんどかったりする…butaosuinu.icon
一方で、操作が複雑な結果を生むようなアプリではスナップショットのほうが効果を発揮する コマンド単体ではアプリケーションの状態を復元できない
「そのコマンドで行った変更を打ち消す変更」としてスナップショットを使う手抜き実装をやりがちmiyanokomiya.icon
複雑なオブジェクト相手にはオブジェクト単位でスナップショット使ったりしますね
snapshotの差分を取って逆操作コマンドにしてもよさそうseanchas_t.icon
virtual DOMみたいにsnapshotをオブジェクトにロードするのを作ったseanchas_t.icon
逆操作コマンドの自動生成の話 obuchiyuki.icon
コマンドによって生じたdiffを逆操作コマンドとする
ObjectとPropertyにIDを振っておいて「自分の変更」のみをHashTableでまとめる。
スナップショットと逆操作コマンドの中間みたいな実装
(永続化もしやすいかも)
別に作ったコマンドを合成とかやり出して難しくなってくるmiyanokomiya.icon
新規オブジェクトを作成 + そのオブジェクトを選択という2つのコマンドを合成するなど
要素の選択を履歴にもつ
キーストロークのundo合成seanchas_t.icon
Cocoaだと連続的なキーストロークかどうかをNSEvent.isARepeatで取れる obuchiyuki.icon
groupByEvent(Cocoaが標準でドラッグのアンドゥをまとめてくれる)obuchiyuki.icon
↑ただこれは中間状態を常に記録してて、1回のUndoで100回のUndoとか発生するので使ってない
AxStudioはDiffなのでドラッグはマウスを離したときのみに記録している
Undoデータにkey的なものを入れておいて、今から入れるコマンドが最後のkeyと同じならマージ的なmiyanokomiya.icon
オブジェクトAに対する移動と、Undoスタックの一番上もオブジェクトAに対する移動だったら
KeyPathみたいな
操作のグルーピングという概念で表されるべきところ、Undoスタックとコマンドに対する処理として表現してる気がする
ドラッグ操作とか連続的な操作をどうコマンドに落とし込むかよく悩むbutaosuinu.icon
例えば1秒で色相環ぐるっと回したら60エントリ積まれたり
AxStudioではPhase<T>クラス(start・continue(T)・end・pulse(T))とか作ってUIがコマンドに送ってるobuchiyuki.icon
ColorPickerで色相環のドラッグならstart→continue(T)→end
色ライブラリの選択ならpulse
これでUndoのタイミングを調整している
STUDIO では "staging状態" を作りました
scrapboxでは連続して編集しまくってる間の操作は貯めておいて、編集が1秒ぐらい止まったらサーバーに送信(保存)してます。その時にundoも記録しているshokai.icon
↑の使い分けに結構悩むんですよね
アンドゥスタックの一番上の命令をその場で書き換えれる場合には書き換えちゃってますね。
Scrapboxのundoはこれですねshokai.icon
他人の見ているエディタにも変更を同期させたいので
操作した人のブラウザ側に、逆操作を溜めておいて
逆操作コマンドを操作コマンドと同様に送信する