Паттерны проектирования в php

Введение

Шаблоны проектирования были представлены общественности в книге Design Patterns (Erich Gamma, Richard Helm, Ralph Johnson и John Vlissides (известные как «банда четырёх»)). Основная концепция, представленная во введении, была простой. За годы разработки программного обеспечения Gamma и сотоварищи открыли определённые шаблоны («паттерны») проектирования, как и архитекторы, строящие дома и здания, могут разработать шаблоны расположения уборных или обустройства кухни. Используя эти шаблоны, или паттерны проектирования, можно проектировать качественные здания быстрее. То же применимо и к разработке программного обеспечения.

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

Сложно продемонстрировать значение паттернов на небольших примерах. Они при этом часто выглядят как излишнее усложнение кода, потому что проявляют себя обычно в крупных системах с большим количеством исходного кода. В этой статье не будут рассматриваться большие приложения, так что нужно использовать воображение, чтобы применить принципы примера (и далеко не обязательно точный его код) в своих больших приложениях. Это не означает, что не нужно использовать паттерны в маленьких приложениях. Многие приложения начинаются как небольшие и развиваются в крупные, так что нет причин не придерживаться этих правил сразу.

Теперь, когда стало немного понятно, что такое паттерны проектирования, и для чего они применяются, пора переходить к пяти самым популярным паттернам проектирования PHP 5.

Паттерн Factory (фабрика)

Многие паттерны проектирования в оригинальной книге Design Patterns поощряют слабую связанность. Для понимания этой концепции нужно упомянуть о борьбе, через которую проходят многие разработчики крупных систем. Проблема возникает при изменении куска кода и дальнейшей попытке отследить влияние этого изменения на систему (и в худшем случае — наблюдением за каскадом отказов системы, которые, предполагалось, никак с изменённым кодом не связанными).

Проблема в сильной свзязанности. Функции и классы в одной части системы слишком сильно полагаются на поведение и архитектуру других функций и классов (в другой части системы). Тут нужны паттерны, которые позволят классам общаться между собой, но при этом позволят избежать слишком сильной их привязки друг к другу, что привело бы к некоторому «переплетению» кода независимых частей.

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

Паттерн factory — это класс, который предоставляет методы для создания объектов. Вместо использования конструкции new напрямую, для создания объектов мы используем factory-класс. Весь код, который использует этот factory-класс, меняется автоматически.
В листинге1 приводится пример factory-класса. Серверная сторна состоит из двух частей: база данных и набор php-страниц для добавления каналов (фидов), запроса списка каналов и получения статьи, привязанной к определённому каналу.

Листинг1. Factory1.php

<?php
interface IUser
{
  function getName();
}

class User implements IUser
{
  public function __construct( $id ) { }

  public function getName()
  {
    return "Jack";
  }
}

class UserFactory
{
  public static function Create( $id )
  {
    return new User( $id );
  }
}

$uo = UserFactory::Create( 1 );
echo( $uo->getName()."\n" );
?>

Интерфейс IUser определяет, что следует делать объекту user. Реализация IUser называется User, и factory-класс UserFactory создаёт объекты User (в оригинале написано IUser). На рисунке1 показана связь в виде UML.

Рисунок1. Factory-класс и его интерфейс IUser и класс User


Если запустить этот код в комендной строке, получим:

% php factory1.php 
Jack
%

В коде примера у factory запрашивается объект User и выводится результат метода getName().

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

Листинг2. Factory2.php

<?php
interface IUser
{
  function getName();
}

class User implements IUser
{
  public static function Load( $id ) 
  {
        return new User( $id );
  }

  public static function Create( ) 
  {
        return new User( null );
  }

  public function __construct( $id ) { }

  public function getName()
  {
    return "Jack";
  }
}

$uo = User::Load( 1 );
echo( $uo->getName()."\n" );
?>

Этот код гораздо проще. В нём только один интерфейс — IUser — и один класс — User — для реализации этого интерфейса. У класса User два статических метода, создающих объект (поясню: оба метода создают объект класса User, но с разными параметрами; при этом можно было бы инициализировать свойства класса по-разному в разных методах). На рисунке2 показана связь в виде UML.

Рисунок2. Интерфейс IUser и класс User с двумя factory-методами


Запустив скрипт в командной строке, получим тот же результат, что и в первом примере:

% php factory2.php 
Jack
%

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

Паттерн Singleton (синглтон)

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

Для этого отлично подойдёт паттерн Singleton. Объект является синглтоном, если в приложении можно обратиться к одному и только к одному такому объекту.

Код в листинге 3 показывает реализацию синглтона соединения с базой данных.

Листинг3. Singleton.php

<?php
require_once("DB.php");

class DatabaseConnection
{
  public static function get()
  {
    static $db = null;
    if ( $db == null )
      $db = new DatabaseConnection();
    return $db;
  }

