Posted at

【PHPデザインパターン】19_Prototype~コピーして作る


引用記事

この記事を書くきっかけになったブログです。

記事内の解説やソースコードは、こちらのブログと著者の公開リポジトリを参考にしています。


Do You PHP はてな〜[doyouphp][phpdp]PHPによるデザインパターン入門 - Prototype~コピーして作る



概要


  • 「prototype」は「試作品」「原型」という意味。

  • オブジェクトをコピーして、新しいオブジェクトを生成する。

  • オブジェクトの生成処理を隠蔽し、クラス同士の関係をゆるくできる。


構成要素


Prototypeクラス


  • コピーするためのメソッドを定義する親クラス。


ConcretePrototypeクラス


  • Prototypeクラスのサブクラス。


Clientクラス


  • Prototype型のオブジェクトを利用して、新しいインスタンスを生成する。


実演


処理の流れ



  • __cloneメソッドを使用し、ItemPrototype型オブジェクトをコピーする。

  • ItemPrototype型オブジェクト内部では、stdClass型オブジェクトへの参照を行なっている。

  • __cloneメソッドの場合、オブジェクトの参照はコピーできないため「浅いコピー」となる。

  • オブジェクトの参照をコピーする場合は、ItemPrototypeクラスのサブクラスで__cloneメソッドを実装する必要がある。

  • 今回は「浅いコピー」を行うShallowCopyItemクラスと、「深いコピー」を行うDeepCopyItemクラスを定義する。

  • クライアントから新しいインスタンスを要求された時は、ItemManagerクラスでオブジェクトをコピーする。


ファイル構造

MyPrototype

  ├── DeepCopyItem.php
  ├── ItemManager.php
  ├── ItemPrototype.php
  ├── ShallowCopyItem.php
  └── my_client.php


ソースコード


Prototypeクラス


ItemPrototype.php


ItemPrototype.php

<?php

namespace DoYouPhp\PhpDesignPattern\Prototype\MyPrototype;

/**
* Prototypeクラスに相当する
* 複製するためのメソッドを定義する
*/

abstract class ItemPrototype
{
// ItemPrototype型オブジェクトのプロパティ
private $item_code;
private $item_name;
// 定義済みクラスであるstdClass型オブジェクトをセットする
private $detail;

public function __construct($code, $name)
{
$this->item_code = $code;
$this->item_name = $name;
}

public function getCode()
{
return $this->item_code;
}

public function getName()
{
return $this->item_name;
}

// $detailのみ、stdClass型のオブジェクトとする
// ItemPrototype型オブジェクトが内包している形になる
public function setDetail(\stdClass $detail)
{
$this->detail = $detail;
}

public function getDetail()
{
return $this->detail;
}

// 各プロパティを出力する
public function dumpData()
{
echo '商品名:'.$this->getName().'<br>'."\n";
echo '商品番号:'.$this->getCode().'<br>'."\n";
echo 'コメント:'.$this->detail->comment.'<br>'."\n";
}

// cloneを使ってインスタンスを複製する
// 実装はサブクラスで行う
public function newInstance()
{
$new_instance = clone $this;

return $new_instance;
}

// protectedキーワードで、このクラスとサブクラス以外はメソッドを呼び出せないようにする
// 外部から直接cloneされることはない
abstract protected function __clone();
}



ConcretePrototypeクラス


DeepCopyItem.php


DeepCopyItem.php

<?php

namespace DoYouPhp\PhpDesignPattern\Prototype\MyPrototype;

use DoYouPhp\PhpDesignPattern\Prototype\MyPrototype\ItemPrototype;

/**
* ConcretePrototypeクラスに相当する
*/

class DeepCopyItem extends ItemPrototype
{
// 深いコピーを行うための実装
// 内包しているstdClass型オブジェクトも複製する
protected function __clone()
{
$this->setDetail(clone $this->getDetail());
}
}



ShallowCopyItem.php


ShallowCopyItem.php

