![Prototype](/images/patterns/cards/prototype-mini.png?id=bc3046bb39ff36574c08d49839fd1c8e)
Prototype を PHP で
Prototype は、 生成に関するデザインパターンの一つで、 特定のクラスに結合することなく、 オブジェクト (たとえ複雑なオブジェクトでも) のクローン作成を可能とします。
プロトタイプのクラス全部には、 共通するインターフェースが必要です。 これにより、 具象クラスが不明であってもオブジェクトを複製することが可能となります。 プロトタイプ・オブジェクトが、 完全なコピーを生成できるのは、 同じクラスのオブジェクト同士が非公開フィールドを互いにアクセスできるからです。
複雑度:
人気度:
使用例: Prototype パターンは、 PHP では、 初めから利用可能です。 オブジェクトの完全なコピーは、 clone
キーワードを使って取得できます。 クラスにクローンのサポートを追加するには、 __clone
メソッドを実装する必要があります。
見つけ方: このパターンは、 clone
や copy
といったメソッドで容易に識別可能です。
概念的な例
この例は、 Prototype デザインパターンの構造を説明するためのものです。 以下の質問に答えることを目的としています:
- どういうクラスからできているか?
- それぞれのクラスの役割は?
- パターンの要素同士はどう関係しているのか?
ここでパターンの構造を学んだ後だと、 これに続く、 現実世界の PHP でのユースケースが理解しやすくなります。
index.php: 概念的な例
<?php
namespace RefactoringGuru\Prototype\Conceptual;
/**
* The example class that has cloning ability. We'll see how the values of field
* with different types will be cloned.
*/
class Prototype
{
public $primitive;
public $component;
public $circularReference;
/**
* PHP has built-in cloning support. You can `clone` an object without
* defining any special methods as long as it has fields of primitive types.
* Fields containing objects retain their references in a cloned object.
* Therefore, in some cases, you might want to clone those referenced
* objects as well. You can do this in a special `__clone()` method.
*/
public function __clone()
{
$this->component = clone $this->component;
// Cloning an object that has a nested object with backreference
// requires special treatment. After the cloning is completed, the
// nested object should point to the cloned object, instead of the
// original object.
$this->circularReference = clone $this->circularReference;
$this->circularReference->prototype = $this;
}
}
class ComponentWithBackReference
{
public $prototype;
/**
* Note that the constructor won't be executed during cloning. If you have
* complex logic inside the constructor, you may need to execute it in the
* `__clone` method as well.
*/
public function __construct(Prototype $prototype)
{
$this->prototype = $prototype;
}
}
/**
* The client code.
*/
function clientCode()
{
$p1 = new Prototype();
$p1->primitive = 245;
$p1->component = new \DateTime();
$p1->circularReference = new ComponentWithBackReference($p1);
$p2 = clone $p1;
if ($p1->primitive === $p2->primitive) {
echo "Primitive field values have been carried over to a clone. Yay!\n";
} else {
echo "Primitive field values have not been copied. Booo!\n";
}
if ($p1->component === $p2->component) {
echo "Simple component has not been cloned. Booo!\n";
} else {
echo "Simple component has been cloned. Yay!\n";
}
if ($p1->circularReference === $p2->circularReference) {
echo "Component with back reference has not been cloned. Booo!\n";
} else {
echo "Component with back reference has been cloned. Yay!\n";
}
if ($p1->circularReference->prototype === $p2->circularReference->prototype) {
echo "Component with back reference is linked to original object. Booo!\n";
} else {
echo "Component with back reference is linked to the clone. Yay!\n";
}
}
clientCode();
Output.txt: 実行結果
Primitive field values have been carried over to a clone. Yay!
Simple component has been cloned. Yay!
Component with back reference has been cloned. Yay!
Component with back reference is linked to the clone. Yay!
現実的な例
Prototype パターンは、 すべてのフィールドを直接コピーしてオブジェクトを再構築する代わりに、 既存のオブジェクトを複製する便利な方法を提供します。 直接的なやり方は、 クローンされるオブジェクトのクラスに密に結合してしまうばかりか、 非公開フィールドの内容はコピーできないという問題があります。 Prototype では、 実際のクローンの作業は、 クローンされるクラス中で行われるため、 非公開フィールドへのアクセスは無制限です。
この例では、 複雑な Page オブジェクトのクローン作成に Prototype パターンを使用しています。 Page クラスには多数の非公開フィールドがありますが、 Prototype パターンのおかげで、 クローンされたオブジェクトにうまく引き継がれます。
index.php: 現実的な例
<?php
namespace RefactoringGuru\Prototype\RealWorld;
/**
* Prototype.
*/
class Page
{
private $title;
private $body;
/**
* @var Author
*/
private $author;
private $comments = [];
/**
* @var \DateTime
*/
private $date;
// +100 private fields.
public function __construct(string $title, string $body, Author $author)
{
$this->title = $title;
$this->body = $body;
$this->author = $author;
$this->author->addToPage($this);
$this->date = new \DateTime();
}
public function addComment(string $comment): void
{
$this->comments[] = $comment;
}
/**
* You can control what data you want to carry over to the cloned object.
*
* For instance, when a page is cloned:
* - It gets a new "Copy of ..." title.
* - The author of the page remains the same. Therefore we leave the
* reference to the existing object while adding the cloned page to the list
* of the author's pages.
* - We don't carry over the comments from the old page.
* - We also attach a new date object to the page.
*/
public function __clone()
{
$this->title = "Copy of " . $this->title;
$this->author->addToPage($this);
$this->comments = [];
$this->date = new \DateTime();
}
}
class Author
{
private $name;
/**
* @var Page[]
*/
private $pages = [];
public function __construct(string $name)
{
$this->name = $name;
}
public function addToPage(Page $page): void
{
$this->pages[] = $page;
}
}
/**
* The client code.
*/
function clientCode()
{
$author = new Author("John Smith");
$page = new Page("Tip of the day", "Keep calm and carry on.", $author);
// ...
$page->addComment("Nice tip, thanks!");
// ...
$draft = clone $page;
echo "Dump of the clone. Note that the author is now referencing two objects.\n\n";
print_r($draft);
}
clientCode();
Output.txt: 実行結果
Dump of the clone. Note that the author is now referencing two objects.
RefactoringGuru\Prototype\RealWorld\Page Object
(
[title:RefactoringGuru\Prototype\RealWorld\Page:private] => Copy of Tip of the day
[body:RefactoringGuru\Prototype\RealWorld\Page:private] => Keep calm and carry on.
[author:RefactoringGuru\Prototype\RealWorld\Page:private] => RefactoringGuru\Prototype\RealWorld\Author Object
(
[name:RefactoringGuru\Prototype\RealWorld\Author:private] => John Smith
[pages:RefactoringGuru\Prototype\RealWorld\Author:private] => Array
(
[0] => RefactoringGuru\Prototype\RealWorld\Page Object
(
[title:RefactoringGuru\Prototype\RealWorld\Page:private] => Tip of the day
[body:RefactoringGuru\Prototype\RealWorld\Page:private] => Keep calm and carry on.
[author:RefactoringGuru\Prototype\RealWorld\Page:private] => RefactoringGuru\Prototype\RealWorld\Author Object
*RECURSION*
[comments:RefactoringGuru\Prototype\RealWorld\Page:private] => Array
(
[0] => Nice tip, thanks!
)
[date:RefactoringGuru\Prototype\RealWorld\Page:private] => DateTime Object
(
[date] => 2018-06-04 14:50:39.306237
[timezone_type] => 3
[timezone] => UTC
)
)
[1] => RefactoringGuru\Prototype\RealWorld\Page Object
*RECURSION*
)
)
[comments:RefactoringGuru\Prototype\RealWorld\Page:private] => Array
(
)
[date:RefactoringGuru\Prototype\RealWorld\Page:private] => DateTime Object
(
[date] => 2018-06-04 14:50:39.306272
[timezone_type] => 3
[timezone] => UTC
)
)