0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全30回:静的と動的でどう違うのか、JavaとPythonで学ぶデザインパターン - Day 20 Mementoパターン:オブジェクトの状態を保存し復元する

Posted at

はじめに

皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第20回目です。
今回は、オブジェクトの外部からその内部状態を保存し、後で復元するためのMemento(メメント)パターンについて解説します。


Mementoパターンとは?

Mementoパターンは、オブジェクトの特定の時点でのスナップショット(状態)を保存し、その状態にいつでも復元できるようにする振る舞いパターンです。これにより、オブジェクトの内部構造を外部に公開することなく、安全な「元に戻す(Undo)」機能などを実装できます。

例えるなら、ゲームのセーブ機能です。
あなたはゲームの途中でいつでも進行状況(キャラクターの位置、アイテム、HPなど)をセーブでき、後でその状態から再開できます。このとき、セーブデータはゲームの内部ロジックを知らなくても、状態を保存し、復元することができます。

このパターンの主な目的は以下の通りです:

  • 状態の保存と復元: オブジェクトの内部状態を外部に保存するメカニズムを提供する
  • カプセル化の維持: 状態を保存・復元する際も、オブジェクトの内部構造を外部に公開しない
  • Undo/Redo機能: 複数の状態を保存することで、操作を何度でも元に戻せるようにする

パターンの構成要素

Mementoパターンでは、以下の3つのコンポーネントが登場します:

  1. オリジネーター(Originator): 状態を保存・復元する元のオブジェクト。自身がMementoオブジェクトを作成します
  2. メメント(Memento): オリジネーターの内部状態を保持するオブジェクト
  3. ケアテイカー(Caretaker): Mementoオブジェクトを保管するクラス。オリジネーターに状態の保存を要求し、必要に応じて復元を要求します

Javaでの実装:厳格なアクセス制御

Javaは、プライベートな内部状態へのアクセスを厳格に制御できるため、Mementoパターンに非常に適しています。以下に、テキストエディタのUndo機能を例に実装を示します。

// JavaでのMementoパターンの実装例

// メメントクラス
class EditorMemento {
    private final String content;
    private final int cursorPosition;  // より現実的なエディタの状態を表現

    public EditorMemento(String content, int cursorPosition) {
        this.content = content;
        this.cursorPosition = cursorPosition;
    }

    public String getContent() {
        return content;
    }
    
    public int getCursorPosition() {
        return cursorPosition;
    }
}

// オリジネータークラス
class TextEditor {
    private String content;
    private int cursorPosition;

    public TextEditor(String content) {
        this.content = content;
        this.cursorPosition = 0;
    }

    public void addContent(String newContent) {
        this.content = this.content.substring(0, cursorPosition) + 
                       newContent + 
                       this.content.substring(cursorPosition);
        this.cursorPosition += newContent.length();
        System.out.println("Current content: '" + this.content + 
                         "' (cursor at position " + this.cursorPosition + ")");
    }

    public void setCursorPosition(int position) {
        this.cursorPosition = Math.max(0, Math.min(position, content.length()));
    }

    // 状態を保存(メメントを作成)
    public EditorMemento save() {
        System.out.println("Saving current state...");
        return new EditorMemento(this.content, this.cursorPosition);
    }

    // 状態を復元(メメントから復元)
    public void restore(EditorMemento memento) {
        this.content = memento.getContent();
        this.cursorPosition = memento.getCursorPosition();
        System.out.println("Restored to content: '" + this.content + 
                         "' (cursor at position " + this.cursorPosition + ")");
    }
    
    public String getContent() {
        return content;
    }
}

// ケアテイカークラス(複数の状態をサポート)
class History {
    private final java.util.Stack<EditorMemento> history;
    
    public History() {
        this.history = new java.util.Stack<>();
    }

    public void saveState(EditorMemento memento) {
        history.push(memento);
        System.out.println("State saved to history (total: " + history.size() + ")");
    }

    public EditorMemento undo() {
        if (!history.isEmpty()) {
            EditorMemento memento = history.pop();
            System.out.println("Undoing to previous state...");
            return memento;
        } else {
            System.out.println("No more states to undo");
            return null;
        }
    }
    
    public boolean hasHistory() {
        return !history.isEmpty();
    }
}

// 使い方
public class Main {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor("Hello, ");
        History history = new History();

        // 初期状態を保存
        history.saveState(editor.save());

        // 新しい内容を追加
        editor.setCursorPosition(7); // "Hello, "の後にカーソルを配置
        editor.addContent("World!");
        
        // この状態も保存
        history.saveState(editor.save());
        
        // さらに追加
        editor.addContent(" How are you?");

        // 元の状態に復元
        if (history.hasHistory()) {
            editor.restore(history.undo());
        }
        
        // さらに前の状態に復元
        if (history.hasHistory()) {
            editor.restore(history.undo());
        }
    }
}

Pythonでの実装:柔軟なオブジェクトの属性管理

Pythonは、動的な属性管理と柔軟なクラス構造を持つため、Javaとは異なるアプローチでMementoパターンを実装できます。Pythonicな実装では、dataclassesnamedtupleを活用することで、より簡潔で読みやすいコードが書けます。

