逆操作コマンド
操作のヒストリーをコマンドという単位でまとめ、時系列で保存しておく
UndoもしくはUndoのUndo(=Redo)をするために、各コマンドは「そのコマンドで行った変更を打ち消す変更」を実装する
例)
オブジェクトを(100, 100)から(200, 200)の位置に移動した <-> (100, 100)の位置に移動させる
設定の値をfalseにした <-> 設定の値をtrueにする
など
Cons
各操作について常に逆操作を実装しないといけないので
スナップショットに比べると面倒
100操作を戻すのに100回逆操作を適用しないといけない
操作前のアプリケーションの状態を得るために、操作後の(現在の)アプリケーションの状態を必要とする
スナップショットに比べて、状態の不整合が起きやすい(これはスナップショット側が純粋に状態を上書きするため)
Pros
使用するメモリが少なく、形式によってはファイルに永続化させることで無制限のUndoを実装することも出来る
Gitは各commit時点におけるファイルのスナップショットを直接保存していたような気がします

そうですね。全部かはわからないけどmergeとかの時にdiffにしたりして変更適用してる

全部diffで保存してると、1万コミットしたら1万回変更適用しないと最新状態が得られない(容量少ないかわりに計算コストが高い)
スナップショットだと容量がでかくなる問題については、ハッシュ値のテーブル持つことで節約等をしてる
git commit object (とtree object)
概念上はファイルシステムのスナップショットです

でも一回のcommitで数個のファイルだけ変わると、多くのtree objectが再利用できるため容量を無駄に食わない。いわば一種のcopy on writeである
複雑になると逆操作コマンドの実装が大変になる。
新しいオブジェクトの生成とかやると辛い
コンストラクタと同じパラメータ持ったCreateObjectCommand再実装するか、生成したオブジェクトのインスタンス直接持たせるか、みたいになる
オブジェクトのインスタンス持たせると後で書き換えるとつらい
CreateObjectCommandでだけインスタンスを持たせてそれ以降の書き換えはmodと逆modにしました
後からUndo機能をつけようとすると大変辛い

既存実装がコマンド単位になってない(1操作で複数回データを編集してる、副作用がめちゃくちゃあるetc)のを直すところから始まるのでしんどかったりする…

一方で、操作が複雑な結果を生むようなアプリでは
スナップショットのほうが効果を発揮する
コマンド単体ではアプリケーションの状態を復元できない
「そのコマンドで行った変更を打ち消す変更」としてスナップショットを使う手抜き実装をやりがち

複雑なオブジェクト相手にはオブジェクト単位でスナップショット使ったりしますね
snapshotの差分を取って逆操作コマンドにしてもよさそう

virtual DOMみたいにsnapshotをオブジェクトにロードするのを作った

逆操作コマンドの自動生成の話

コマンドによって生じたdiffを逆操作コマンドとする
ObjectとPropertyにIDを振っておいて「自分の変更」のみをHashTableでまとめる。
スナップショットと逆操作コマンドの中間みたいな実装
(永続化もしやすいかも)
別に作ったコマンドを合成とかやり出して難しくなってくる

新規オブジェクトを作成 + そのオブジェクトを選択という2つのコマンドを合成するなど
要素の選択を履歴にもつ
キーストロークのundo合成

Cocoaだと連続的なキーストロークかどうかをNSEvent.isARepeatで取れる

groupByEvent(Cocoaが標準でドラッグのアンドゥをまとめてくれる)

↑ただこれは中間状態を常に記録してて、1回のUndoで100回のUndoとか発生するので使ってない
AxStudioはDiffなのでドラッグはマウスを離したときのみに記録している
Undoデータにkey的なものを入れておいて、今から入れるコマンドが最後のkeyと同じならマージ的な

オブジェクトAに対する移動と、Undoスタックの一番上もオブジェクトAに対する移動だったら
KeyPathみたいな
操作のグルーピングという概念で表されるべきところ、Undoスタックとコマンドに対する処理として表現してる気がする
ドラッグ操作とか連続的な操作をどうコマンドに落とし込むかよく悩む

例えば1秒で色相環ぐるっと回したら60エントリ積まれたり
AxStudioではPhase<T>クラス(start・continue(T)・end・pulse(T))とか作ってUIがコマンドに送ってる

ColorPickerで色相環のドラッグならstart→continue(T)→end
色ライブラリの選択ならpulse
これでUndoのタイミングを調整している
STUDIO では "staging状態" を作りました
scrapboxでは連続して編集しまくってる間の操作は貯めておいて、編集が1秒ぐらい止まったらサーバーに送信(保存)してます。その時にundoも記録している

↑の使い分けに結構悩むんですよね
アンドゥスタックの一番上の命令をその場で書き換えれる場合には書き換えちゃってますね。
Scrapboxのundoはこれですね

他人の見ているエディタにも変更を同期させたいので
操作した人のブラウザ側に、逆操作を溜めておいて
逆操作コマンドを操作コマンドと同様に送信する