![Прототип](/images/patterns/cards/prototype-mini.png?id=bc3046bb39ff36574c08d49839fd1c8e)
Прототип на PHP
Прототип — это порождающий паттерн, который позволяет копировать объекты любой сложности без привязки к их конкретным классам.
Все классы—Прототипы имеют общий интерфейс. Поэтому вы можете копировать объекты, не обращая внимания на их конкретные типы и всегда быть уверены, что получите точную копию. Клонирование совершается самим объектом-прототипом, что позволяет ему скопировать значения всех полей, даже приватных.
Сложность:
Популярность:
Применимость: Возможность клонирования объектов встроена в PHP. При помощи ключевого слова clone
вы можете сделать точную копию объекта. Чтобы добавить поддержку клонирования в класс, необходимо реализовать метод __clone
.
Признаки применения паттерна: Прототип легко определяется в коде по наличию ключевого слова clone
и реализаций метода __clone
.
Концептуальный пример
Этот пример показывает структуру паттерна Прототип, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\Prototype\Conceptual;
/**
* Пример класса, имеющего возможность клонирования. Мы посмотрим как происходит
* клонирование значений полей разных типов.
*/
class Prototype
{
public $primitive;
public $component;
public $circularReference;
/**
* PHP имеет встроенную поддержку клонирования. Вы можете «клонировать»
* объект без определения каких-либо специальных методов, при условии, что
* его поля имеют примитивные типы. Поля, содержащие объекты, сохраняют свои
* ссылки в клонированном объекте. Поэтому в некоторых случаях вам может
* понадобиться клонировать также и вложенные объекты. Это можно сделать
* специальным методом clone.
*/
public function __clone()
{
$this->component = clone $this->component;
// Клонирование объекта, который имеет вложенный объект с обратной
// ссылкой, требует специального подхода. После завершения клонирования
// вложенный объект должен указывать на клонированный объект, а не на
// исходный объект.
$this->circularReference = clone $this->circularReference;
$this->circularReference->prototype = $this;
}
}
class ComponentWithBackReference
{
public $prototype;
/**
* Обратите внимание, что конструктор не будет выполнен во время
* клонирования. Если у вас сложная логика внутри конструктора, вам может
* потребоваться выполнить ее также и в методе clone.
*/
public function __construct(Prototype $prototype)
{
$this->prototype = $prototype;
}
}
/**
* Клиентский код.
*/
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!
Пример из реальной жизни
Паттерн Прототип предоставляет удобный способ репликации существующих объектов вместо их восстановления и копирования всех полей напрямую. Прямое копирование не только связывает вас с классами клонируемых объектов, но и не позволяет копировать содержимое приватных полей. Паттерн Прототип позволяет выполнять клонирование в контексте клонированного класса, где доступ к приватным полям класса не ограничен.
В этом примере показано, как клонировать сложный объект Страницы, используя паттерн Прототип. Класс Страница имеет множество приватных полей, которые будут перенесены в клонированный объект благодаря паттерну Прототип.
index.php: Пример из реальной жизни
<?php
namespace RefactoringGuru\Prototype\RealWorld;
/**
* Прототип.
*/
class Page
{
private $title;
private $body;
/**
* @var Author
*/
private $author;
private $comments = [];
/**
* @var \DateTime
*/
private $date;
// +100 приватных полей.
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;
}
/**
* Вы можете контролировать, какие данные вы хотите перенести в
* клонированный объект.
*
* Например, при клонировании страницы:
* - Она получает новый заголовок «Копия ...».
* - Автор страницы остаётся прежним. Поэтому мы оставляем ссылку на
* существующий объект, добавляя клонированную страницу в список страниц
* автора.
* - Мы не переносим комментарии со старой страницы.
* - Мы также прикрепляем к странице новый объект даты.
*/
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;
}
}
/**
* Клиентский код.
*/
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
)
)