<?php

namespace DoYouPhp\PhpDesignPattern\Prototype\MyPrototype;

use DoYouPhp\PhpDesignPattern\Prototype\MyPrototype\ItemPrototype;

/**
* ConcretePrototypeクラスに相当する
*/

class ShallowCopyItem extends ItemPrototype
{
// 浅いコピーを行うので、空の実装を行う
protected function __clone()
{
}
}



Clientクラス


ItemManager.php


ItemManager.php

<?php

namespace DoYouPhp\PhpDesignPattern\Prototype\MyPrototype;

use DoYouPhp\PhpDesignPattern\Prototype\MyPrototype\ItemPrototype;

/**
* Clientクラスに相当する
* ItemPrototype型オブジェクトを利用して、新しいインスタンスを生成する
*/

class ItemManager
{
private $items;

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

// $item_codeをキーとする配列に、ItemPrototype型インスタンスをセットする
public function registItem(ItemPrototype $item)
{
$this->items[$item->getCode()] = $item;
}

// インスタンスを複製する
public function create($item_code)
{
// 指定した$item_codeを持つインスタンスが配列に存在しない場合は、例外を投げる
// 存在する場合は、インスタンスの複製を行う
if (!array_key_exists($item_code, $this->items)) {
throw new \Exception('コード'.$item_code.'の商品は存在しません');
}

$cloned_item = $this->items[$item_code]->newInstance();

return $cloned_item;
}
}



Client


my_client.php


my_client.php

<?php

namespace DoYouPhp\PhpDesignPattern\Prototype\MyPrototype;

require dirname(dirname(__DIR__)).'/vendor/autoload.php';

use DoYouPhp\PhpDesignPattern\Prototype\MyPrototype\ItemManager;
use DoYouPhp\PhpDesignPattern\Prototype\MyPrototype\DeepCopyItem;
use DoYouPhp\PhpDesignPattern\Prototype\MyPrototype\ShallowCopyItem;

// オブジェクト複製後、内包しているオブジェクトを変更して出力するメソッド
function testCopy(ItemManager $manager, $item_code)
{
// コピー用のメソッド
// 同型のインスタンスを2つ生成して比較する
$item1 = $manager->create($item_code);
$item2 = $manager->create($item_code);

// 内包しているstdClass型オブジェクトのプロパティを変更する
$item2->getDetail()->comment = '書き換えたコメント';

// 出力する
echo '----- オリジナル -----'.'<br>'."\n";
$item1->dumpData();
echo '----- コピー -----'.'<br>'."\n";
$item2->dumpData();
}

$manager = new ItemManager();

// 商品データの作成
// ItemPrototype型オブジェクトを生成する
// オブジェクトの中で、stdClass型オブジェクトへの参照を内包している

// 深いコピーを行う
// 各オブジェクトは複製された個々の$detailを参照する
// そのため、コピー後にstdClass型オブジェクトのプロパティを変更した場合、他の同型オブジェクトに影響はない
$item = new DeepCopyItem(1, 'apple');
$detail = new \stdClass();
$detail->comment = 'appleテストコメント';
$item->setDetail($detail);
$manager->registItem($item);

// 浅いコピーを行う
// $detail自身は複製されないので、各オブジェクトは共通の$detailを参照する
// そのため、コピー後にstdClass型オブジェクトのプロパティを変更した場合、他の同型オブジェクトに影響がある
$item = new ShallowCopyItem(2, 'orange');
$detail = new \stdClass();
$detail->comment = 'orangeテストコメント';
$item->setDetail($detail);
$manager->registItem($item);

// オブジェクトの参照の変化を確認する
try {
echo '<深いコピー>'.'<br>'."\n";
testCopy($manager, 1);

echo '<浅いコピー>'.'<br>'."\n";
testCopy($manager, 2);

testCopy($manager, 3);
} catch (\Exception $e) {
echo '<例外>'.'<br>'."\n";
echo $e->getMessage();
}