はじめに
皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第20回目です。
今回は、オブジェクトの外部からその内部状態を保存し、後で復元するためのMemento(メメント)パターンについて解説します。
Mementoパターンとは?
Mementoパターンは、オブジェクトの特定の時点でのスナップショット(状態)を保存し、その状態にいつでも復元できるようにする振る舞いパターンです。これにより、オブジェクトの内部構造を外部に公開することなく、安全な「元に戻す(Undo)」機能などを実装できます。
例えるなら、ゲームのセーブ機能です。
あなたはゲームの途中でいつでも進行状況(キャラクターの位置、アイテム、HPなど)をセーブでき、後でその状態から再開できます。このとき、セーブデータはゲームの内部ロジックを知らなくても、状態を保存し、復元することができます。
このパターンの主な目的は以下の通りです:
- 状態の保存と復元: オブジェクトの内部状態を外部に保存するメカニズムを提供する
- カプセル化の維持: 状態を保存・復元する際も、オブジェクトの内部構造を外部に公開しない
- Undo/Redo機能: 複数の状態を保存することで、操作を何度でも元に戻せるようにする
パターンの構成要素
Mementoパターンでは、以下の3つのコンポーネントが登場します:
-
オリジネーター(Originator): 状態を保存・復元する元のオブジェクト。自身が
Mementoオブジェクトを作成します - メメント(Memento): オリジネーターの内部状態を保持するオブジェクト
-
ケアテイカー(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な実装では、dataclassesやnamedtupleを活用することで、より簡潔で読みやすいコードが書けます。
# 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パターンは、以下のような場面でよく使われます:
- テキストエディタのUndo/Redo機能
- ゲームのセーブ/ロード機能
- データベーストランザクションのロールバック
- 設定の一時保存と復元
- キャンバス系アプリケーションの作業履歴
パターンの利点と注意点
利点
- オブジェクトのカプセル化を破ることなく状態を保存・復元できる
- Undo/Redo機能を簡単に実装できる
- オリジネーターとケアテイカーの疎結合を保てる
注意点
- 状態が大きい場合、メモリ使用量が増大する可能性がある
- 頻繁な状態保存は性能に影響を与える場合がある
- 状態の一部のみを保存する場合、設計が複雑になることがある
まとめ:本質は「状態のバックアップと復元」
| 特性 | Java | Python |
|---|---|---|
| 主な解決策 | 状態を保持する専用のクラス(Memento) |
dataclassや辞書を使った状態保持 |
| 設計思想 | カプセル化を厳格に守り、安全性を確保 | 柔軟なデータ構造とメソッドを使い、簡潔さを優先 |
| コードの意図 | 型を通じて状態の保存・復元を明示 | dataclassやプロパティを通じて状態の属性を表現 |
| メモリ効率 | オブジェクトのオーバーヘッドがある | よりコンパクトなデータ構造が可能 |
Mementoパターンは、両言語で実装のスタイルは異なりますが、 「オブジェクトの内部状態を外部にバックアップし、必要に応じて復元する」 という本質は共通です。Javaは厳格なクラス設計で安全性を追求し、Pythonは言語の柔軟性を活かしてよりシンプルに実装します。
このパターンは、Undo機能や履歴管理など、状態の復元が必要なシステムで非常に役立ちます。特に、ユーザーの操作を元に戻せる機能が求められるアプリケーション開発において、必須のパターンといえるでしょう。
明日は、あるオブジェクトの状態によって振る舞いを変化させるStateパターンについて解説します。お楽しみに!
次回のテーマは、「Day 21 Stateパターン:オブジェクトの状態によって振る舞いを変える」です。