RitoLabo

Prototypeパターン | PHPデザインパターン

  • 公開:
  • カテゴリ: PHP DesignPatterns
  • タグ: PHP,DesignPatterns,Creation,FactoryMethod,Prototype

Prototypeパターンは、オブジェクトの生成に関するデザインパターン手法の1つで、インスタンスの複製に関する処理モデルです。プロトタイプ、つまりは「原型」。既にある型を利用して効率的にインスタンスを回していく手法になります。

Prototypeパターンでは大まかに、プロトタイプとなるインスタンスを流用し、作成するオブジェクトのタイプを指定する事で原型となるインスタンスのコピーを行いますが、その際に自分自身を複製する事から、よく細胞に例えられたりします。

また、オブジェクトのコピーに関して、シャロー(浅い)コピーとディープ(深い)コピーを意識する必要があります。

ちなみになぜインスタンスを生成せずコピーするのか。例えば状態を持つインスタンスを複製したい場合、再度インスタンスを生成した場合はその後、複製したいインスタンスと同じ状態に持っていく処理が必要になりますが、それならばそもそもコピーした方が早く効率的です。ただし、その場合にコピーの性質を把握し複製を行う事、そして、ただ複製するのではなく、インスタンスや状態の取り回しを行うマネージャーを挟む事で、疎結合な関係を構築する事、そういったエッセンスが詰まっているのがこのパターンの特徴になります。

逆を言えば、状態を持たないものは複製する必要がないので、このパターンの採用はそぐわないという事になります(そしてその場合はFactoryMethodパターンを採用します)。

Prototypeパターンの基本的なクラス図は以下になります。

Prototypeパターンのクラス図

一点補足すると、Prototypeクラスに関してはインターフェイスでの実装の場合もあります。

実装例

ある洋食屋さんのメニュー表を例にPrototypeパターンを実装していきます。

この店では看板メニューであるボロネーゼとナポリタンについて、メニュー表に都度店主からのコメントを付与し表示させています。

これについて、Prototypeパターンを用いて実装していきます。まずはPrototypeクラスです。

/prototype/App/Prototype/MenuPrototype.php
<?php

namespace App\Prototype;


abstract class MenuPrototype
{
private $menu_code;
private $name;
private $price;
private $category;
private $comments;

public function __construct($menu_code, $name, $price, $category)
{
$this->menu_code = $menu_code;
$this->name = $name;
$this->price = $price;
$this->category = $category;
}

public function getMenuCode() { return $this->menu_code; }
public function getName() { return $this->name; }
public function getPrice() { return $this->price; }
public function getCategory() { return $this->category; }
public function getComments() { return $this->comments; }

public function setComments(\stdClass $comment)
{
$this->comments = $comment;
}

public function cngComment($idx, $comment)
{
$this->comments->comment[$idx] = $comment;
}

private function getData()
{
return [
'name' => $this->getName(),
'price' => $this->getPrice(),
'category' => $this->getCategory(),
'comments' => $this->getComments()
];
}

public function display()
{
$data = $this->getData();

$html = '';
$html .= '<ul>';
$html .= $this->getHtmlList($data['name']);
$html .= $this->getHtmlList($data['price']);
$html .= $this->getHtmlList($data['category']);
$html .= '<li><ul>';
foreach ($data['comments']->comment as $comment) {
$html .= $this->getHtmlList($comment['comment']);
}
$html .= '</li></ul>';
$html .= '</ul>';

return $html;
}

private function getHtmlList($value)
{
return sprintf('<li>%s</li>', $value);
}

public function newInstance()
{
return clone $this;
}

protected abstract function __clone();
}

メニューを形成する為のメンバ変数と、ゲッタセッタなどはよくある基本の通りですが、ポイントは2点、自身のインスタンスをcloneして返す newInstance() メソッドと、継承先で実装を行うようにabstractで指定された __clone() メソッドです。

次に、この親クラスを継承してそれぞれのコピーパターンに即したサブクラスを定義していきます。

/prototype/App/Prototype/Concretes/DeepCopyMenu.php
<?php

namespace App\Prototype\Concretes;

use App\Prototype\MenuPrototype;

class DeepCopyMenu extends MenuPrototype
{
protected function __clone()
{
$this->setComments(clone $this->getComments());
}
}

ディープコピーを行うので、__clone()メソッドで保持しているオブジェクトをcloneしセットしています。

/prototype/App/Prototype/Concretes/ShallowCopyMenu.php
<?php

namespace App\Prototype\Concretes;

use App\Prototype\MenuPrototype;

class ShallowCopyMenu extends MenuPrototype
{
protected function __clone()
{
}
}

こちらはシャローコピーなのでメソッドの宣言のみになっています。

次に、オブジェクトの取り回しを行うマネージャーを定義します。

