Тема заезженная до дыр, не спорю… Вероятно, для опытных разработчиков моя статья будет мало, чем полезна. Я бы рекомендовал её к прочтению тем, кто только начал осознавать, что его коду чего-то не хватает, и что он созрел для вникания в это далёкое понятие – «паттерны». По себе помню, что довольно долгое время я путался в шаблонах, иногда даже не понимая, чем один отличается от другого. Именно этот факт стал основой для моей статьи. Примеры в ней не будут реальными. Они будут абстрактными и максимально простыми. Однако я постараюсь все примеры держать в едином контексте, чтобы можно было наглядно видеть отличия их использования в одной и той же ситуации. Я не буду нагружать классы лишним функционалом, чтобы можно было понять, какая именно часть кода имеет непосредственное отношение к шаблону. Главными героями примеров станут Factory (фабрика) и Product (продукт, производимый этой фабрикой). Возьмём это отношение за отправную точку. Возможно, в некоторых примерах это будет не очень уместно, но зато очень наглядно…
Статья будет разбита на несколько частей. В каждой я буду рассказывать о новом типе шаблонов проектирования. Всем, кого эта тема может заинтересовать, прошу под кат.
Порождающие шаблоны проектирования
С вашего позволения, я не буду пересказывать на сотый раз, кто такие, эти порождающие шаблоны… Я просто оставлю здесь ссылку на википедию. Краткость – сестра таланта. Поэтому сразу предлагаю примеры.
Реестр (Registry)
Хотелось бы начать с этого шаблона. Он немного выбивается из общего ряда, потому что не является порождающим, но в дальнейшем нам потребуется его знание. Итак, реестр – это хэш, доступ к данным у которого осуществляется через статические методы:
<?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)
Этот шаблон, по сути, является частным случаем реестра. Пул объектов – это хэш, в который можно складывать инициализированные объекты и доставать их оттуда при необходимости:
<?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)
Наверное, один из самых популярных шаблонов. Как правило, его все запоминают первым. А ещё при поиске работы про него очень любят спрашивать на собеседованиях. Вот самый простой пример:
<?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)
Возможно, кому-то захочется использовать множество различных синглтонов в своём проекте. Тогда, наверное, стоит отделить логику шаблона от конкретной реализации. Давайте попробуем скрестить шаблоны «Одиночка» и «Пул объектов»:
<?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. В примере мы создали два таких класса и проверили, что у каждого из этих классов свой единственный экземпляр.
Я не случайно разбил общую логику на два абстрактных класса. Теперь давайте ещё немного усложним пример. Позволим создавать несколько одиночек для каждого класса, отличающихся уникальным идентификатором.
<?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)
А теперь предлагаю немного понизить градус и снова вернуться к истокам. Допустим, мы знаем, что бывают фабрики, производящие какой-то свой продукт. Нам не важно, как именно фабрика делает этот продукт, но мы знаем, что у любой фабрики есть один универсальный способ попросить его:
<?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)
Бывает ситуация, когда у нас есть несколько однотипных фабрик и мы хотим инкапсулировать логику выбора, какую из фабрик использовать для той или иной задачи. Тут-то нам на помощь и приходит этот шаблон.
<?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)
А вот вам ещё одна интересная ситуация. Представьте, что у вас есть фабрика, но вы не знаете, какая часть её функционала вам потребуется, а какая – нет. В таких случаях необходимые операции выполнятся только если они нужны и только один раз:
<?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)
Некоторые объекты приходится создавать многократно. Есть смысл сэкономить на их инициализации, особенно, если инициализация требует времени и ресурсов. Прототип – это заранее инициализированный и сохранённый объект. В случае необходимости он клонируется:
<?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)
Ну и последний на сегодня шаблон – строитель. Он полезен, когда мы хотим инкапсулировать создание сложного объекта. Мы просто расскажем фабрике, какому строителю доверить создание продукта:
<?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 шаблонов проектирования. Это довольно длинная статья. Поэтому хотелось бы узнать ваше мнение. Есть ли смысл в проделанной работе и стоит ли мне завершить цикл, рассказав о структурных шаблонах и шаблонах поведения?
Весь опубликованный код можно найти и на гитхабе.
Автор: Webtoucher