Паттерны проектирования в 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

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

Выводы

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

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

Similar Posts

23 Responses 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 вообще не используется.

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