ソフトウェア設計

Command vs Memento:undo実装の2つのアプローチ総まとめ

undo機能を自分で作ろうとして、行き詰まったことはありませんか?

読了 4 分
Command vs Memento:undo実装の2つのアプローチ総まとめのカバー画像

undo機能を自分で作ろうとして、行き詰まったことはありませんか?

Ctrl+Z一回で済むおなじみの機能ですが、コードに落とし込むと意外に厄介です。ペイントソフト風の小さなトイプロジェクトでも、何日も悩むことがあります。

結論から言うと、undoの実装は大きく2通りです。

1つは実行した操作を逆に戻すCommandパターン、もう1つは操作前の状態全体を保存しておくMementoパターンです。

どちらも正解です。使う場面が違うだけです。今回はコードも交えて比較します。


Commandパターン — 操作をオブジェクトとしてラップする

Commandパターンでは、「何をしたか」を1つのオブジェクトにまとめます。

executeとundoを1組にしたオブジェクトだと考えてください。

たとえば文字を入力する操作なら、その反対である「入力した分だけ削除する」処理を同じオブジェクトにあらかじめ定義します。

protocol Command {
    func execute()
    func undo()
}

struct TypeCommand: Command {
    let text: String
    let editor: Editor
    func execute() { editor.content += text }
    func undo() { editor.content.removeLast(text.count) }
}

こうして作ったCommandオブジェクトを、スタックに順番に積みます。

Ctrl+Zが入力されたら、いちばん上を取り出してundo()を呼ぶだけです。

Commandパターンが保存するのは状態ではなく、「操作」です。


Mementoパターン — 状態をスナップショットとして保存する

Mementoはまったく異なるアプローチを取ります。

操作をどう戻すか考える代わりに、変更直前の状態全体をそのまま保存します。写真を撮るようなものです。

struct Memento { let content: String }

class Editor {
    var content = ""
    func save() -> Memento { Memento(content: content) }
    func restore(_ m: Memento) { content = m.content }
}

let editor = Editor()
editor.content = "こんにちは"
let snapshot = editor.save()   // ここでスナップショットを保存
editor.content = "こんにちは"
editor.restore(snapshot)
print(editor.content)
// 出力:こんにちは

undoが必要になったら、保存済みのスナップショットで状態を置き換えます。それだけです。

操作ごとに復元ロジックを書く必要がないので、気が楽です。

ただし、欠点も明確です。状態が大きいと、毎回全体をコピーするためメモリをかなり消費します。

Commandオブジェクトをスタックに積む構造の図
Commandオブジェクトをスタックに積む構造の図

では、どちらを使うべきか?

両方を使ってみて感じた違いを表にまとめました。

基準 Commandパターン Mementoパターン
保存対象 操作(execute/undo) 状態のスナップショット
メモリ 軽い 状態が大きいと重い
undoロジック 操作ごとに実装 復元するだけ
redo スタックを戻すだけで自然 スナップショット配列で可能
向いている場所 エディタ、グラフィックツール ゲームセーブ、フォーム状態

一言でまとめると、こうです。

戻す操作を明確に定義できるならCommand、状態全体を保存するほうが楽ならMementoです。

Ctrl+Zひとつに何日も苦戦したあの机
Ctrl+Zひとつに何日も苦戦したあの机

いつ使い、いつ避けるか

実際に選ぶときは、次の基準を見ます。

  • 操作単位が明確でredoも必要 → Command。代表例はエディタやドローイングツールです。
  • 状態が小さく、スナップショットのコピーが負担にならない → Memento。フォーム入力や設定値の復元などです。
  • 状態が巨大なのに変更は局所的 → Mementoは避けましょう。毎回全体をコピーするとメモリを圧迫します。Commandのほうが適しています。
  • 戻すロジックがどうしても思いつかない → Commandを無理に使わず、Mementoでスナップショットを保存するのがすっきりします。

実務では、2つを組み合わせることもあります。操作はCommandで管理し、戻しにくい一部だけMementoでスナップショットを残す方法です。


面接ではこう聞かれます

Q. CommandパターンとMementoパターンの違いは何ですか?

Commandは操作をオブジェクトにカプセル化し、executeとundoを対にします。Mementoはオブジェクトの状態をスナップショットとして保存し、後から復元します。つまり、操作の逆演算で戻すか、状態を復元するかの違いです。

Q. undoスタックを実装するなら、どちらを選びますか?

操作単位が明確でredoも自然にサポートしたいならCommandを選びます。一方、状態が小さく、戻すロジックを書くのが面倒なら、Mementoでスナップショットを積むほうが単純で安全です。


2つのパターンは、ぜひ一度ずつ自分で実装してみてください。概念を読むだけのときと、スタックにオブジェクトが積まれ、Ctrl+Zで状態が戻る様子を目で見るときとでは、理解の深さがまったく違います。この記事が最初の一歩の助けになればうれしいです。

あわせて読みたい