Шаблоны проектирования в PHP

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

Так что же такое шаблоны проектирования? Шаблоны проектирования – это не аналитические шаблоны, не описание типичных структур данных вроде связных списков. Это также не правила, по которым строятся конкретные приложения или интерфейсы.

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

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

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

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

Конечно же, необходимо проявлять осмотрительность в выборе шаблона проектирования для решения каждой конкретной проблемы. В дополнение к теории мы представим достаточно абстрактные, но вместе с тем простые примеры применения шаблонов проектирования на практике.

«Каждый шаблон описывает одну из тех проблем, которые возникают раз за разом… И затем описывает основу решения этой проблемы таким образом, что вы можете воплотить это решение в миллионе программ, ни разу не повторившись.» ; Кристофер Александер


На сегодняшний день существует 23 шаблона проектирования, которые разделены на 3 группы в соответствии с их назначением:

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

Ниже приведён полный список шаблонов проектирования:

НазначениеНазвание шаблонаВарьируемые аспекты
ПорождающиеАбстрактная фабрика (Abstract Factory)Имена производимых объектов
Строитель (Builder)Соотношение частей сложных объектов
Фабричный метод (Factory Method)Подкласс создаваемых объектов
Прототип (Prototype)Класс создаваемых объектов
Одиночка (Singleton)Класс имеет единственную инстанцию
СтруктурныеАдаптер (Adapter)Интерфейс к объекту
Мост (Bridge)Реализация объекта
Компоновщик (Composite)Структура и соотношение частей объекта
Декоратор (Decorator) или обёрткаОтветственность объекта того же класса
Фасад (Facade)Интерфейс к подсистеме объекта
Приспособленец (Flyweight)Подсистема хранения объекта
Заместитель (Proxy)Место и способ обращения к объекту
ПоведенческиеЦепочка ответственности (Chain of Responsibility)Выбор объекта, способного выполнить данный запрос
Команда (Command)Время и способ выполнения запроса
Итератор (Iterator)Порядок доступа к вложенным элементам
Посредник (Mediator)Способ и порядок взаимодействия объектов друг с другом
Хранитель (Memento)Какая частная информация класса хранится вне его, и когда
Наблюдатель (Observer)Количество зависимых объектов и способ обновления их состояния
Состояние (State)Состояние объектов
Стратегия (Strategy)Выбор алгоритма
Шаблонный метод (Template Method)Последовательность действий
Посетитель (Visitor)Операции, осуществляемые с объектом, без изменения его класса

А теперь расскажем подробнее о самых востребованных из перечисленных выше шаблонов (а также о некоторых не перечисленных).

Одиночка

Это ; один из самых популярных шаблонов. Часто во время проектирования сайта возникает потребность в обращении к единственной инстанции определённого класса на протяжении всего времени выполнения. Такое поведение класса и называется «Одиночка». Например:

<?php
/**
 * Singleton class
 */
final class Product
{

    /**
     * @var self
     */
    private static $instance;

    /**
     * @var mixed
     */
    public $mix;

    /**
     * Return self instance
     *
     * @return self
     */
    public static function getInstance() {
        if (!(self::$instance instanceof self)) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
    }

    private function __clone() {
    }
}

$firstProduct = Product::getInstance();
$secondProduct = Product::getInstance();

$firstProduct->mix = 'test';
$secondProduct->mix = 'example';

print_r($firstProduct->mix);
// example
print_r($secondProduct->mix);
// example

Пул одиночек

В определённом случае может понадобиться обеспечить доступ к нескольким одиночкам в одном проекте:

<?php

abstract class FactoryAbstract {

    protected static $instances = array();

    public static function getInstance() {
        $className = static::getClassName();
        if (!(self::$instances[$className] instanceof $className)) {
            self::$instances[$className] = new $className();
        }
        return self::$instances[$className];
    }

    public static function removeInstance() {
        $className = static::getClassName();
        if (array_key_exists($className, self::$instances)) {
            unset(self::$instances[$className]);
        }
    }

    final protected static function getClassName() {
        return get_called_class();
    }

    protected function __construct() { }

    final protected function __clone() { }
}

abstract class Factory extends FactoryAbstract {

    final public static function getInstance() {
        return parent::getInstance();
    }

    final public static function removeInstance() {
        parent::removeInstance();
    }
}
// using:

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)

Стратегия

Шаблон проектирования «Стратегия» имеет дело с алгоритмами. Реализуя его, вы инкапсулируете определённую группу алгоритмов так, чтобы порождённый класс мог воспользоваться ими, не зная ничего об их конкретной реализации. Например:

<?php

interface OutputInterface {
    public function load();
}

class SerializedArrayOutput implements OutputInterface {
    public function load() {
        return serialize($arrayOfData);
    }
}

class JsonStringOutput implements OutputInterface {
    public function load() {
        return json_encode($arrayOfData);
    }
}

