PHP: Прототип

Prototype Прототип Prototype

Прототип — это порождающий паттерн, который позволяет копировать объекты любой сложности без привязки к их конкретным классам.

Все классы—Прототипы имеют общий интерфейс. Поэтому вы можете копировать объекты, не обращая внимания на их конкретные типы и всегда быть уверены, что получите точную копию. Клонирование совершается самим объектом-прототипам, что позволяет ему скопировать значения всех полей, даже приватных.

Подробней о Прототипе

Особенности паттерна на PHP

Сложность:

Популярность:

Применимость: Возможность клонирования объектов встроена в PHP. При помощи ключевого слова clone вы можете сделать точную копию объекта. Чтобы добавить поддержку клонирования в класс, необходимо реализовать метод __clone.

Признаки применения паттерна: Прототип легко определяется в коде по ключевого слова clone и реализаций метода __clone.

Пример: Структура паттерна

Этот пример показывает структуру паттерна Прототип, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом. После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.

PrototypeStructural.php: Пример структуры паттерна

<?php

namespace RefactoringGuru\Prototype\Structural;

/**
 * Пример класса, имеющего возможность клонирования.
 */
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) {
        print("Primitive field values have been carried over to a clone. Yay!\n");
    } else {
        print("Primitive field values have not been copied. Booo!\n");
    }
    if ($p1->component === $p2->component) {
        print("Simple component has not been cloned. Booo!\n");
    } else {
        print("Simple component has been cloned. Yay!\n");
    }

    if ($p1->circularReference === $p2->circularReference) {
        print("Component with back reference has not been cloned. Booo!\n");
    } else {
        print("Component with back reference has been cloned. Yay!\n");
    }

    if ($p1->circularReference->prototype === $p2->circularReference->prototype) {
        print("Component with back reference is linked to original object. Booo!\n");
    } else {
        print("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!

Пример: Пример из жизни

Паттерн Прототип предоставляет удобный способ репликации существующих объектов вместо их восстановления и копирования всех полей напрямую. Прямое копирование не только связывает вас с классами клонируемых объектов, но и не позволяет копировать содержимое приватных полей. Паттерн Прототип позволяет выполнять клонирование в контексте клонированного класса, где доступ к приватным полям класса не ограничен.

В этом примере показано, как клонировать сложный объект Страницы, используя паттерн Прототип. Класс Страница имеет множество приватных полей, которые будут перенесены в клонированный объект благодаря паттерну Прототип.

PrototypeRealWorld.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($title, $body, $author)
    {
        $this->title = $title;
        $this->body = $body;
        $this->author = $author;
        $this->author->addToPage($this);
        $this->date = new \DateTime();
    }

    public function addComment($comment)
    {
        $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($name)
    {
        $this->name = $name;
    }

    public function addToPage($page)
    {
        $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;
    print("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
        )

)