Шаблоны проектирования PHP. Часть 1. Порождающие

от автора

Тема заезженная до дыр, не спорю… Вероятно, для опытных разработчиков моя статья будет мало, чем полезна. Я бы рекомендовал её к прочтению тем, кто только начал осознавать, что его коду чего-то не хватает, и что он созрел для вникания в это далёкое понятие – «паттерны». По себе помню, что довольно долгое время я путался в шаблонах, иногда даже не понимая, чем один отличается от другого. Именно этот факт стал основой для моей статьи. Примеры в ней не будут реальными. Они будут абстрактными и максимально простыми. Однако я постараюсь все примеры держать в едином контексте, чтобы можно было наглядно видеть отличия их использования в одной и той же ситуации. Я не буду нагружать классы лишним функционалом, чтобы можно было понять, какая именно часть кода имеет непосредственное отношение к шаблону. Главными героями примеров станут Factory (фабрика) и Product (продукт, производимый этой фабрикой). Возьмём это отношение за отправную точку. Возможно, в некоторых примерах это будет не очень уместно, но зато очень наглядно…

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

Порождающие шаблоны проектирования

С вашего позволения, я не буду пересказывать на сотый раз, кто такие, эти порождающие шаблоны… Я просто оставлю здесь ссылку на википедию. Краткость – сестра таланта. Поэтому сразу предлагаю примеры.

Реестр (Registry)

Хотелось бы начать с этого шаблона. Он немного выбивается из общего ряда, потому что не является порождающим, но в дальнейшем нам потребуется его знание. Итак, реестр – это хэш, доступ к данным у которого осуществляется через статические методы:

Пример 1

<?php /**  * Реестр  */ class Product {      /**      * @var mixed[]      */     protected static $data = array();       /**      * Добавляет значение в реестр      *      * @param string $key      * @param mixed $value      * @return void      */     public static function set($key, $value)     {         self::$data[$key] = $value;     }      /**      * Возвращает значение из реестра по ключу      *      * @param string $key      * @return mixed      */     public static function get($key)     {         return isset(self::$data[$key]) ? self::$data[$key] : null;     }      /**      * Удаляет значение из реестра по ключу      *      * @param string $key      * @return void      */     final public static function removeProduct($key)     {         if (isset(self::$data[$key])) {             unset(self::$data[$key]);         }     } }  /*  * =====================================  *           USING OF REGISTRY  * =====================================  */  Product::set('name', 'First product');  print_r(Product::get('name')); // First product 

Нередко можно встретить реестры, реализующие интерфейсы ArrayAccess и/или Iterator, но на мой взгляд, это лишнее. Основное применение реестра – в качестве безопасной замены глобальным переменным.

Пул объектов (Object pool)

Этот шаблон, по сути, является частным случаем реестра. Пул объектов – это хэш, в который можно складывать инициализированные объекты и доставать их оттуда при необходимости:

Пример 2

<?php /**  * Пул объектов  */ class Factory {      /**      * @var Product[]      */     protected static $products = array();       /**      * Добавляет продукт в пул      *      * @param Product $product      * @return void      */     public static function pushProduct(Product $product)     {         self::$products[$product->getId()] = $product;     }      /**      * Возвращает продукт из пула      *      * @param integer|string $id - идентификатор продукта      * @return Product $product      */     public static function getProduct($id)     {         return isset(self::$products[$id]) ? self::$products[$id] : null;     }      /**      * Удаляет продукт из пула      *      * @param integer|string $id - идентификатор продукта      * @return void      */     public static function removeProduct($id)     {         if (isset(self::$products[$id])) {             unset(self::$products[$id]);         }     } }  class Product {      /**      * @var integer|string      */     protected $id;       public function __construct($id) {         $this->id = $id;     }      /**      * @return integer|string      */     public function getId()     {         return $this->id;     } }  /*  * =====================================  *         USING OF OBJECT POOL  * =====================================  */  Factory::pushProduct(new Product('first')); Factory::pushProduct(new Product('second'));  print_r(Factory::getProduct('first')->getId()); // first print_r(Factory::getProduct('second')->getId()); // second 

Одиночка (Singleton)

Наверное, один из самых популярных шаблонов. Как правило, его все запоминают первым. А ещё при поиске работы про него очень любят спрашивать на собеседованиях. Вот самый простой пример:

Пример 3

<?php /**  * Одиночка  */ final class Product {      /**      * @var self      */     private static $instance;      /**      * @var mixed      */     public $a;       /**      * Возвращает экземпляр себя      *      * @return self      */     public static function getInstance()     {         if (!isset(self::$instance)) {             self::$instance = new self();         }         return self::$instance;     }      /**      * Конструктор закрыт      */     private function __construct()     {     }      /**      * Клонирование запрещено      */     private function __clone()     {     }      /**      * Сериализация запрещена      */     private function __sleep()     {     }      /**      * Десериализация запрещена      */     private function __wakeup()     {     } }  /*  * =====================================  *           USING OF SINGLETON  * =====================================  */  $firstProduct = Product::getInstance(); $secondProduct = Product::getInstance();  $firstProduct->a = 1; $secondProduct->a = 2;  print_r($firstProduct->a); // 2 print_r($secondProduct->a); // 2 

