RitoLabo

Mementoパターン - PHPデザインパターン

  • 公開:
  • カテゴリ: PHP DesignPatterns
  • タグ: PHP,Behavior,DesignPatterns,Memento

Mementoパターン(メメント・パターン)は、振る舞いに関するデザインパターン手法の1つで、オブジェクトの状態をスナップショットとして保存しておく事で、必要な時のその状態をリストアできる処理モデルです。

Mementoパターンの基本的なクラス図は以下の通りです。

Mementoパターンクラス図

実装例

今回は、デザインパターンの一覧を登録・書き出ししていく中でのスナップショットとリストアの流れを実装していきます。

PHPを勉強したてのジョンは書籍「オブジェクト指向における再利用のためのデザインパターン」を読んでデザインパターンを学んだので、それを出典順にPHPで書き出して表示させるプログラムを書こうと思い立ちました。

ただし、GoF本には23個のデザインパターンが紹介されており、途中で間違えたりするのが少し心配です。そこでジョンは、Memento パターンを取り入れて、途中でスナップショットを取れる仕組みを構築しつつ進めていく事にしました。

まずはMementoクラスから。スナップショットを保持するクラスです。

App/Memento/Memento.php
<?php

namespace App\Memento;

class Memento
{
private $list;

public function __construct($list)
{
$this->list = $list;
}

public function getList()
{
return $this->list;
}
}

構造はシンプルで、コンストラクタで保持すべきリストを受け取る、そしてそのリストを返すgetList() メソッドだけです。

次はOriginatorクラスです。

App/Originator/Originator.php
<?php

namespace App\Originator;

use App\Memento\Memento;

class Originator
{
private $list = [];

public function createMemento()
{
return new Memento($this->list);
}
public function restoreMemento(Memento $memento)
{
$this->list = $memento->getList();
}

public function addList($list)
{
$this->list[] = $list;
}

public function getList()
{
return $this->list;
}

public function display()
{
foreach ($this->getList() as $item) {
echo sprintf('<li>%s</li>', $item);
}
}
}

作成者ともいわれるこのクラスでは、道中の状態の取り回し、自身の状態を保存する為のMementoクラスの作成、そしてMementoクラスを用いたリストアを実装しています。

最後に、Caretakerクラスです。

App/Caretaker/Caretaker.php
<?php

namespace App\Caretaker;

class Caretaker
{
private $snapshot;

public function setSnapshot($snapshot)
{
$this->snapshot = $snapshot;
}

public function getSnapshot()
{
return $this->snapshot;
}
}

世話人ともいわれるこのクラスでは、リストアを行う際にOriginatorが要求したスナップショットを提供する役割を担っています。いわばMementoの管理クラスです。今回はシンプルな構造ですが、ここを発展させていくと世代管理も行えます。

実装が完了したので、クライアントから利用してみます。

index.html
<?php

use App\Caretaker\Caretaker;
use App\Originator\Originator;

$caretaker = new Caretaker();
$originator = new Originator();

echo 'いまからデザインパターンをGoF本の出展順で登録していくぞ。<br><br>';

$originator->addList('Iterator パターン');
$originator->addList('Adapter パターン');
$originator->addList('TemplateMethod パターン');

echo 'まずは3つ登録。出力してみよう。<br>';
echo '<ol>';
$originator->display();
echo '</ol>';

echo 'まだまだ、がんばろう。<br><br>';

$originator->addList('FactoryMethod パターン');
$originator->addList('Singleton パターン');
$originator->addList('Prototype パターン');

echo 'ふう、やっと6個。出力してみよう。<br>';
echo '<ol>';
$originator->display();
echo '</ol>';

echo 'いいね、ここで一旦スナップショットを取っておこう。<br><br>';
$caretaker->setSnapshot($originator->createMemento());


$originator->addList('Builder パターン');
$originator->addList('AbstractFactory パターン');
$originator->addList('Bridge パターン');
$originator->addList('Composite パターン');

echo 'ここで10個か、もう少しだ。出力してみよう。<br>';
echo '<ol>';
$originator->display();
echo '</ol>';

echo 'あ!Strategy パターンを入れ忘れてる!うむむ、、リストアしよう。<br><br>';
$originator->restoreMemento($caretaker->getSnapshot());

echo 'リストアできているかな?<br>';
echo '<ol>';
$originator->display();
echo '</ol>';

echo 'ちゃんとリストアできてた!よーしこのまま一気にいくぞ!<br><br>';

$originator->addList('Builder パターン');
$originator->addList('AbstractFactory パターン');
$originator->addList('Bridge パターン');
$originator->addList('Strategy パターン');
$originator->addList('Composite パターン');
$originator->addList('Decorator パターン');
$originator->addList('Visitor パターン');
$originator->addList('ChainOfResponsibility パターン');
$originator->addList('Facade パターン');
$originator->addList('Mediator パターン');
$originator->addList('Observer パターン');
$originator->addList('Memento パターン');
$originator->addList('State パターン');
$originator->addList('Flyweight パターン');
$originator->addList('Proxyパターン');
$originator->addList('Command パターン');
$originator->addList('Interpreter パターン');

echo '<ol>';
$originator->display();
echo '</ol>';

echo 'よしできた!ふう、よかったよかった。';

少し長いですが、ジョンの目的を達成する為にお付き合いください。特筆すべきは、中盤でスナップショットを取り、現時点での状態を保存している点です。そしてその後、そのスナップショットを使って以前のデータへリストアを行っています。

結果は以下になります。

PHPでのMementoパターン実行結果

まとめ

Memento = 記念品・形見」を意味しますが、Originator(作成者)がMemento(記念品)を生成し、Caretaker(世話人)を介してスナップショットを記録・保持・管理するという流れがこのデザインパターン手法の基本的な流れです。

ポイントとしては、OriginatorはMementoを保持せず、管理はあくまでもCaretakerに任せる事です。こうする事で互いが分離し依存が無くなるので、拡張が容易になります。例えば、Caretakerを拡張し、スナップショットに履歴を持たせる事も簡単に行えるようになります。

ただし、CaretakerはMementoオブジェクトを変更してはならないという決まりがあります。Mementoは不透明オブジェクトであり、OriginatorやCaretakerがMementoを変更できないような関係を築く事が、完全なるMementoパターンとなります。

さらに、Mementoクラスで保持するスナップショットに関しては、オブジェクトなどを突っ込むというよりは、状態を戻すのに必要最小限の情報のみを格納していく事が望ましいとされています(メモリを圧迫する為)。

とはいえ、カプセル化を破壊せずに状態を元に戻す事の出来るMementoパターンは覚えておくとふとした時に役に立つと思います。是非試してみてください。