class ArrayOutput implements OutputInterface {
    public function load() {
        return $arrayOfData;
    }
}

Декоратор

Этот шаблон подразумевает внедрение нового поведения (или уточнение уже имеющегося) в объект во время выполнения программы. Например:

<?php
class HtmlTemplate {
    // any parent class methods
}

class Template1 extends HtmlTemplate {
    protected $_html;

    public function __construct() {
        $this->_html = "<p>__text__</p>";
    }

    public function set($html) {
        $this->_html = $html;
    }

    public function render() {
        echo $this->_html;
    }
}

class Template2 extends HtmlTemplate {
    protected $_element;

    public function __construct($s) {
        $this->_element = $s;
        $this->set("<h2>" . $this->_html . "</h2>");
    }

    public function __call($name, $args) {
        $this->_element->$name($args[0]);
    }
}

class Template3 extends HtmlTemplate {
    protected $_element;

    public function __construct($s) {
        $this->_element = $s;
        $this->set("<u>" . $this->_html . "</u>");
    }

    public function __call($name, $args) {
        $this->_element->$name($args[0]);
    }

}

Реестр

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

<?php
/**
* Registry class
*/
class Package {

    protected static $data = array();

    public static function set($key, $value) {
        self::$data[$key] = $value;
    }

    public static function get($key) {
        return isset(self::$data[$key]) ? self::$data[$key] : null;
    }

    final public static function removeObject($key) {
        if (array_key_exists($key, self::$data)) {
            unset(self::$data[$key]);
        }
    }
}

Package::set('name', 'Package name');

print_r(Package::get('name'));
// Package name

Фабрика

Это ещё один очень распространённый шаблон. Он делает именно то, что можно предположить, исходя из его названия: производит инстанции объектов.

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

<?php

interface Factory {
    public function getProduct();
}

interface Product {
    public function getName();
}

class FirstFactory implements Factory {

    public function getProduct() {
        return new FirstProduct();
    }
}

class SecondFactory implements Factory {

    public function getProduct() {
        return new SecondProduct();
    }
}

class FirstProduct implements Product {

    public function getName() {
        return 'The first product';
    }
}

class SecondProduct implements Product {

    public function getName() {
        return 'Second product';
    }
}

$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

Абстрактная фабрика

Бывают ситуации, когда у нас есть несколько схожих фабрик, и нам нужно скрыть логику выбора фабрики для каждого конкретного запроса. В этом нам поможет шаблон «Абстрактная фабрика»:

<?php

class Config {
    public static $factory = 1;
}

interface Product {
    public function getName();
}

abstract class AbstractFactory {

    public static function getFactory() {
        switch (Config::$factory) {
            case 1:
                return new FirstFactory();
            case 2:
                return new SecondFactory();
        }
        throw new Exception('Bad config');
    }

    abstract public function getProduct();
}

class FirstFactory extends AbstractFactory {
    public function getProduct() {
        return new FirstProduct();
    }
}
class FirstProduct implements Product {
    public function getName() {
        return 'The product from the first factory';
    }
}

class SecondFactory extends AbstractFactory {
    public function getProduct() {
        return new SecondProduct();
    }
}
class SecondProduct implements Product {
    public function getName() {
        return 'The product from second 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

Наблюдатель

«Наблюдатель» – это шаблон, предусматривающий для наблюдаемого объекта возможность зарегистрировать наблюдателя, когда тот вызовет специальный метод.

Далее, если состояние наблюдаемого объекта меняется, он посылает сообщение об этом своим наблюдателям:

<?php

interface Observer {
  function onChanged($sender, $args);
}

interface Observable {
  function addObserver($observer);
}

class CustomerList implements Observable {
  private $_observers = array();

  public function addCustomer($name) {
    foreach($this->_observers as $obs)
      $obs->onChanged($this, $name);
  }

  public function addObserver($observer) {
    $this->_observers []= $observer;
  }
}

class CustomerListLogger implements Observer {
  public function onChanged($sender, $args) {
    echo( "'$args' Customer has been added to the list n" );
  }
}

$ul = new UserList();
$ul->addObserver( new CustomerListLogger() );
$ul->addCustomer( "Jack" );

Адаптер

«Адаптер» позволяет надстроить над классом интерфейс, чтобы использовать его в системе, использующей иные соглашения вызова:

<?php

class SimpleBook {

    private $author;
    private $title;

    function __construct($author_in, $title_in) {
        $this->author = $author_in;
        $this->title  = $title_in;
    }

    function getAuthor() {
        return $this->author;
    }

    function getTitle() {
        return $this->title;
    }
}

class BookAdapter {

    private $book;

    function __construct(SimpleBook $book_in) {
        $this->book = $book_in;
    }
    function getAuthorAndTitle() {
        return $this->book->getTitle().' by '.$this->book->getAuthor();
    }
}

// Usage
$book = new SimpleBook("Gamma, Helm, Johnson, and Vlissides", "Design Patterns");
$bookAdapter = new BookAdapter($book);
echo 'Author and Title: '.$bookAdapter->getAuthorAndTitle();

function echo $line_in) {
  echo $line_in."<br/>";
}

Поздняя (ленивая) инициализация

Представьте себе такую ситуацию: при создании инстанции объекта вы ещё не знаете, какие из (довольно ресурсоёмких) функций понадобятся объекту в будущем, а какие – нет.

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

<?php

interface Product {
    public function getName();
}

class Factory {