Принцип синглтона прост, как пять копеек. Для того, чтобы обеспечить существование только одного экземпляра класса Product, мы закрыли все магические методы для создания экземпляра класса, клонирования и сериализации. Единственный возможный способ получить объект – воспользоваться статическим методом Product::getInstance(). При первом обращении класс сам создаст экземпляр себя и положит его в статическое свойство Product::$instance. При последующих обращениях, в рамках выполнения скрипта, метод будет нам возвращать тот же, ранее созданный, экземпляр класса.

Я добавил в класс открытое свойство $a, чтобы продемонстрировать работу одиночки. В данном примере можно увидеть, что и $firstProduct, и $secondProduct – есть ни что иное, как ссылка на один и тот же объект.

Пул одиночек (Multiton)

Возможно, кому-то захочется использовать множество различных синглтонов в своём проекте. Тогда, наверное, стоит отделить логику шаблона от конкретной реализации. Давайте попробуем скрестить шаблоны «Одиночка» и «Пул объектов»:

Пример 4.1

<?php /**  * Общий интерфейс пула одиночек  */ abstract class FactoryAbstract {      /**      * @var array      */     protected static $instances = array();       /**      * Возвращает экземпляр класса, из которого вызван      *      * @return static      */     public static function getInstance()     {         $className = static::getClassName();         if (!isset(self::$instances[$className])) {             self::$instances[$className] = new $className();         }         return self::$instances[$className];     }      /**      * Удаляет экземпляр класса, из которого вызван      *      * @return void      */     public static function removeInstance()     {         $className = static::getClassName();         if (isset(self::$instances[$className])) {             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) 

Итак, для добавления нового класса-одиночки нам просто нужно унаследовать его от класса Factory. В примере мы создали два таких класса и проверили, что у каждого из этих классов свой единственный экземпляр.

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

Пример 4.2

<?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 (!isset(self::$instances[$className][$id])) {                 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) 

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

А теперь, пока ещё не слишком поздно, верну мечтателей с небес на землю. Шаблон Одиночка и его продвинутые братья, несомненно, могут быть полезны, но не надо забываться и лепить его где нужно и где не нужно. Напомню (или поведаю), что есть такой антипаттерн, «Одиночество» (Singletonitis), который как раз заключается в неуместном использовании синглтонов. Так для чего же нам этот шаблон? Самый распространённый пример – соединение с базой данных, которое создаётся один раз и используется на протяжении работы скрипта. А ещё во многих фреймворках реестр делают одиночкой и используют его, как объект, а не как класс со статическими методами.

Фабричный метод (Factory method)

А теперь предлагаю немного понизить градус и снова вернуться к истокам. Допустим, мы знаем, что бывают фабрики, производящие какой-то свой продукт. Нам не важно, как именно фабрика делает этот продукт, но мы знаем, что у любой фабрики есть один универсальный способ попросить его:

Пример 5

<?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 FirstProduct();     } }  /**  * Первый продукт  */ 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 

В данном примере метод getProduct() является фабричным.

Абстрактная фабрика (Abstract Factory)

Бывает ситуация, когда у нас есть несколько однотипных фабрик и мы хотим инкапсулировать логику выбора, какую из фабрик использовать для той или иной задачи. Тут-то нам на помощь и приходит этот шаблон.

Пример 6

<?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 

Как видно из примера, нам не приходится заботится о том, какую фабрику взять. Абстрактная фабрика сама проверяет настройки конфигурации и возвращает подходящую фабрику. Разумеется, вовсе не обязательно абстрактная фабрика должна руководствоваться файлу конфигурации. Логика выбора может быть любой.

Отложенная инициализация (Lazy Initialization)

А вот вам ещё одна интересная ситуация. Представьте, что у вас есть фабрика, но вы не знаете, какая часть её функционала вам потребуется, а какая – нет. В таких случаях необходимые операции выполнятся только если они нужны и только один раз:

Пример 7

<?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 

При первом вызове метода, фабрика создаёт объект и сохраняет его в себя. При повторном вызове – возвращает уже готовый объект. Если бы мы не вызвали метод, объект бы не создался вовсе. Признаю, в данном примере мало смысла. Здесь использование этого шаблона не оправдано. Я просто хотел показать его смысл. А теперь представьте, что создание объекта требует сложных вычислений, многократных обращений к базе данных, да и ресурсов кушает массу. Весьма хороший повод обратить внимание на этот шаблон.

Прототип (Prototype)

Некоторые объекты приходится создавать многократно. Есть смысл сэкономить на их инициализации, особенно, если инициализация требует времени и ресурсов. Прототип – это заранее инициализированный и сохранённый объект. В случае необходимости он клонируется:

Пример 8

<?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 

Как видно из примера мы создали два никак не связанных объекта.

Строитель (Builder)

Ну и последний на сегодня шаблон – строитель. Он полезен, когда мы хотим инкапсулировать создание сложного объекта. Мы просто расскажем фабрике, какому строителю доверить создание продукта:

Пример 9

<?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 

Итак, мы рассмотрели 9 шаблонов проектирования. Это довольно длинная статья. Поэтому хотелось бы узнать ваше мнение. Есть ли смысл в проделанной работе и стоит ли мне завершить цикл, рассказав о структурных шаблонах и шаблонах поведения?

Весь опубликованный код можно найти и на гитхабе.

ссылка на оригинал статьи http://habrahabr.ru/post/214285/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *