はじめに
初めてUndoを自力で実装することになったときに何から考えればいいのか分からなかったので自分なりの考え方を整理してみます。
サンプルコードも載せようかと思ったんですがググったらいっぱいサンプルが出てきたので今さら自分がやらなくてもいいかなと思ったのでやめておきます。
Undoのアプローチ
Undoというのは時間(ユーザーの操作)で変化するオブジェクトの状態を元に戻したり、戻したあとに先に進めたりするような機能です。gitのTreeのようなものです。と言ってもこの説明で理解できる人は今さらUndoの実装方法なんて興味ないかもしれませんが。
とにかくアプリケーションというのは時間でオブジェクトの状態が変わり、そのオブジェクトの状態を行ったり来たり遷移させるのがUndo/Redo機能ということになります。
で、Undoを実装する上では2つのアプローチがあると思っています。
- 「状態」に着目する
- 「遷移」に着目する
「状態」に着目するアプローチ
簡単に言うと、オブジェクトの内部状態がA→B→C
と変化していくときに、それぞれのオブジェクトのコピーを作成しておきます。で、Undoが実行されたら1つ前の状態のオブジェクトに入れ替える、という方法になります。
いわゆるMementoパターンです。
オブジェクトの状態が変わるたびに現在の状態のバックアップを追加していくようなイメージでしょうか。
メリット
- 状態のスナップショットをとっていくので何世代も前の状態にいきなり戻せる。
- Undo処理の大きさが処理自体の大きさに依存しない(めちゃくちゃ重い処理でもデータが軽ければ一瞬でUndoできる)
- Undoの対象となる操作が仕様変更で拡大しても操作の処理自体に手を入れる必要がない(勝手にスナップショットがとられて差分が保持されるため)
デメリット
- 何か変化が発生するたびにオブジェクトのコピーを作るのでメモリがどんどん圧迫される
- オブジェクトの量が多い場合は上手いこと変更のあったオブジェクトだけ選んでコピーを取る機構が必要かも(状態変化前と後で差分を取るとか)
- コピーを作成できるような対象にしか利用できない
- Undo対象のオブジェクトの種類が拡大するとUndoの機構自体に手を入れる必要がある
参考資料
デザインパターン勉強会 第18回:Mementoパターン
https://qiita.com/mitakaosamu/items/31d881ae14b28919e910
C#でUndo機能が気に入らないので力技で自作した(RichTextBox)
http://pineplanter.moo.jp/non-it-salaryman/2017/11/29/csharp-undo-richtextbox/
「変化」に着目するアプローチ
簡単に言うと、オブジェクトの内部状態がA→B
と変化するとき、A→B
へ変化する処理とB→A
に戻す処理を同時に記憶しておいて、Undoが実行されると逆方向の処理が行われる、という方法です(RedoするときはA→B
へ変化する処理をもう1回実行する)。簡単に言えない。
いわゆるCommandパターンです。
オブジェクトの状態が実際に変わる前に「未来方向への変化」と「過去方向への変化」をペアでUndoのスタックに積んでいきます。
メリット
- オブジェクトそのもののコピーは作らないので省メモリ
- Undo対象のオブジェクトの種類が拡大してもUndo機構自体は影響を受けない
デメリット
- Undoするたびにオブジェクトを変化させるので変化処理自体が重いとUndoも重くなる
- 可逆的な変化に対してしかUndoできない(1回やっちゃうと戻せない処理はUndoできない)
- 具体的な例を挙げようと思ったら思いつかなかったのでもしかしたらそんなデメリットないかも
- Undo対象の操作が仕様変更で拡大した場合は操作処理自体に手を入れる必要がある(Undo機構の呼び出しが必要のため)
参考資料
C#でUndo/Redoを実装した
https://qiita.com/nossey/items/c59910558d5501f03ad0
Undo,Redoの実装って何十回もやってる気がする
http://d.hatena.ne.jp/Youchan/20081110/1226282911
感想
ネットで検索するとCommandパターンでのUndo実装のやり方の方がサンプルが多いので、まずはこっちからアプローチしてみればいいかもしれない。あとC#だとUndoのためのクラスライブラリがNugetで公開されてたりしました。