    protected $firstProduct;
    protected $secondProduct;

    public function getFirstProduct() {
        if (!$this->firstProduct) {
            $this->firstProduct = new FirstProduct();
        }
        return $this->firstProduct;
    }

    public function getSecondProduct() {
        if (!$this->secondProduct) {
            $this->secondProduct = new SecondProduct();
        }
        return $this->secondProduct;
    }
}

class FirstProduct implements Product {
    public function getName() {
        return 'The first product';
    }
}

class SecondProduct implements Product {
    public function getName() {
        return 'Second product';
    }
}

$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

Цепочка ответственности

Этот шаблон ещё иногда называют по-армейски: «Цепь командования». В его основе – серия обработчиков событий, передающих сообщения по цепочке.

Когда сообщение (команда, запрос) пробегает по цепочке, каждый обработчик самостоятельно определяет, должен ли он реагировать на сообщение, и если да, то как. Процесс останавливается, если обработчик знает, как обработать событие:

<?php

interface Command {
    function onCommand($name, $args);
}

class CommandChain {
    private $_commands = array();

    public function addCommand($cmd) {
        $this->_commands[]= $cmd;
    }

    public function runCommand($name, $args) {
        foreach($this->_commands as $cmd) {
            if ($cmd->onCommand($name, $args))
                return;
        }
    }
}

class CustCommand implements Command {
    public function onCommand($name, $args) {
        if ($name != 'addCustomer')
            return false;
        echo("This is CustomerCommand handling 'addCustomer'n");
        return true;
    }
}

class MailCommand implements Command {
    public function onCommand($name, $args) {
        if ($name != 'mail')
            return false;
        echo("This is MailCommand handling 'mail'n");
        return true;
    }
}

$cc = new CommandChain();
$cc->addCommand( new CustCommand());
$cc->addCommand( new MailCommand());
$cc->runCommand('addCustomer', null);
$cc->runCommand('mail', null);

Пул объектов

«Пул объектов» – это ещё одна хэш-таблица. Объекты помещаются в неё сразу после инициализации и впоследствии извлекаются по мере необходимости:

<?php

class Product {

    protected $id;

    public function __construct($id) {
        $this->id = $id;
    }

    public function getId() {
        return $this->id;
    }
}

class Factory {

    protected static $products = array();

    public static function pushProduct(Product $product) {
        self::$products[$product->getId()] = $product;
    }

    public static function getProduct($id) {
        return isset(self::$products[$id]) ? self::$products[$id] : null;
    }

    public static function removeProduct($id) {
        if (array_key_exists($id, self::$products)) {
            unset(self::$products[$id]);
        }
    }
}

Factory::pushProduct(new Product('first'));
Factory::pushProduct(new Product('second'));

print_r(Factory::getProduct('first')->getId());
// first
print_r(Factory::getProduct('second')->getId());
// second

Прототип

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

Для этого создаётся «прототип» – предварительно инициализированный и сохранённый объект, который затем может быть клонирован и окончательно инициализирован:

<?php

interface Product {
}

class Factory {

    private $product;

    public function __construct(Product $product) {
        $this->product = $product;
    }

    public function getProduct() {
        return clone $this->product;
    }
}

class SomeProduct implements Product {
    public $name;
}

$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

Строитель

Мы можем использовать этот шаблон, чтобы упростить создание сложного объекта:

<?php

class Product {

    private $name;

    public function setName($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Builder {

    protected $product;

    final public function getProduct() {
        return $this->product;
    }

    public function buildProduct() {
        $this->product = new Product();
    }
}

class FirstBuilder extends Builder {

    public function buildProduct() {
        parent::buildProduct();
        $this->product->setName('The product of the first builder');
    }
}

class SecondBuilder extends Builder {

    public function buildProduct() {
        parent::buildProduct();
        $this->product->setName('The product of second builder');
    }
}

class Factory {

    private $builder;

    public function __construct(Builder $builder) {
        $this->builder = $builder;
        $this->builder->buildProduct();
    }

    public function getProduct() {
        return $this->builder->getProduct();
    }
}

$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

Бонус

Бонус

Сергей Бензенкоавтор-переводчик статьи «Design Patterns in PHP»