MementoパターンでUndo/Redo機能を実装：テキストエディタの状態管理

## はじめに

ソフトウェア開発において、ユーザー操作の取り消し（Undo）や再実行（Redo）機能は非常に重要です。特にテキストエディタのような複雑なアプリケーションでは、この機能が不可欠となります。本記事では、Pythonを使用してMementoパターンによるUndo/Redo機能を実装する方法を解説します。

## Mementoパターンとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/833c6941-9aa4-df1c-0cad-b18a7566e761.png)

Mementoパターンは、オブジェクトの内部状態を保存し、後でその状態を復元できるようにするデザインパターンです。このパターンは以下の3つの主要な役割で構成されています：

1. Originator: 状態を持つオブジェクト
2. Memento: Originatorの状態のスナップショット
3. Caretaker: Mementoオブジェクトを管理する役割

## なぜMementoパターンを使うのか

Mementoパターンを使用する主な理由は以下の通りです：

1. 状態の保存と復元が容易: オブジェクトの状態を簡単に保存し、必要に応じて復元できます。
2. カプセル化の維持: オブジェクトの内部状態を外部に公開せずに保存できます。
3. 責任の分離: 状態の保存と管理を別々のクラスで行うことができ、単一責任の原則に従えます。

## Mementoパターンの使い所

Mementoパターンは以下のような場面で特に有用です：

1. Undo/Redo機能の実装
2. トランザクション処理
3. バージョン管理システム
4. ゲームの状態保存

## メリット

1. 柔軟性: 必要な時に簡単に状態を保存・復元できます。
2. カプセル化: オブジェクトの内部状態を外部に露出せずに保存できます。
3. 拡張性: 新しい状態や操作を追加しやすい設計になります。

## デメリット

1. メモリ使用量: 多くの状態を保存する場合、メモリ使用量が増加する可能性があります。
2. パフォーマンス: 頻繁な状態の保存と復元は、パフォーマンスに影響を与える可能性があります。
3. 複雑さ: 単純なアプリケーションでは、この設計パターンが過剰になる場合があります。

## 実装例

以下に、Pythonを使用してMementoパターンを実装したテキストエディタの例を示します。

```mermaid
classDiagram
    class Originator {
        -state: String
        +setState(state: String): void
        +getState(): String
        +saveStateToMemento(): Memento
        +restoreStateFromMemento(memento: Memento): void
    }
    class Memento {
        -state: String
        +Memento(state: String)
        +getState(): String
    }
    class Caretaker {
        -mementoList: List~Memento~
        +add(memento: Memento): void
        +get(index: int): Memento
    }
    Originator ..> Memento : creates >
    Caretaker o--> Memento : stores

    class OriginatorNote {
        <<ロール説明>>
        状態を持つオブジェクト
        状態の保存と復元を行う
    }
    class MementoNote {
        <<ロール説明>>
        Originatorの状態のスナップショット
        不変オブジェクト
    }
    class CaretakerNote {
        <<ロール説明>>
        Mementoオブジェクトを管理
        状態の履歴を保持
    }
    class PatternNote {
        <<パターン説明>>
        Mementoパターン:
        オブジェクトの内部状態を保存し、
        後で復元できるようにする。
        カプセル化を破壊せずに
        オブジェクトの状態をキャプチャし、
        外部に保存することができる。
    }

    Originator .. OriginatorNote
    Memento .. MementoNote
    Caretaker .. CaretakerNote
    PatternNote .. Originator
    PatternNote .. Memento
    PatternNote .. Caretaker
```

```mermaid
sequenceDiagram
    participant Client
    participant Originator
    participant Memento
    participant Caretaker

    Note over Client,Caretaker: Mementoパターンの典型的な使用シナリオ

    Client->>Originator: setState("State 1")
    Client->>Originator: saveStateToMemento()
    Originator->>Memento: new Memento("State 1")
    Originator-->>Client: memento
    Client->>Caretaker: add(memento)

    Client->>Originator: setState("State 2")
    Client->>Originator: saveStateToMemento()
    Originator->>Memento: new Memento("State 2")
    Originator-->>Client: memento
    Client->>Caretaker: add(memento)

    Client->>Caretaker: get(0)
    Caretaker-->>Client: memento
    Client->>Originator: restoreStateFromMemento(memento)
    Originator->>Memento: getState()
    Memento-->>Originator: "State 1"
    
    Client->>Originator: getState()
    Originator-->>Client: "State 1"

    Note over Client,Caretaker: 状態が "State 1" に復元されました
```