# PythonでのMementoパターンの実装例
from dataclasses import dataclass
from typing import Optional, List

# メメントクラス(dataclassを使用)
@dataclass(frozen=True)  # イミュータブルにして安全性を確保
class EditorMemento:
    content: str
    cursor_position: int

# オリジネータークラス
class TextEditor:
    def __init__(self, content: str = ""):
        self._content = content
        self._cursor_position = 0

    def add_content(self, new_content: str) -> None:
        self._content = (self._content[:self._cursor_position] + 
                        new_content + 
                        self._content[self._cursor_position:])
        self._cursor_position += len(new_content)
        print(f"Current content: '{self._content}' "
              f"(cursor at position {self._cursor_position})")

    def set_cursor_position(self, position: int) -> None:
        self._cursor_position = max(0, min(position, len(self._content)))

    # 状態を保存(dataclassのメメントを返す)
    def save(self) -> EditorMemento:
        print("Saving current state...")
        return EditorMemento(content=self._content, 
                           cursor_position=self._cursor_position)

    # 状態を復元(メメントから復元)
    def restore(self, memento: EditorMemento) -> None:
        self._content = memento.content
        self._cursor_position = memento.cursor_position
        print(f"Restored to content: '{self._content}' "
              f"(cursor at position {self._cursor_position})")
    
    @property
    def content(self) -> str:
        return self._content

# ケアテイカークラス
class History:
    def __init__(self):
        self._history: List[EditorMemento] = []

    def save_state(self, memento: EditorMemento) -> None:
        self._history.append(memento)
        print(f"State saved to history (total: {len(self._history)})")

    def undo(self) -> Optional[EditorMemento]:
        if self._history:
            memento = self._history.pop()
            print("Undoing to previous state...")
            return memento
        else:
            print("No more states to undo")
            return None
    
    def has_history(self) -> bool:
        return len(self._history) > 0

# 使い方
def main():
    editor = TextEditor("Hello, ")
    history = History()

    # 初期状態を保存
    history.save_state(editor.save())

    # 新しい内容を追加
    editor.set_cursor_position(7)  # "Hello, "の後にカーソルを配置
    editor.add_content("World!")
    
    # この状態も保存
    history.save_state(editor.save())
    
    # さらに追加
    editor.add_content(" How are you?")

    # 元の状態に復元
    if history.has_history():
        memento = history.undo()
        if memento:
            editor.restore(memento)
    
    # さらに前の状態に復元
    if history.has_history():
        memento = history.undo()
        if memento:
            editor.restore(memento)

if __name__ == "__main__":
    main()

実装の違いと特徴

Javaの実装の特徴

  • 厳格な型システム: メメントクラスの構造が明確で、コンパイル時に型安全性が保証される
  • カプセル化の徹底: プライベートフィールドとゲッターによる明確なアクセス制御
  • オブジェクト指向らしい設計: 各クラスの責務が明確に分離されている

Pythonの実装の特徴

  • dataclassの活用: ボイラープレートコードを削減し、より簡潔な実装
  • 型ヒント: コードの可読性と保守性を向上
  • Pythonic: 言語の特性を活かした自然な実装

応用例と実際の使用場面

Mementoパターンは、以下のような場面でよく使われます:

  1. テキストエディタのUndo/Redo機能
  2. ゲームのセーブ/ロード機能
  3. データベーストランザクションのロールバック
  4. 設定の一時保存と復元
  5. キャンバス系アプリケーションの作業履歴

パターンの利点と注意点

利点

  • オブジェクトのカプセル化を破ることなく状態を保存・復元できる
  • Undo/Redo機能を簡単に実装できる
  • オリジネーターとケアテイカーの疎結合を保てる

注意点

  • 状態が大きい場合、メモリ使用量が増大する可能性がある
  • 頻繁な状態保存は性能に影響を与える場合がある
  • 状態の一部のみを保存する場合、設計が複雑になることがある

まとめ:本質は「状態のバックアップと復元」

特性 Java Python
主な解決策 状態を保持する専用のクラス(Memento dataclassや辞書を使った状態保持
設計思想 カプセル化を厳格に守り、安全性を確保 柔軟なデータ構造とメソッドを使い、簡潔さを優先
コードの意図 型を通じて状態の保存・復元を明示 dataclassやプロパティを通じて状態の属性を表現
メモリ効率 オブジェクトのオーバーヘッドがある よりコンパクトなデータ構造が可能

Mementoパターンは、両言語で実装のスタイルは異なりますが、 「オブジェクトの内部状態を外部にバックアップし、必要に応じて復元する」 という本質は共通です。Javaは厳格なクラス設計で安全性を追求し、Pythonは言語の柔軟性を活かしてよりシンプルに実装します。

このパターンは、Undo機能や履歴管理など、状態の復元が必要なシステムで非常に役立ちます。特に、ユーザーの操作を元に戻せる機能が求められるアプリケーション開発において、必須のパターンといえるでしょう。

明日は、あるオブジェクトの状態によって振る舞いを変化させるStateパターンについて解説します。お楽しみに!

次回のテーマは、「Day 21 Stateパターン:オブジェクトの状態によって振る舞いを変える」です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?