/prototype/App/Prototype/Manager/MenuManager.php
<?php
namespace App\Prototype\Manager;

use App\Prototype\MenuPrototype;

class MenuManager
{
private $menus;

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

public function register(MenuPrototype $menuPrototype)
{
$this->menus[$menuPrototype->getMenuCode()] = $menuPrototype;
}

public function create($menu_code)
{
if (!array_key_exists($menu_code, $this->menus)) {
throw new Exception(sprintf('要求されたメニュー番号 %s は存在しません', $menu_code));
}

return $this->menus[$menu_code]->newInstance();
}
}

register() メソッドでメニューデータの登録を行い、create() メソッドでデータに関するインスタンスを提供します。

最後にこれらを用いて動作確認を行います。

<?php

use App\Prototype\Manager\MenuManager;
use App\Prototype\Concretes\DeepCopyMenu;
use App\Prototype\Concretes\ShallowCopyMenu;

function executeCopy(MenuManager $manager, $menu_code) {
// インスタンス生成
$menu1 = $manager->create($menu_code); // オリジナル
$menu2 = $manager->create($menu_code); // コピー

// オリジナルのコメントを変更
$menu1->cngComment(1, [ 'date' => '2018-06-27', 'comment' => '大盛サービスは終了しました。']);

// 結果表示
echo '<h2>オリジナル</h2>';
echo $menu1->display();
echo '<h2>コピー</h2>';
echo $menu2->display();
echo '<hr>';
}

// メニューマネージャーのインスタンス化
$manager = new MenuManager();


// ディープコピーインスタンス
$menu = new DeepCopyMenu('P001', 'ボロネーゼ', 1350, 'pasta');
$comments = new \stdClass();
$comments->comment[] = [
'date' => '2018-06-23',
'comment' => 'イタリアのトマトたっぷり。'
];
$comments->comment[] = [
'date' => '2018-06-25',
'comment' => '大盛無料です。'
];
$menu->setComments($comments);
$manager->register($menu);


// シャローコピーインスタンス
$menu = new ShallowCopyMenu('P002', 'ナポリタン', 1100, 'pasta');
$comments = new \stdClass();
$comments->comment[] = [
'date' => '2018-06-23',
'comment' => '昔なつかしい味に仕上げました。'
];
$comments->comment[] = [
'date' => '2018-06-25',
'comment' => '大盛無料です'
];
$menu->setComments($comments);
$manager->register($menu);


// メニューコードを指定してオリジナルとコピーのインスタンスを作成&表示
executeCopy($manager, 'P001');
executeCopy($manager, 'P002');

少しソースが長めですが、行っている事はシンプルです。

$manager = new MenuManager();

まずはインスタンスの取り回しを行うマネージャをインスタンス化しています。

$menu = new DeepCopyMenu('P001', 'ボロネーゼ', 1350, 'pasta');
$comments = new \stdClass();
$comments->comment[] = [
'date' => '2018-06-23',
'comment' => 'イタリアのトマトたっぷり。'
];
$comments->comment[] = [
'date' => '2018-06-25',
'comment' => '大盛無料です。'
];
$menu->setComments($comments);
$manager->register($menu);

ディープ(深い)コピーのインスタンスを生成し、1つめのメニューを格納、そしてマネージャへメニューオブジェクトを登録しています。

$menu = new ShallowCopyMenu('P002', 'ナポリタン', 1100, 'pasta');
$comments = new \stdClass();
$comments->comment[] = [
'date' => '2018-06-23',
'comment' => '昔なつかしい味に仕上げました。'
];
$comments->comment[] = [
'date' => '2018-06-25',
'comment' => '大盛無料です'
];
$menu->setComments($comments);
$manager->register($menu);

シャロー(浅い)コピーのインスタンスを生成し、2つめのメニューを格納、そしてマネージャへメニューオブジェクトを登録しています。

この時点で、デーィプコピーインスタンスのボロネーゼとシャローコピーインスタンスのナポリタンがマネージャーに登録されている状態になっています。

そして最後にexecuteCopy()メソッドを叩いていますが、検証用のメソッドとして、それぞれのメニューコードのオリジナルとコピーのインスタンスを取得し、そこへ1カ所、コメントを変更しています。

結果は以下になります。

Prototypeパターン実行結果

「大盛サービスは終了しました。」というコメントでオリジナルに変更を行いましたが、ディープコピーの場合はオリジナル側のみが変更されているのに対し、シャローコピーの場合はコピー側も影響を受け変更されている事が確認できます。

まとめ

Prototypeパターンではインスタンスのコピーを用いて新たなオブジェクトを作成しますが、例えば現状から何かの検証を行ったりする、つまり、数通りの計算パターンを試したりとか、そういった事にも使えます。

性質を理解することで、様々な用途で用いる事が出来るので、是非試してみてください。