```python
from typing import List

class EditorMemento:
    def __init__(self, content: str):
        self._content = content

    def get_content(self) -> str:
        return self._content

class TextEditor:
    def __init__(self):
        self._content = ""

    def type(self, text: str):
        self._content += text

    def get_content(self) -> str:
        return self._content

    def save(self) -> EditorMemento:
        return EditorMemento(self._content)

    def restore(self, memento: EditorMemento):
        self._content = memento.get_content()

class History:
    def __init__(self):
        self._mementos: List[EditorMemento] = []
        self._current_index = -1

    def push(self, memento: EditorMemento):
        self._mementos = self._mementos[:self._current_index + 1]
        self._mementos.append(memento)
        self._current_index += 1

    def undo(self) -> EditorMemento:
        if self._current_index > 0:
            self._current_index -= 1
            return self._mementos[self._current_index]
        return None

    def redo(self) -> EditorMemento:
        if self._current_index < len(self._mementos) - 1:
            self._current_index += 1
            return self._mementos[self._current_index]
        return None

class TextEditorApp:
    def __init__(self):
        self._editor = TextEditor()
        self._history = History()

    def type(self, text: str):
        self._editor.type(text)
        self._history.push(self._editor.save())

    def undo(self):
        memento = self._history.undo()
        if memento:
            self._editor.restore(memento)

    def redo(self):
        memento = self._history.redo()
        if memento:
            self._editor.restore(memento)

    def get_content(self) -> str:
        return self._editor.get_content()

# 使用例
if __name__ == "__main__":
    app = TextEditorApp()

    app.type("Hello ")
    app.type("World!")
    print(app.get_content())  # "Hello World!"

    app.undo()
    print(app.get_content())  # "Hello "

    app.redo()
    print(app.get_content())  # "Hello World!"

    app.type(" How are you?")
    print(app.get_content())  # "Hello World! How are you?"

    app.undo()
    app.undo()
    print(app.get_content())  # "Hello "
```

この実装では、`TextEditor`クラスが Originator、`EditorMemento`クラスが Memento、`History`クラスが Caretaker の役割を果たしています。`TextEditorApp`クラスはこれらを組み合わせて、実際のアプリケーションとしての機能を提供しています。

## 動作確認

上記のコードをPythonの実行環境で実行すると、Undo/Redo機能が正しく動作することを確認できます。コンソールに表示される出力を見ることで、テキストの追加、Undo、Redoの各操作が適切に処理されていることがわかります。

実行結果は以下のようになります：

```
Hello World!
Hello 
Hello World!
Hello World! How are you?
Hello 
```

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/26c5e29a-cb39-ddd1-7cad-04294258b6b2.png)

Pythonを使用してMementoパターンを実装することで、テキストエディタのUndo/Redo機能を効果的に実現できました。このパターンは状態管理を柔軟かつ拡張性の高い方法で行うことができ、複雑なアプリケーションの開発に非常に有用です。

Pythonの特徴を活かし、タイプヒントを使用することで、コードの可読性と保守性が向上しています。また、リスト内包表記や条件式を使用することで、簡潔で効率的なコードを書くことができます。

ただし、メモリ使用量やパフォーマンスへの影響を考慮する必要があります。特に、大量のテキストを扱う場合や、頻繁な状態変更が行われる場合は、メモリ使用量の最適化やパフォーマンスチューニングが必要になる可能性があります。

適切な使用場面を見極め、アプリケーションの要件に合わせて採用を検討することが重要です。Mementoパターンは、状態管理が重要な役割を果たすアプリケーションで特に威力を発揮します。