  private $_handle = null;

  private function __construct()
  {
    $dsn = 'mysql://root:password@localhost/photos';
    $this->_handle =& DB::Connect( $dsn, array() );
  }
  
  public function handle()
  {
    return $this->_handle;
  }
}

print( "Handle = ".DatabaseConnection::get()->handle()."\n" );
print( "Handle = ".DatabaseConnection::get()->handle()."\n" );
?>

В коде присутствует один класс, DatabaseConnection. Нельзя создать экземпляр класса DatabaseConnection напрямую, т.к. конструктор класса закрытый. Но можно получить один и только один экземпляр класса DatabaseConnection, использую метод get. UML-диаграмма этого кода показана на рисунке 3.

Рисунок 3. Синглтон соединения с базой данных


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

% php singleton.php 
Handle = Object id #3
Handle = Object id #3
%

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

Паттерн Observer (наблюдатель)

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

Простой пример — список пользователей системы. Код в Листинге 4 показывает список пользователей, который отсылает уведомление при добавлении новых пользователей. За этим списком следит наблюдатель, ведущий лог; при получении уведомления он выводит сообщение.

Листинг 4. Observer.php

<?php
interface IObserver
{
  function onChanged( $sender, $args );
}

interface IObservable
{
  function addObserver( $observer );
}

class UserList implements IObservable
{
  private $_observers = array();

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

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

class UserListLogger implements IObserver
{
  public function onChanged( $sender, $args )
  {
    echo( "'$args' added to user list\n" );
  }
}

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

В коде определяются 4 элемента: 2 интерфейса и 2 класса. Интерфейс IObservable определяет наблюдаемый объект, а UserList реализует этот интерфейс, чтобы зарегистрироваться в качестве наблюдаемого. IObserver определяет, что нужно, чтобы стать наблюдателем, а UserListLogger реализует интерфейс IObserver. Это показано в виде UML на рисунке 4.

Рисунок 4. Наблюдаемый UserList и наблюдатель UserListLogger


При запуске в командной строке получим вывод:

% php observer.php 
'Jack' added to user list
%

Код примера создаёт UserList и добавляет UserListLogger в качестве наблюдателя. Затем добавляется новый посетитель, и UserListLogger уведомляется об этом изменении.
Важно понимать, что UserList не знает, что логгер собирается сделать. Может быть один или несколько наблюдателей, которые будут делать что-то другое. К примеру, можно сделать наблюдателя, который будет посылать некоторое сообщение новому пользователю, приветствуя вновь прибывшего.
Ценность такого подхода — в том, что UserList ничего не знает об объектах, зависимых от него; он концентрируется на управлении списком пользователей и рассылает уведомления при его изменении.

Этот паттерн не ограничивается объектами, хранящимися в памяти. Он используется и для систем очереди сообщений на базе баз данных (database-driven message queuing systems), использующихся в крупных приложениях.

Паттерн Сhain-of-command (цепочка команд)

Для реализации идеи слабой связанности паттерн Сhain-of-command передаёт сообщение, команду, запрос, как угодно, через набор обработчиков. Каждый обработчик решает, сможет ли он обработать этот запрос. Если может, запрос обрабатывается и процесс передачи останавливается. Можно добавлять/удалять обработчики без влияния на другие обработчики. Листинг 5 показывает пример реализации этого паттерна.

Листинг 5. Chain.php

<?php
interface ICommand
{
  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 UserCommand implements ICommand
{
  public function onCommand( $name, $args )
  {
    if ( $name != 'addUser' ) return false;
    echo( "UserCommand handling 'addUser'\n" );
    return true;
  }
}

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

$cc = new CommandChain();
$cc->addCommand( new UserCommand() );
$cc->addCommand( new MailCommand() );
$cc->runCommand( 'addUser', null );
$cc->runCommand( 'mail', null );
?>

В коде определяется класс CommandChain, который управляет списком ICommand-объектов. Два класса реализуют интерфейс ICommand — один отвечает на запросы на почту, а другой — за добавление пользователей. UML показан на рисунке 5.

Рисунок 5. Цепочка команд и соответствующие команды


При запуске из командной строки скрипт выведет:

% php chain.php 
UserCommand handling 'addUser'
MailCommand handling 'mail'
%

Код сначала создаёт объект CommandChain и добавляет в него два объекта команд. Потом запускаются две команды, чтобы посмотреть, кто на них ответит. Если имя команды не соответствует ни UserCommand, ни MailCommand, то код отрабатывает и ничего не происходит.

Паттерн Chain-of-command может быть ценным при создании расширяемой архитектуры обработки запросов, которая может быть применена для решения многих проблем.

Паттерн Strategy (стратегия)

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

В более простом примере Листинга 6 приведён класс списка пользователей, который обеспечивает метод поиска набора пользователей, основанный на plug-and-play-наборе стратегий.

Листинг 6. Strategy.php

<?php
interface IStrategy
{
  function filter( $record );
}

class FindAfterStrategy implements IStrategy
{
  private $_name;

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

