Python

Python デザインパターンを学ぶ Mementoパターン

概要

Mementoパターンは、オブジェクトがその状態の履歴を外部に保持することを可能にすること(したがって、復帰を実行できる)。バージョンの履歴を保持したり、古い状態に戻したりする必要がある場合、いくつかの方法で対処することができる。

CommandパターンとMementoパターンの2つの方法があり、それぞれに長所と短所がある。Commandパターンでは、起動者は異なるバージョンの状態を知らないまま、状態変更の原因となったコマンドを元に戻すことができる。一方、Mementoは状態を変化させたアクションを記録するのではなく、選択されたイタレーションで変化した状態のコピーをキャッシュする。

これにより、Commandパターンはスペース効率が良くなるが、速度が低下する可能性がある(望ましい状態になるまで、各アクションを逆の順序で実行する必要がある)。また、Commandの変更を元に戻すには、自明ではないアクションが必要になるという問題もある。Mementoパターンは、前述の問題を解決し、元に戻す際の時間的な複雑さを大幅に改善するが(記録された任意の状態を一定時間で戻すことができる)、チェックポイントごとに変更された状態の完全なコピーを保存することで、空間的な複雑さを大幅に悪化させる。

次の実装例では、仮想マップビルダをシミュレートする。これは、ユーザがタイルベースのマップを構築しながら、進捗状況のチェックポイントを保存し、いつでも以前の状態に戻せるようにするものだ。

実装例

MapBuilderクラスは、バージョン管理が必要な内部状態を保存する「Originator」として実装されている。CheckpointLogクラスは、Originatorの状態の異なるバージョンの履歴ログを保存する「Caretaker」として実装されている。MapCheckpointクラスは、「Memento」の役割を果たし、マップのスナップショットの状態とメタデータ(チェックポイントを説明するメッセージ)を保存する。

MapBuilderクラスは、 Originator として、地図の作成、描画、管理を行う。また、適切な間隔でチェックポイントを作成し、それを外部のログに保存する役割も担っている。マップの状態は、保存されたチェックポイントにいつでも戻すことができる。

CheckpointLogクラスの1つのインスタンスは、MapBuilderの1つのインスタンスのチェックポイントを記録するために使用する必要がある。max_lengthには、ログの長さを指定できる(デフォルトは3)revert関数では、古い MapCheckpoint を返す。実際の使用例では、このメソッドは、元に戻された後のチェックポイントを再配置または削除する役割も担っている可能性もある。

import copy


class MapCheckpoint:
    def __init__(self, map_state, message):

        self.map_state = map_state
        self.message = message


class MapBuilder:

    def __init__(self, map_size):
        self.map_size = map_size
        self.map = self._build_default_map()

    def draw(self, coords, value):
        if not (coords[0] < self.map_size[0] and coords[1] < self.map_size[1]):
            print("Coordinates not in range.")
            return False

        self.map[coords[1]][coords[0]] = value

    def create_checkpoint(self, message):

        print("Create new checkpoint: <{}>".format(message))
        return MapCheckpoint(map_state=copy.deepcopy(self.map), message=message)

    def restore_from_checkpoint(self, checkpoint):
        print("Restore checkpoint: <{}>".format(checkpoint.message))
        self.map = copy.deepcopy(checkpoint.map_state)

    def _build_default_map(self):
        return [["-" for x in range(self.map_size[0])]
                for y in range(self.map_size[1])]

    def print_map(self):

        for row in self.map:
            print(row)
        print("\n")


class CheckpointLog():

    def __init__(self, max_length=3):

        self._checkpoints = []
        self.max_length = max_length

    def add(self, new_checkpoint):
        if len(self._checkpoints) == self.max_length:
            del self._checkpoints[0]

        self._checkpoints.append(new_checkpoint)

    def revert(self, revert_count=1):

        return self._checkpoints[-revert_count]


if __name__ == "__main__":

    map_builder = MapBuilder((3, 3))
    cp_log = CheckpointLog()

    map_builder.draw(coords=(1, 2), value="x")
    cp_log.add(map_builder.create_checkpoint("x:1, y:0"))
    map_builder.print_map()

    map_builder.draw(coords=(2, 1), value="y")
    cp_log.add(map_builder.create_checkpoint("x:1, y:1"))
    map_builder.print_map()

    map_builder.draw(coords=(2, 2), value="y")
    cp_log.add(map_builder.create_checkpoint("x:1, y:2"))
    map_builder.print_map()

    map_builder.restore_from_checkpoint(cp_log.revert(3))
    map_builder.print_map()