Javaを鍛える|デザインパターン全23回 #20

Mementoパターン|編集キャンセルで「元の内容」を復元する

  • Java
  • デザインパターン
  • Memento
  • Javaを鍛える

📋 この記事の目次
  1. このパターンは何のためにあるか
  2. Before
  3. After
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftでチケットの詳細を編集中、概要欄を書き換えている最中に「キャンセル」ボタンを押すと、保存済みの内容まで消えてしまうという問題が起きていました。開発者が意図したのは保存前の状態へ戻ることでしたが、内部では変更した値を直接上書きしているだけだったため、元に戻す処理のロジックが組み込まれておらず、編集前の内容が二度と取得できなくなっていました。

このパターンは何のためにあるか

Mementoパターンは、オブジェクトのある時点の状態を保存し、後でその状態へ戻すための設計パターンです。日常で言えば、Word文書の「元に戻す」ボタンや、ゲームのセーブデータをイメージすると分かりやすいでしょう。

Before

TicketCraftではチケット情報をTicketクラスとして管理していますが、編集画面でキャンセルした場合に元の内容を復元する仕組みがなく、以下のような実装になっていました。

public class Ticket {
    private String title;
    private String description;

    public Ticket(String title, String description) {
        this.title = title;
        this.description = description;
    }

    public String getTitle() {
        return title;
    }

    public void updateDescription(String newDesc) {
        this.description = newDesc;
    }

    public String getDescription() {
        return description;
    }
}

public class BeforeMain {
    public static void main(String[] args) {
        Ticket ticket = new Ticket("API仕様を策定する", "要件定義から設計フェーズの概要");
        System.out.println("変更前: " + ticket.getDescription());
        ticket.updateDescription("新機能のAPIエンドポイント一覧と認証方式の検討");
        System.out.println("変更後: " + ticket.getDescription());
        System.out.println("元に戻したいが、変更前の値はどこにも保存されていない。");
    }
}

この実装の問題は、updateDescription()メソッドがフィールドを直接上書きしている点です。一度更新されると、保存済みの元の値は消滅し、別のオブジェクトにコピーを取らない限り復元できません。編集キャンセル時に「前の状態に戻す」という要件を満たすには、呼び出し側で手動で値を保持する必要がありますが、そのコードはどこにも書かれていないため、キャンセル機能を実装しようとしても復元元の情報が存在しないという状況に陥ります。

After

Mementoパターンを適用すると、TicketEditorが内部状態をラップし、必要に応じてスナップショットを取得・設定できるようになります。

public class Ticket {
    private String title;
    private String description;

    public Ticket(String title, String description) {
        this.title = title;
        this.description = description;
    }

    public String getTitle() {
        return title;
    }

    public void updateDescription(String newDesc) {
        this.description = newDesc;
    }

    public String getDescription() {
        return description;
    }
}

public class TicketMemento {
    private final String savedTitle;
    private final String savedDescription;

    public TicketMemento(Ticket ticket) {
        this.savedTitle = ticket.getTitle();
        this.savedDescription = ticket.getDescription();
    }

    public String getTitle() { return savedTitle; }
    public String getDescription() { return savedDescription; }
}

public class TicketEditor {
    private Ticket currentTicket;

    public void open(Ticket ticket) {
        this.currentTicket = new Ticket(ticket.getTitle(), ticket.getDescription());
    }

    public void updateDescription(String newDesc) {
        if (currentTicket != null) {
            currentTicket.updateDescription(newDesc);
        }
    }

    public String getDescription() { return currentTicket.getDescription(); }

    public TicketMemento saveSnapshot() {
        return new TicketMemento(currentTicket);
    }

    public void restoreSnapshot(TicketMemento memento) {
        this.currentTicket = new Ticket(memento.getTitle(), memento.getDescription());
    }
}

public class AfterMain {
    public static void main(String[] args) {
        Ticket original = new Ticket("API仕様を策定する", "要件定義から設計フェーズの概要");
        TicketEditor editor = new TicketEditor();
        editor.open(original);
        System.out.println("変更前: " + editor.getDescription());

        TicketMemento snapshot = editor.saveSnapshot();
        editor.updateDescription("新機能のAPIエンドポイント一覧と認証方式の検討");
        System.out.println("変更後: " + editor.getDescription());

        System.out.println("元に戻す...");
        editor.restoreSnapshot(snapshot);
        System.out.println("復元後: " + editor.getDescription());
    }
}

実行結果は以下の通りです。

変更前: 要件定義から設計フェーズの概要
変更後: 新機能のAPIエンドポイント一覧と認証方式の検討
元に戻す...
復元後: 要件定義から設計フェーズの概要

TicketMementoは不変(immutable)なスナップショットクラスであり、内部状態を直接書き換える手段を提供しません。これにより「どの時点の値が保存されているか」が明確になり、編集画面で「キャンセル」ボタンを押した際に、内部的にrestoreSnapshot()を呼び出すことで元の状態へ戻せるようになりました。Commandパターンによる操作履歴との違いは、このパターンが「ある時点の状態そのもの」をオブジェクトとして保持する点にあります。

🧓 ベテランの現場コラム MementoとUndoは似ているようで目的が異なります。Mementoは「ある瞬間のスナップショット」を丸ごと保持するのに対し、Commandパターンは「1つ1つの操作を逆再生する」ことでUndoを実現します。編集キャンセルのように「今の状態を丸ごと元に戻したい」場面ではMemento、操作履歴を1件ずつ取り消したい場面ではCommandが適しています。

こんな場面で使う/使わない

  • 使う: 編集画面のキャンセル機能のように、ある時点の状態を一時的に保持し、あとで丸ごと復元したい場合
  • 使わない: システム全体の状態を永続化して長期保存する必要があり、ファイルやDBへの書き出しが必要な場合

まとめ

  • Mementoパターンは「ある時点の状態」をスナップショットとしてオブジェクト化する設計
  • スナップショットは不変にするのが安全で、復元先のロジックから内部状態を分離できる
  • Undo機能と似た用途に見えるが、Commandパターンとは目的が異なるため使い分けが必要

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。