User Tools

Site Tools


php_patterns

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
php_patterns [2021/07/07 05:44] chifekphp_patterns [2023/09/14 06:06] (current) – external edit 127.0.0.1
Line 171: Line 171:
  
  
-**Одиночка (Singleton)**+===== Одиночка (Singleton) ===== 
  
 Наверное, один из самых популярных шаблонов. Как правило, его все запоминают первым. А ещё при поиске работы про него очень любят спрашивать на собеседованиях. Вот самый простой пример: Наверное, один из самых популярных шаблонов. Как правило, его все запоминают первым. А ещё при поиске работы про него очень любят спрашивать на собеседованиях. Вот самый простой пример:
Line 261: Line 262:
  
  
 +===== Пул одиночек (Multiton) =====
 +
 +
 +Возможно, кому-то захочется использовать множество различных синглтонов в своём проекте. Тогда, наверное, стоит отделить логику шаблона от конкретной реализации. Давайте попробуем скрестить шаблоны «Одиночка» и «Пул объектов»:
 +
 +<code>
 +<?php
 +/**
 + * Общий интерфейс пула одиночек
 + */
 +abstract class FactoryAbstract
 +{
 +
 +    /**
 +     * @var array
 +     */
 +    protected static $instances = array();
 +
 +
 +    /**
 +     * Возвращает экземпляр класса, из которого вызван
 +     *
 +     * @return static
 +     */
 +    public static function getInstance()
 +    {
 +        $className = static::getClassName();
 +        if (!(self::$instances[$className] instanceof $className)) {
 +            self::$instances[$className] = new $className();
 +        }
 +        return self::$instances[$className];
 +    }
 +
 +    /**
 +     * Удаляет экземпляр класса, из которого вызван
 +     *
 +     * @return void
 +     */
 +    public static function removeInstance()
 +    {
 +        $className = static::getClassName();
 +        if (array_key_exists($className, self::$instances)) {
 +            unset(self::$instances[$className]);
 +        }
 +    }
 +
 +    /**
 +     * Возвращает имя экземпляра класса
 +     *
 +     * @return string
 +     */
 +    final protected static function getClassName()
 +    {
 +        return get_called_class();
 +    }
 +
 +    /**
 +     * Конструктор закрыт
 +     */
 +    protected function __construct()
 +    {
 +    }
 +
 +    /**
 +     * Клонирование запрещено
 +     */
 +    final protected function __clone()
 +    {
 +    }
 +
 +    /**
 +     * Сериализация запрещена
 +     */
 +    final protected function __sleep()
 +    {
 +    }
 +
 +    /**
 +     * Десериализация запрещена
 +     */
 +    final protected function __wakeup()
 +    {
 +    }
 +}
 +
 +/**
 + * Интерфейс пула одиночек
 + */
 +abstract class Factory extends FactoryAbstract
 +{
 +
 +    /**
 +     * Возвращает экземпляр класса, из которого вызван
 +     *
 +     * @return static
 +     */
 +    final public static function getInstance()
 +    {
 +        return parent::getInstance();
 +    }
 +
 +    /**
 +     * Удаляет экземпляр класса, из которого вызван
 +     *
 +     * @return void
 +     */
 +    final public static function removeInstance()
 +    {
 +        parent::removeInstance();
 +    }
 +}
 +
 +/*
 + * =====================================
 +           USING OF MULTITON
 + * =====================================
 + */
 +
 +/**
 + * Первый одиночка
 + */
 +class FirstProduct extends Factory
 +{
 +    public $a = [];
 +}
 +
 +/**
 + * Второй одиночка
 + */
 +class SecondProduct extends FirstProduct
 +{
 +}
 +
 +// Заполняем свойства одиночек
 +FirstProduct::getInstance()->a[] = 1;
 +SecondProduct::getInstance()->a[] = 2;
 +FirstProduct::getInstance()->a[] = 3;
 +SecondProduct::getInstance()->a[] = 4;
 +
 +print_r(FirstProduct::getInstance()->a);
 +// array(1, 3)
 +print_r(SecondProduct::getInstance()->a);
 +// array(2, 4)
 +</code>
 +
 +Итак, для добавления нового класса-одиночки нам просто нужно унаследовать его от класса Factory. В примере мы создали два таких класса и проверили, что у каждого из этих классов свой единственный экземпляр.
 +
 +Я не случайно разбил общую логику на два абстрактных класса. Теперь давайте ещё немного усложним пример. Позволим создавать несколько одиночек для каждого класса, отличающихся уникальным идентификатором.
 +
 +<code>
 +<?php
 +/**
 + * Интерфейс сложного пула одиночек
 + */
 +abstract class RegistryFactory extends FactoryAbstract
 +{
 +
 +    /**
 +     * Возвращает экземпляр класса, из которого вызван
 +     *
 +     * @param integer|string $id - уникальный идентификатор одиночки
 +     * @return static
 +     */
 +    final public static function getInstance($id)
 +    {
 +        $className = static::getClassName();
 +        if (isset(self::$instances[$className])) {
 +            if (!(self::$instances[$className][$id] instanceof $className)) {
 +                self::$instances[$className][$id] = new $className($id);
 +            }
 +        } else {
 +            self::$instances[$className] = [
 +                $id => new $className($id),
 +            ];
 +        }
 +        return self::$instances[$className][$id];
 +    }
 +
 +    /**
 +     * Удаляет экземпляр класса, из которого вызван
 +     *
 +     * @param integer|string $id - уникальный идентификатор одиночки. Если не указан, все экземпляры класса будут удалены
 +     * @return void
 +     */
 +    final public static function removeInstance($id = null)
 +    {
 +        $className = static::getClassName();
 +        if (isset(self::$instances[$className])) {
 +            if (is_null($id)) {
 +                unset(self::$instances[$className]);
 +            } else {
 +                if (isset(self::$instances[$className][$id])) {
 +                    unset(self::$instances[$className][$id]);
 +                }
 +                if (empty(self::$instances[$className])) {
 +                    unset(self::$instances[$className]);
 +                }
 +            }
 +        }
 +    }
 +
 +    protected function __construct($id)
 +    {
 +    }
 +}
 +
 +/*
 + * =====================================
 +           USING OF MULTITON
 + * =====================================
 + */
 +
 +/**
 + * Первый пул одиночек
 + */
 +class FirstFactory extends RegistryFactory
 +{
 +    public $a = [];
 +}
 +
 +/**
 + * Второй пул одиночек
 + */
 +class SecondFactory extends FirstFactory
 +{
 +}
 +
 +// Заполняем свойства одиночек
 +FirstFactory::getInstance('FirstProduct')->a[] = 1;
 +FirstFactory::getInstance('SecondProduct')->a[] = 2;
 +SecondFactory::getInstance('FirstProduct')->a[] = 3;
 +SecondFactory::getInstance('SecondProduct')->a[] = 4;
 +FirstFactory::getInstance('FirstProduct')->a[] = 5;
 +FirstFactory::getInstance('SecondProduct')->a[] = 6;
 +SecondFactory::getInstance('FirstProduct')->a[] = 7;
 +SecondFactory::getInstance('SecondProduct')->a[] = 8;
 +
 +print_r(FirstFactory::getInstance('FirstProduct')->a);
 +// array(1, 5)
 +print_r(FirstFactory::getInstance('SecondProduct')->a);
 +// array(2, 6)
 +print_r(SecondFactory::getInstance('FirstProduct')->a);
 +// array(3, 7)
 +print_r(SecondFactory::getInstance('SecondProduct')->a);
 +// array(4, 8)
 +</code>
 +
 +Примерно по такому принципу работают некоторые ORM, позволяя хранить уже загруженные и инициализированные модели.
 +
 +А теперь, пока ещё не слишком поздно, верну мечтателей с небес на землю. Шаблон Одиночка и его продвинутые братья, несомненно, могут быть полезны, но не надо забываться и лепить его где нужно и где не нужно. Напомню (или поведаю), что есть такой антипаттерн, «Одиночество» (Singletonitis), который как раз заключается в неуместном использовании синглтонов. Так для чего же нам этот шаблон? Самый распространённый пример – соединение с базой данных, которое создаётся один раз и используется на протяжении работы скрипта. А ещё во многих фреймворках реестр делают одиночкой и используют его, как объект, а не как класс со статическими методами.
 +
 +===== Фабричный метод (Factory method) =====
 +
 +
 +А теперь предлагаю немного понизить градус и снова вернуться к истокам. Допустим, мы знаем, что бывают фабрики, производящие какой-то свой продукт. Нам не важно, как именно фабрика делает этот продукт, но мы знаем, что у любой фабрики есть один универсальный способ попросить его:
 +
 +<code>
 +<?php
 +/**
 + * Фабрика
 + */
 +interface Factory
 +{
 +
 +    /**
 +     * Возвращает продукт
 +     *
 +     * @return Product
 +     */
 +    public function getProduct();
 +}
 +
 +/**
 + * Продукт
 + */
 +interface Product
 +{
 +
 +    /**
 +     * Возвращает название продукта
 +     *
 +     * @return string
 +     */
 +    public function getName();
 +}
 +
 +/**
 + * Первая фабрика
 + */
 +class FirstFactory implements Factory
 +{
 +
 +    /**
 +     * Возвращает продукт
 +     *
 +     * @return Product
 +     */
 +    public function getProduct()
 +    {
 +        return new FirstProduct();
 +    }
 +}
 +
 +/**
 + * Вторая фабрика
 + */
 +class SecondFactory implements Factory
 +{
 +
 +    /**
 +     * Возвращает продукт
 +     *
 +     * @return Product
 +     */
 +    public function getProduct()
 +    {
 +        return new SecondProduct();
 +    }
 +}
 +
 +/**
 + * Первый продукт
 + */
 +class FirstProduct implements Product
 +{
 +
 +    /**
 +     * Возвращает название продукта
 +     *
 +     * @return string
 +     */
 +    public function getName()
 +    {
 +        return 'The first product';
 +    }
 +}
 +
 +/**
 + * Второй продукт
 + */
 +class SecondProduct implements Product
 +{
 +
 +    /**
 +     * Возвращает название продукта
 +     *
 +     * @return string
 +     */
 +    public function getName()
 +    {
 +        return 'Second product';
 +    }
 +}
 +
 +/*
 + * =====================================
 +        USING OF FACTORY METHOD
 + * =====================================
 + */
 +
 +$factory = new FirstFactory();
 +$firstProduct = $factory->getProduct();
 +$factory = new SecondFactory();
 +$secondProduct = $factory->getProduct();
 +
 +print_r($firstProduct->getName());
 +// The first product
 +print_r($secondProduct->getName());
 +// Second product
 +</code>
 +
 +В данном примере метод getProduct() является фабричным.
 +
 +===== Абстрактная фабрика (Abstract Factory) =====
 +
 +Бывает ситуация, когда у нас есть несколько однотипных фабрик и мы хотим инкапсулировать логику выбора, какую из фабрик использовать для той или иной задачи. Тут-то нам на помощь и приходит этот шаблон.
 +
 +<code>
 +<?php
 +/**
 + * Какой-нибудь файл конфигурации
 + */
 +class Config
 +{
 +    public static $factory = 1;
 +}
 +
 +/**
 + * Какой-то продукт
 + */
 +interface Product
 +{
 +
 +    /**
 +     * Возвращает название продукта
 +     *
 +     * @return string
 +     */
 +    public function getName();
 +}
 +
 +/**
 + * Абстрактная фабрика
 + */
 +abstract class AbstractFactory
 +{
 +
 +    /**
 +     * Возвращает фабрику
 +     *
 +     * @return AbstractFactory - дочерний объект
 +     * @throws Exception
 +     */
 +    public static function getFactory()
 +    {
 +        switch (Config::$factory) {
 +            case 1:
 +                return new FirstFactory();
 +            case 2:
 +                return new SecondFactory();
 +        }
 +        throw new Exception('Bad config');
 +    }
 +
 +    /**
 +     * Возвращает продукт
 +     *
 +     * @return Product
 +     */
 +    abstract public function getProduct();
 +}
 +
 +/*
 + * =====================================
 +             FIRST FAMILY
 + * =====================================
 + */
 +
 +class FirstFactory extends AbstractFactory
 +{
 +
 +    /**
 +     * Возвращает продукт
 +     *
 +     * @return Product
 +     */
 +    public function getProduct()
 +    {
 +        return new FirstProduct();
 +    }
 +}
 +
 +/**
 + * Продукт первой фабрики
 + */
 +class FirstProduct implements Product
 +{
 +
 +    /**
 +     * Возвращает название продукта
 +     *
 +     * @return string
 +     */
 +    public function getName()
 +    {
 +        return 'The product from the first factory';
 +    }
 +}
 +
 +/*
 + * =====================================
 +             SECOND FAMILY
 + * =====================================
 + */
 +
 +class SecondFactory extends AbstractFactory
 +{
 +
 +    /**
 +     * Возвращает продукт
 +     *
 +     * @return Product
 +     */
 +    public function getProduct()
 +    {
 +        return new SecondProduct();
 +    }
 +}
 +
 +/**
 + * Продукт второй фабрики
 + */
 +class SecondProduct implements Product
 +{
 +
 +    /**
 +     * Возвращает название продукта
 +     *
 +     * @return string
 +     */
 +    public function getName()
 +    {
 +        return 'The product from second factory';
 +    }
 +}
 +
 +/*
 + * =====================================
 +       USING OF ABSTRACT FACTORY
 + * =====================================
 + */
 +
 +$firstProduct = AbstractFactory::getFactory()->getProduct();
 +Config::$factory = 2;
 +$secondProduct = AbstractFactory::getFactory()->getProduct();
 +
 +print_r($firstProduct->getName());
 +// The first product from the first factory
 +print_r($secondProduct->getName());
 +// Second product from second factory
 +
 +</code>
 +
 +Как видно из примера, нам не приходится заботится о том, какую фабрику взять. Абстрактная фабрика сама проверяет настройки конфигурации и возвращает подходящую фабрику. Разумеется, вовсе не обязательно абстрактная фабрика должна руководствоваться файлу конфигурации. Логика выбора может быть любой.
 +
 +===== Отложенная инициализация (Lazy Initialization) =====
 +
 +А вот вам ещё одна интересная ситуация. Представьте, что у вас есть фабрика, но вы не знаете, какая часть её функционала вам потребуется, а какая – нет. В таких случаях необходимые операции выполнятся только если они нужны и только один раз:
 +
 +<code>
 +<?php
 +/**
 + * Какой-то продукт
 + */
 +interface Product
 +{
 +
 +    /**
 +     * Возвращает название продукта
 +     *
 +     * @return string
 +     */
 +    public function getName();
 +}
 +
 +class Factory
 +{
 +
 +    /**
 +     * @var Product
 +     */
 +    protected $firstProduct;
 +
 +    /**
 +     * @var Product
 +     */
 +    protected $secondProduct;
 +
 +
 +    /**
 +     * Возвращает продукт
 +     *
 +     * @return Product
 +     */
 +    public function getFirstProduct()
 +    {
 +
 +        if (!$this->firstProduct) {
 +            $this->firstProduct = new FirstProduct();
 +        }
 +        return $this->firstProduct;
 +    }
 +
 +    /**
 +     * Возвращает продукт
 +     *
 +     * @return Product
 +     */
 +    public function getSecondProduct()
 +    {
 +
 +        if (!$this->secondProduct) {
 +            $this->secondProduct = new SecondProduct();
 +        }
 +        return $this->secondProduct;
 +    }
 +}
 +
 +/**
 + * Первый продукт
 + */
 +class FirstProduct implements Product
 +{
 +
 +    /**
 +     * Возвращает название продукта
 +     *
 +     * @return string
 +     */
 +    public function getName()
 +    {
 +        return 'The first product';
 +    }
 +}
 +
 +/**
 + * Второй продукт
 + */
 +class SecondProduct implements Product
 +{
 +
 +    /**
 +     * Возвращает название продукта
 +     *
 +     * @return string
 +     */
 +    public function getName()
 +    {
 +        return 'Second product';
 +    }
 +}
 +
 +/*
 + * =====================================
 +      USING OF LAZY INITIALIZATION
 + * =====================================
 + */
 +
 +$factory = new Factory();
 +
 +print_r($factory->getFirstProduct()->getName());
 +// The first product
 +print_r($factory->getSecondProduct()->getName());
 +// Second product
 +print_r($factory->getFirstProduct()->getName());
 +// The first product
 +</code>
 +
 +При первом вызове метода, фабрика создаёт объект и сохраняет его в себя. При повторном вызове – возвращает уже готовый объект. Если бы мы не вызвали метод, объект бы не создался вовсе. Признаю, в данном примере мало смысла. Здесь использование этого шаблона не оправдано. Я просто хотел показать его смысл. А теперь представьте, что создание объекта требует сложных вычислений, многократных обращений к базе данных, да и ресурсов кушает массу. Весьма хороший повод обратить внимание на этот шаблон.
 +
 +===== Прототип (Prototype) =====
 +
 +
 +Некоторые объекты приходится создавать многократно. Есть смысл сэкономить на их инициализации, особенно, если инициализация требует времени и ресурсов. Прототип – это заранее инициализированный и сохранённый объект. В случае необходимости он клонируется:
 +
 +<code>
 +<?php
 +/**
 + * Какой-то продукт
 + */
 +interface Product
 +{
 +}
 +
 +/**
 + * Какая-то фабрика
 + */
 +class Factory
 +{
 +
 +    /**
 +     * @var Product
 +     */
 +    private $product;
 +
 +
 +    /**
 +     * @param Product $product
 +     */
 +    public function __construct(Product $product)
 +    {
 +        $this->product = $product;
 +    }
 +
 +    /**
 +     * Возвращает новый продукт путём клонирования
 +     *
 +     * @return Product
 +     */
 +    public function getProduct()
 +    {
 +        return clone $this->product;
 +    }
 +}
 +
 +/**
 + * Продукт
 + */
 +class SomeProduct implements Product
 +{
 +    public $name;
 +}
 +
 +/*
 + * =====================================
 +          USING OF PROTOTYPE
 + * =====================================
 + */
 +
 +$prototypeFactory = new Factory(new SomeProduct());
 +
 +$firstProduct = $prototypeFactory->getProduct();
 +$firstProduct->name = 'The first product';
 +
 +$secondProduct = $prototypeFactory->getProduct();
 +$secondProduct->name = 'Second product';
 +
 +print_r($firstProduct->name);
 +// The first product
 +print_r($secondProduct->name);
 +// Second product
 +</code>
 +
 +Как видно из примера мы создали два никак не связанных объекта.
 +
 +Строитель (Builder)
 +
 +Ну и последний на сегодня шаблон – строитель. Он полезен, когда мы хотим инкапсулировать создание сложного объекта. Мы просто расскажем фабрике, какому строителю доверить создание продукта:
 +
 +<code>
 +<?php
 +/**
 + * Какой-то продукт
 + */
 +class Product
 +{
 +
 +    /**
 +     * @var string
 +     */
 +    private $name;
 +
 +
 +    /**
 +     * @param string $name
 +     */
 +    public function setName($name) {
 +        $this->name = $name;
 +    }
 +
 +    /**
 +     * @return string
 +     */
 +    public function getName() {
 +        return $this->name;
 +    }
 +}
 +
 +/**
 + * Какая-то фабрика
 + */
 +class Factory
 +{
 +
 +    /**
 +     * @var Builder
 +     */
 +    private $builder;
 +
 +
 +    /**
 +     * @param Builder $builder
 +     */
 +    public function __construct(Builder $builder)
 +    {
 +        $this->builder = $builder;
 +        $this->builder->buildProduct();
 +    }
 +
 +    /**
 +     * Возвращает созданный продукт
 +     *
 +     * @return Product
 +     */
 +    public function getProduct()
 +    {
 +        return $this->builder->getProduct();
 +    }
 +}
 +
 +/**
 + * Какой-то строитель
 + */
 +abstract class Builder
 +{
 +
 +    /**
 +     * @var Product
 +     */
 +    protected $product;
 +
 +
 +    /**
 +     * Возвращает созданный продукт
 +     *
 +     * @return Product
 +     */
 +    final public function getProduct()
 +    {
 +        return $this->product;
 +    }
 +
 +    /**
 +     * Создаёт продукт
 +     *
 +     * @return void
 +     */
 +    public function buildProduct()
 +    {
 +        $this->product = new Product();
 +    }
 +}
 +
 +/**
 + * Первый строитель
 + */
 +class FirstBuilder extends Builder
 +{
 +
 +    /**
 +     * Создаёт продукт
 +     *
 +     * @return void
 +     */
 +    public function buildProduct()
 +    {
 +        parent::buildProduct();
 +        $this->product->setName('The product of the first builder');
 +    }
 +}
 +
 +/**
 + * Второй строитель
 + */
 +class SecondBuilder extends Builder
 +{
 +
 +    /**
 +     * Создаёт продукт
 +     *
 +     * @return void
 +     */
 +    public function buildProduct()
 +    {
 +        parent::buildProduct();
 +        $this->product->setName('The product of second builder');
 +    }
 +}
 +
 +/*
 + * =====================================
 +            USING OF BUILDER
 + * =====================================
 + */
 +
 +$firstDirector = new Factory(new FirstBuilder());
 +$secondDirector = new Factory(new SecondBuilder());
 +
 +print_r($firstDirector->getProduct()->getName());
 +// The product of the first builder
 +print_r($secondDirector->getProduct()->getName());
 +// The product of second builder
 +</code>
 +
 +Итак, мы рассмотрели 9 шаблонов проектирования. Это довольно длинная статья. Поэтому хотелось бы узнать ваше мнение. Есть ли смысл в проделанной работе и стоит ли мне завершить цикл, рассказав о структурных шаблонах и шаблонах поведения?
 +
 +Весь опубликованный код можно найти и на [[https://github.com/Chifek/php-design-patterns/tree/master/Creational|гитхабе]]
php_patterns.1625636666.txt.gz · Last modified: 2023/09/14 06:06 (external edit)