  public function filter( $record )
  {
    return strcmp( $this->_name, $record ) <= 0;
  }
}

class RandomStrategy implements IStrategy
{
  public function filter( $record )
  {
    return rand( 0, 1 ) >= 0.5;
  }
}

class UserList
{
  private $_list = array();

  public function __construct( $names )
  {
    if ( $names != null )
    {
      foreach( $names as $name )
      {
        $this->_list []= $name;
      }
    }
  }

  public function add( $name )
  {
    $this->_list []= $name;
  }

  public function find( $filter )
  {
    $recs = array();
    foreach( $this->_list as $user )
    {
      if ( $filter->filter( $user ) )
        $recs []= $user;
    }
    return $recs;
  }
}

$ul = new UserList( array( "Andy", "Jack", "Lori", "Megan" ) );
$f1 = $ul->find( new FindAfterStrategy( "J" ) );
print_r( $f1 );

$f2 = $ul->find( new RandomStrategy() );
print_r( $f2 );
?>

UML примера представлен на рисунке 6.

Рисунок 6. Список пользователей и стратегии отбора пользователей


Класс UserList представляет собой обёртку для массива имён. Он реализует метод find, который работает с одной из нескольких стратегий отбора из этих имён. Эти стратегии определены интерфейсом IStrategy, у которого есть две реализации: одна выбирает пользователей случайным образом, а в другой выбираются все имена после определённого (т.е. с учётом алфафвита).

При запуске в командной строке получим следующее:

% php strategy.php 
Array
(
    [0] => Jack
    [1] => Lori
    [2] => Megan
)
Array
(
    [0] => Andy
    [1] => Megan
)
%

В примере один и тот же список пользователей прогоняется через две стратегии и выводятся результаты. В первом случае стратегия ищет все имена, «больше», чем «J» (Jack, Lori и Megan). Вторая стратегия выбирает имена случайным образом, и поэтому выдаёт разные результаты при разных запусках. В нашем случае это Andy и Megan.

Паттерн Strategy очень хорош для сложных систем управления данными, где нужна большая гибкость в фильтрации, поиске и обработке данных.

Выводы

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

Перевод статьи.

23 комментария so far.

  1. Тёмыч:
    Отличная статья. Я полгода как в серьез взялся за php и теперь неизбежно пришел к паттернам все про них говорят, а у вас тут про них толково и понятно написано — мне новичку понятно. Идеи, кстати классные. Сайт в закладки.
  2. Очень напоминает информацию из книги: «PHP 5 Advanced OOP and Design Patterns By Stig Bakken, Andi Gutmans, Derick Rethans»
  3. Bael:
    Конечно интересная статья, особенно в оригинале, но перевод какой-то не совсем логичный и понятный..
  4. Действительно, статья просто и наглядно рассказывает про паттерны.
    спасибо большое
  5. stnw:
    Очень хорошо написано.
    Спасибо за перевод.
  6. Breeze:
    есть продолжение оригинальной статьи http://www.ibm.com/developerworks/opensource/library/os-php-designpatterns
    Было бы очень хорошо, что бы его автор также перевел.
    :)
  7. bullgare:
    да там вроде ничего сложного)
    постараюсь
  8. ждем с нетерпением продолжения
  9. саша:
    у меня такое впечатление что этот код

    public static function get()
    {
    static $db = null;
    if ( $db == null )
    $db = new DatabaseConnection();
    return $db;
    }

    всегда будет возвращать новый экземпляр класса DatabaseConnection();
    переубедите меня пожалуйста

  10. Иван:
    Безобразные стрелочки на диаграммах
  11. Супер, очень наглядное и хорошее объяснение. Спасибо за статью.
  12. Богдан:
    Помоему в Singleton опечатка,
    static $db = null нужно вынести из метода get() в описание класса.

    public static function get()
    {
    static $db = null; // <— вот это
    if ( $db == null )
    $db = new DatabaseConnection();
    return $db;
    }

  13. Виктор:
    Богдан, да, Вы правы.
  14. monster:
    В описании патерна Observer есть переменная $sender, которая не применяет нигде.
    • bullgare:
      Потому что это упрощённый пример.
      В реальном мире нужно знать, кто вызвал наблюдателя, а для этого — обратиться к его свойствам/методам.
  15. […] Наблюдатель (Observer) в PHP | perevodik.net Паттерн Observer (наблюдатель) | job-blog.bullgare.com Pattern Observer — «Наблюдатель» | omurashov.ru Паттерн Observer | […]
  16. Антон:
    В последнем листинге в классе UserList в foreach (__constract метод) почему не вызывали метод add? Метод add вообще не используется.

Добавить комментарий для bullgare Отменить ответ