Паттерны ооп с примерами и описанием. Проблема инициализации объектов в ООП приложениях на PHP. Поиск решения при помощи шаблонов Registry, Factory Method, Service Locator и Dependency Injection Что нам это даст

Постараюсь расказать о моей реализации паттерна Registry под php. Registry — это ООП замена глобальным переменным, предназначеная для хранения данных и передачи их между модулями системы. Соответственно, его наделяют стандартными свойствами — запись, чтение, удаление. Вот типовая реализация.

Ну и таким образом получаем тупую замену методов $key = $value — Registry::set($key, $value) $key — Registry::get($key) unset($key) — remove Registry::remove($key) Только становится непонятно — а зачем этот лишний код. Итак, научим наш класс делать то, что не умеют глобальные переменные. Добавим в него перчика.

getMessage()); } Amdy_Registry::unlock("test"); var_dump(Amdy_Registry::get("test")); ?>

К типичным задачам паттерна, я добавил возможность блокировки переменной от изменений, это очень удобно на больших проектах, случайно не всунешь ничего. Например, удобно для работы с бд
define(‘DB_DNS’, ‘mysql:host=localhost;dbname=’);
define(‘DB_USER’, ‘’);
define(‘DB_PASSWORD’, ‘’);
define(‘DB_HANDLE’);

Amdy_Regisrtry::set(DB_HANDLE, new PDO(DB_DNS, DB_USER, DB_PASSWORD));
Amdy_Registry::lock(DB_HANDLE);

Сейчас пояснения по коду, чтобы хранить данные, мы используем статическую переменную $data, в переменной $lock хранятся данные о заблокированых для изменения ключах. В сетере мы проверяем залочена ли переменная и изменяем или добавляем её в регистр. При удалении, также проверяем лок, гетер остаётся неизменным, за исключением опционального параметра по умолчанию. Ну и стоит обратить внимание на обработку исключений, которой почему-то редко пользуются, кстати, у меня уже есть черновик по исключениям, ждите статью. Чуть ниже черновой код для тестирования, вот и статью о тестировании, тоже бы не мешало настрочить, хотя я и не почетатель TDD.

В следующей статье ещё расширим функционал, дабавив инициализацию данных и реализуем «ленивость».

Проблема инициализации объектов в ООП приложениях на PHP. Поиск решения при помощи шаблонов Registry, Factory Method, Service Locator и Dependency Injection

Так уж повелось, что программисты закрепляют удачные решения в виде шаблонов проектирования. По шаблонам существует множество литературы. Классикой безусловно считается книга Банды четырех «Design Patterns» by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides" и еще, пожалуй, «Patterns of Enterprise Application Architecture» by Martin Fowler . Лучшее из того, что я читал с примерами на PHP – это . Так уж получилось, что вся эта литература достаточно сложна для людей, которые только начали осваивать ООП. Поэтому у меня появилась идея изложить некоторые паттерны, которые я считаю наиболее полезными, в сильно упрощенном виде. Другими словами, эта статья – моя первая попытка интерпретировать шаблоны проектирования в KISS стиле.
Сегодня речь пойдет о том, какие проблемы могут возникнуть с инициализацией объектов в ООП приложении и о том, как можно использовать некоторые популярные шаблоны проектирования для решения этих проблем.

Пример

Современное ООП приложение работает с десятками, сотнями, а иногда и тысячами объектов. Что же, давайте внимательно посмотрим на то, каким образом происходит инициализация этих объектов в наших приложениях. Инициализация объектов – это единственный аспект, который нас интересует в данной статье, поэтому я решил опустить всю «лишнюю» реализацию.
Допустим, мы создали супер-пупер полезный класс, который умеет отправлять GET запрос на определенный URI и возвращать HTML из ответа сервера. Чтобы наш класс не казался чересчур простым, пусть он также проверяет результат и бросает исключение в случае «неправильного» ответа сервера.

Class Grabber { public function get($url) {/** returns HTML code or throws an exception */} }

Создадим еще один класс, объекты которого будут отвечать за фильтрацию полученного HTML. Метод filter принимает в качестве аргументов HTML код и CSS селектор, а возвращает он пусть массив найденных элементов по заданному селектору.

Class HtmlExtractor { public function filter($html, $selector) {/** returns array of filtered elements */} }

Теперь, представим, что нам нужно получить результаты поиска в Google по заданным ключевым словам. Для этого введем еще один класс, который будет использовать класс Grabber для отправки запроса, а для извлечения необходимого контента класс HtmlExtractor. Так же он будет содержать логику построения URI, селектор для фильтрации полученного HTML и обработку полученных результатов.

Class GoogleFinder { private $grabber; private $filter; public function __construct() { $this->grabber = new Grabber(); $this->filter = new HtmlExtractor(); } public function find($searchString) { /** returns array of founded results */} }

Вы заметили, что инициализация объектов Grabber и HtmlExtractor находится в конструкторе класса GoogleFinder? Давайте подумаем, насколько это удачное решение.
Конечно же, хардкодить создание объектов в конструкторе не лучшая идея. И вот почему. Во-первых, мы не сможем легко подменить класс Grabber в тестовой среде, чтобы избежать отправки реального запроса. Справедливости ради, стоит сказать, что это можно сделать при помощи Reflection API . Т.е. техническая возможность существует, но это далеко не самый удобный и очевидный способ.
Во-вторых, та же проблема возникнет, если мы захотим повторно использовать логику GoogleFinder c другими реализациями Grabber и HtmlExtractor. Создание зависимостей жестко прописано в конструкторе класса. И в самом лучшем случае у нас получится унаследовать GoogleFinder и переопределить его конструктор. Да и то, только если область видимости свойств grabber и filter будет protected или public.
И последний момент, каждый раз при создании нового объекта GoogleFinder в памяти будет создаваться новая пара объектов-зависимостей, хотя мы вполне можем использовать один объект типа Grabber и один объект типа HtmlExtractor в нескольких объектах типа GoogleFinder.
Я думаю, что вы уже поняли, что инициализацию зависимостей нужно вынести за пределы класса. Мы можем потребовать, чтобы в конструктор класса GoogleFinder передавались уже подготовленные зависимости.

Class GoogleFinder { private $grabber; private $filter; public function __construct(Grabber $grabber, HtmlExtractor $filter) { $this->grabber = $grabber; $this->filter = $filter; } public function find($searchString) { /** returns array of founded results */} }

Если мы хотим предоставить другим разработчикам возможность добавлять и использовать свои реализации Grabber и HtmlExtractor, то стоит подумать о введении интерфейсов для них. В данном случае это не только полезно, но и необходимо. Я считаю, что если в проекте мы используем только одну реализацию и не предполагаем создание новых в будущем, то стоит отказаться от создания интерфейса. Лучше действовать по ситуации и сделать простой рефакторинг, когда в нем появится реальная необходимость.
Теперь у нас есть все нужные классы и мы можем использовать класс GoogleFinder в контроллере.

Class Controller { public function action() { /* Some stuff */ $finder = new GoogleFinder(new Grabber(), new HtmlExtractor()); $results = $finder->

Подведем промежуточный итог. Мы написали совсем немного кода, и на первый взгляд, не сделали ничего плохого. Но… а что если нам понадобится использовать объект типа GoogleFinder в другом месте? Нам придется продублировать его создание. В нашем примере это всего одна строка и проблема не так заметна. На практике же инициализация объектов может быть достаточно сложной и может занимать до 10 строк, а то и более. Так же возникают другие проблемы типичные для дублирования кода. Если в процессе рефакторинга понадобится изменить имя используемого класса или логику инициализации объектов, то придется вручную поменять все места. Я думаю, вы знаете как это бывает:)
Обычно с хардкодом поступают просто. Дублирующиеся значения, как правило, выносятся в конфигурацию. Это позволяет централизованно изменять значения во всех местах, где они используются.

Шаблон Registry.

Итак, мы решили вынести создание объектов в конфигурацию. Давайте сделаем это.

$registry = new ArrayObject(); $registry["grabber"] = new Grabber(); $registry["filter"] = new HtmlExtractor(); $registry["google_finder"] = new GoogleFinder($registry["grabber"], $registry["filter"]);
Нам остается только передать наш ArrayObject в контроллер и проблема решена.

Class Controller { private $registry; public function __construct(ArrayObject $registry) { $this->registry = $registry; } public function action() { /* Some stuff */ $results = $this->registry["google_finder"]->find("search string"); /* Do something with results */ } }

Можно дальше развить идею Registry. Унаследовать ArrayObject, инкапсулировать создание объектов внутри нового класса, запретить добавлять новые объекты после инициализации и т.д. Но на мой взгляд приведенный код в полной мере дает понять, что из себя представляет шаблон Registry. Этот шаблон не относится к порождающим, но он в некоторой степени позволяет решить наши проблемы. Registry – это всего лишь контейнер, в котором мы можем хранить объекты и передавать их внутри приложения. Чтобы объекты стали доступными, нам необходимо их предварительно создать и зарегистрировать в этом контейнере. Давайте разберем достоинства и недостатки этого подхода.
На первый взгляд, мы добились своей цели. Мы перестали хардкодить имена классов и создаем объекты в одном месте. Мы создаем объекты в единственном экземпляре, что гарантирует их повторное использование. Если изменится логика создания объектов, то отредактировать нужно будет только одно место в приложении. Как бонус мы получили, возможность централизованно управлять объектами в Registry. Мы легко можем получить список всех доступных объектов, и провести с ними какие-нибудь манипуляции. Давайте теперь посмотрим, что нас может не устроить в этом шаблоне.
Во-первых, мы должны создать объект перед тем как зарегистрировать его в Registry. Соответственно, высока вероятность создания «ненужных объектов», т.е. тех которые будут создаваться в памяти, но не будут использоваться в приложении. Да, мы можем добавлять объекты в Registry динамически, т.е. создавать только те объекты, которые нужны для обработки конкретного запроса. Так или иначе контролировать это нам придется вручную. Соответственно, со временем поддерживать это станет очень тяжело.
Во-вторых, у нас появилась новая зависимость у контроллера. Да, мы можем получать объекты через статический метод в Registry, чтобы не передавать Registry в конструктор. Но на мой взгляд, не стоит этого делать. Статические методы, это даже более жесткая связь, чем создание зависимостей внутри объекта, и сложности в тестировании (вот на эту тему).
В-третьих, интерфейс контроллера ничего не говорит нам о том, какие объекты в нем используются. Мы можем получить в контроллере любой объект доступный в Registry. Нам тяжело будет сказать, какие именно объекты использует контроллер, пока мы не проверим весь его исходный код.

Factory Method

В Registry нас больше всего не устраивает то, что объект необходимо предварительно инициализировать, чтобы он стал доступным. Вместо инициализации объекта в конфигурации, мы можем выделить логику создания объектов в другой класс, у которого можно будет «попросить» построить необходимый нам объект. Классы, которые отвечают за создание объектов называют фабриками. А шаблон проектирования называется Factory Method. Давайте посмотрим на пример фабрики.

Class Factory { public function getGoogleFinder() { return new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor()); } private function getGrabber() { return new Grabber(); } private function getHtmlExtractor() { return new HtmlFiletr(); } }

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

Class Factory { private $finder; public function getGoogleFinder() { if (null === $this->finder) { $this->finder = new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor()); } return $this->finder; } }

Мы можем параметризировать метод фабрики и делегировать инициализацию другим фабрикам в зависимости от входящего параметра. Это уже будет шаблон Abstract Factory.
Если появится необходимость разбить приложение на модули, мы можем потребовать, чтобы каждый модуль предоставлял свои фабрики. Мы можем и дальше развивать тему фабрик, но думаю что суть этого шаблона понятна. Давайте посмотрим как мы будем использовать фабрику в контроллере.

Class Controller { private $factory; public function __construct(Factory $factory) { $this->factory = $factory; } public function action() { /* Some stuff */ $results = $this->factory->getGoogleFinder()->find("search string"); /* Do something with results */ } }

К преимуществам данного подхода, отнесем его простоту. Наши объекты создаются явно, и Ваша IDE легко приведет Вас к месту, в котором это происходит. Мы также решили проблему Registry и объекты в памяти будут создаваться только тогда, когда мы «попросим» фабрику об этом. Но мы пока не решили, как поставлять контроллерам нужные фабрики. Тут есть несколько вариантов. Можно использовать статические методы. Можно предоставить контроллерам самим создавать нужные фабрики и свести на нет все наши попытки избавиться от копипаста. Можно создать фабрику фабрик и передавать в контроллер только ее. Но получение объектов в контроллере станет немного сложнее, да и нужно будет управлять зависимостями между фабриками. Кроме того не совсем понятно, что делать, если мы хотим использовать модули в нашем приложении, как регистрировать фабрики модулей, как управлять связями между фабриками из разных модулей. В общем, мы лишились главного преимущества фабрики – явного создания объектов. И пока все еще не решили проблему «неявного» интерфейса контроллера.

Service Locator

Шаблон Service Locator позволяет решит недостаток разрозненности фабрик и управлять созданием объектов автоматически и централизованно. Если подумать, мы можем ввести дополнительный слой абстракции, который будет отвечать за создание объектов в нашем приложении и управлять связями между этими объектами. Для того чтобы этот слой смог создавать объекты для нас, мы должны будем наделить его знаниями, как это делать.
Термины шаблона Service Locator:
  • Сервис (Service) - готовый объект, который можно получить из контейнера.
  • Описание сервиса (Service Definition) – логика инициализации сервиса.
  • Контейнер (Service Container) – центральный объект который хранит все описания и умеет по ним создавать сервисы.
Любой модуль может зарегистрировать свои описания сервисов. Чтобы получить какой-то сервис из конейнера мы должны будем запросить его по ключу. Существует масса вариантов реализации Service Locator, в простейшем варианте мы можем использовать ArrayObject в качестве контейнера и замыкания, в качестве описания сервисов.

Class ServiceContainer extends ArrayObject { public function get($key) { if (is_callable($this[$key])) { return call_user_func($this[$key]); } throw new \RuntimeException("Can not find service definition under the key [ $key ]"); } }

Тогда регистрация Definitions будет выглядеть так:

$container = new ServiceContainer(); $container["grabber"] = function () { return new Grabber(); }; $container["html_filter"] = function () { return new HtmlExtractor(); }; $container["google_finder"] = function() use ($container) { return new GoogleFinder($container->get("grabber"), $container->get("html_filter")); };

А использование, в контроллере так:

Class Controller { private $container; public function __construct(ServiceContainer $container) { $this->container = $container; } public function action() { /* Some stuff */ $results = $this->container->get("google_finder")->find("search string"); /* Do something with results */ } }

Service Container может быть очень простым, а может быть очень сложным. Например, Symfony Service Container предоставляет массу возможностей: параметры (parameters), области видимости сервисов (scopes), поиск сервисов по тегам (tags), псевдонимы (aliases), закрытые сервисы (private services), возможность внести изменения в контейнер после добавления всех сервисов (compiller passes) и еще много чего. DIExtraBundle еще больше расширяет возможности стандартной реализации.
Но, вернемся к нашему примеру. Как видим, Service Locator не только решает все те проблемы, что и предыдущие шаблоны, но и позволяет легко использовать модули с собственными определениями сервисов.
Кроме того, на уровне фреймворка мы получили дополнительный уровень абстракции. А именно, изменяя метод ServiceContainer::get мы сможем, например, подменить объект на прокси. А область применения прокси-объектов ограниченна лишь фантазией разработчика. Тут можно и AOP парадигму реализовать, и LazyLoading и т.д.
Но, большинство разработчиков, все таки считают Service Locator анти-паттерном. Потому что, в теории, мы можем иметь сколько угодно т.н. Container Aware классов (т.е. таких классов, которые содержат в себе ссылку на контейнер). Например, наш Controller, внутри которого мы можем получить любой сервис.
Давайте, посмотрим, почему это плохо.
Во-первых, опять же тестирование. Вместо того, чтобы создавать моки только для используемых классов в тестах придется делать мок всему контейнеру или использовать реальный контейнер. Первый вариант не устраивает, т.к. приходится писать много ненужного кода в тестах, второй, т.к. он противоречит принципам модульного тестирования, и может привести к дополнительным издержкам на поддержку тестов.
Во-вторых, нам будет трудно рефакторить. Изменив любой сервис (или ServiceDefinition) в контейнере, мы будем вынуждены проверить также все зависимые сервисы. И эта задача не решается при помощи IDE. Отыскать такие места по всему приложению будет не так-то и просто. Кроме зависимых сервисов, нужно будет еще проверить все места, где отрефакторенный сервис получается из контейнера.
Ну и третья причина в том, что бесконтрольное дергание сервисов из контейнера рано или поздно приведет к каше в коде и излишней путанице. Это сложно объяснить, просто Вам нужно будет тратить все больше и больше времени, чтобы понять как работает тот или иной сервис, иными словами полностью понять что делает или как работает класс можно будет только прочитав весь его исходный код.

Dependency Injection

Что же можно еще предпринять, чтобы ограничить использование контейнера в приложении? Можно передать в фреймворк управление созданием всех пользовательских объектов, включая контроллеры. Иными словами, пользовательский код не должен вызывать метод get у контейнера. В нашем примере мы cможем добавить в контейнер Definition для контроллера:

$container["google_finder"] = function() use ($container) { return new Controller(Grabber $grabber); };

И избавиться от контейнера в контроллере:

Class Controller { private $finder; public function __construct(GoogleFinder $finder) { $this->finder = $finder; } public function action() { /* Some stuff */ $results = $this->finder->find("search string"); /* Do something with results */ } }

Такой вот подход (когда доступ к Service Container не предоставляется клиентским классам) называют Dependency Injection. Но и этот шаблон имеет как преимущества, так и недостатки. Пока у нас соблюдается принцип единственной ответственности, то код выглядит очень красиво. В-первую очередь, мы избавились от контейнера в клиентских классах, благодаря чему их код стал намного понятнее и проще. Мы легко можем протестировать контроллер, подменив необходимые зависимости. Мы можем создавать и тестировать каждый класс независимо от других (в том числе и классы контроллеров) используя TDD или BDD подход. При создании тестов мы сможем абстрагироваться от контейнера, и позже добавить Definition, когда нам понадобится использовать конкретные экземпляры. Все это сделает наш код проще и понятнее, а тестирование прозрачнее.
Но, необходимо упомянуть и об обратной стороне медали. Дело в том, что контроллеры – это весьма специфичные классы. Начнем с того, что контроллер, как правило, содержит в себе набор экшенов, значит, нарушает принцип единственной ответственности. В результате у класса контроллера может появиться намного больше зависимостей, чем необходимо для выполнения конкретного экшена. Использование отложенной инициализации (объект инстанцианируется в момент первого использования, а до этого используется легковесный прокси) в какой-то мере решает вопрос с производительностью. Но с точки зрения архитектуры создавать множество зависимостей у контроллера тоже не совсем правильно. Кроме того тестирование контроллеров, как правило излишняя операция. Все, конечно, зависит от того как тестирование организовано в Вашем приложении и от того как вы сами к этому относитесь.
Из предыдущего абзаца Вы поняли, что использование Dependency Injection не избавляет полностью от проблем с архитектурой. Поэтому, подумайте как Вам будет удобнее, хранить в контроллерах ссылку на контейнер или нет. Тут нет единственно правильного решения. Я считаю что оба подхода хороши до тех пор, пока код контроллера остается простым. Но, однозначно, не стоит создавать Conatiner Aware сервисы помимо контроллеров.

Выводы

Ну вот и пришло время подбить все сказанное. А сказано было немало… :)
Итак, чтобы структурировать работу по созданию объектов мы можем использовать следующие паттерны:
  • Registry : Шаблон имеет явные недостатки, самый основной из которых, это необходимость создавать объекты перед тем как положить их в общий контейнер. Очевидно, что мы получим скорее больше проблем, чем выгоды от его использования. Это явно не лучшее применение шаблона.
  • Factory Method : Основное достоинство паттерна: объекты создаются явно. Основной недостаток: контроллеры должны либо сами беспокоиться о создании фабрик, что не решает проблему хардкода имен классов полностью, либо фреймворк должен отвечать за снабжение контроллеров всеми необходимыми фабриками, что будет уже не так очевидно. Отсутствует возможность централизованно управлять процессом создания объектов.
  • Service Locator : Более «продвинутый» способ управлять созданием объектов. Дополнительный уровень абстракции может быть использован, чтобы автоматизировать типичные задачи встречающиеся при создании объектов. Например:
    class ServiceContainer extends ArrayObject { public function get($key) { if (is_callable($this[$key])) { $obj = call_user_func($this[$key]); if ($obj instanceof RequestAwareInterface) { $obj->setRequest($this->get("request")); } return $obj; } throw new \RuntimeException("Can not find service definition under the key [ $key ]"); } }
    Недостаток Service Locator в том, что публичный API классов перестает быть информативным. Необходимо прочитать весь код класса, чтобы понять, какие сервисы в нем используются. Класс, который содержит ссылку на контейнер сложнее протестировать.
  • Dependency Injection : По сути мы можем использовать тот же Service Container, что и для предыдущего паттерна. Разница в том, как этот контейнер используется. Если мы будем избегать создания классов зависимых от контейнера, мы получим четкий и явный API классов.
Это не все, что я хотел бы рассказать о проблеме создания объектов в PHP приложениях. Есть еще паттерн Prototype, мы не рассмотрели использование Reflection API, оставили в стороне проблему ленивой загрузки сервисов да и еще много других нюансов. Статья получилась не маленькая, потому закругляюсь:)
Я хотел показать, что Dependency Injection и другие паттерны не так уж и сложны, как принято считать.
Если говорить о Dependency Injection, то существуют и KISS реализации этого паттерна, например

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

Рассмотрим оба варианта по порядку.

То, что называются «чистым реестром» или просто Registry представляет собой реализацию класса со статическим интерфейсом. Основным отличием от паттерна Singleton является блокирование возможности создания хотя бы одного экземпляра класса. Ввиду этого скрывать магические методы __clone() и __wakeup() за модификатором private или protected нет смысла.

Класс Registry должен иметь два статических метода – геттер и сеттер. Сеттер помещает передаваемый объект в хранилище с привязкой к заданному ключу. Геттер, соответственно, возвращает объект из хранилища. Хранилище – не что иное, как ассоциативный массив ключ – значение.

Для полного контроля над реестром вводят еще один элемент интерфейса – метод, позволяющий удалить объект из хранилища.

Помимо проблем, идентичных паттерну Singleton, выделают еще две:

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

В первом случае избежать дополнительной зависимости невозможно. В какой-то степени мы действительно становимся привязаны к именам ключей.

Вторая проблема решается введением проверки в метод Registry::set() :

Public static function set($key, $item) { if (!array_key_exists($key, self::$_registry)) { foreach (self::$_registry as $val) { if ($val === $item) { throw new Exception("Item already exists"); } } self::$_registry[$key] = $item; } }

«Чистый паттерн Registry » порождает еще одну проблему – усиление зависимости за счет необходимости обращения к сеттеру и геттеру через имя класса. Нельзя создать ссылку на объект и работать с ней, как в случае с паттерном Одиночка, когда был доступен такой подход:

$instance = Singleton::getInstance(); $instance->Foo();

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

Для разрешения этого вопроса существует реализация Singleton Registry , которую многие не любят за избыточный, как им кажется код. Я думаю, причиной такого отношения является некоторое непонимание принципов ООП или осознанное пренебрежение ими.

_registry[$key] = $object; } static public function get($key) { return self::getInstance()->_registry[$key]; } private function __wakeup() { } private function __construct() { } private function __clone() { } } ?>

С целью экономии, осознанно опустил блоки комментариев для методов и свойств. Полагаю, в них нет необходимости.

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

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

Singleton (одиночка)

Основной смысл «одиночки» в том, чтобы когда вы говорите «Мне нужна телефонная станция», вам бы говорили «Она уже построена там-то», а не «Давай ее сделаем заново». «Одиночка» всегда один.

Class Singleton { private static $instance = null; private function __construct(){ /* ... @return Singleton */ } // Защищаем от создания через new Singleton private function __clone() { /* ... @return Singleton */ } // Защищаем от создания через клонирование private function __wakeup() { /* ... @return Singleton */ } // Защищаем от создания через unserialize public static function getInstance() { if (is_null(self::$instance)) { self::$instance = new self; } return self::$instance; } }

Registry (реестр, журнал записей)

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

Class Registry { private $registry = array(); public function set($key, $object) { $this->registry[$key] = $object; } public function get($key) { return $this->registry[$key]; } }

Singleton Registry (одинокий реестр) - не путайте с )

«Реестр» нередко является «одиночкой», однако это не всегда должно быть именно так. Например мы можем заводить в бухгалтерии несколько журналов, в одном работники от «А» до «М», в другом от «Н» до «Я». Каждый такой журнал будет «реестром», но не «одиночкой», потому как журналов уже 2.

Class SingletonRegistry { private static $instance = null; private $registry = array(); private function __construct(){ /* ... @return Singleton */ } // Защищаем от создания через new Singleton private function __clone() { /* ... @return Singleton */ } // Защищаем от создания через клонирование private function __wakeup() { /* ... @return Singleton */ } // Защищаем от создания через unserialize public static function getInstance() { if (is_null(self::$instance)) { self::$instance = new self; } return self::$instance; } public function set($key, $object) { $this->registry[$key] = $object; } public function get($key) { return $this->registry[$key]; } }

Multiton (пул «одиночек») или другими словами Registry Singleton (реестр одиночек ) - не путайте с Singleton Registry (одинокий реестр)

Нередко «реестр» служит именно для хранения «одиночек». Но, т.к. паттерн «реестр» не является «порождающим паттерном» а хотелось бы рассматривать «реестр» во взаимосвязи с «одиночкой» . Поэтому придумали паттерн Multiton , который по своей сути является «реестром» содержащий несколько «одиночек», каждый из которых имеет своё «имя» по которому к нему можно получить доступ.

Коротко : позволяет создавать объекты данного класса, но только в случае именования объекта. Жизненного примера нет, но в интернете нарыл такой пример:

Class Database { private static $instances = array(); private function __construct() { } private function __clone() { } public static function getInstance($key) { if(!array_key_exists($key, self::$instances)) { self::$instances[$key] = new self(); } return self::$instances[$key]; } } $master = Database::getInstance("master"); var_dump($master); // object(Database)#1 (0) { } $logger = Database::getInstance("logger"); var_dump($logger); // object(Database)#2 (0) { } $masterDupe = Database::getInstance("master"); var_dump($masterDupe); // object(Database)#1 (0) { } // Fatal error: Call to private Database::__construct() from invalid context $dbFatalError = new Database(); // PHP Fatal error: Call to private Database::__clone() $dbCloneError = clone $masterDupe;

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

По сути данный паттерн является «реестром», который хранит только объекты, никаких строк, массивов и т.п. типов данных.

Factory (фабрика)

Суть паттерна практически полностью описывается его названием. Когда вам требуется получать какие-то объекты, например пакеты сока, вам совершенно не нужно знать как их делают на фабрике. Вы просто говорите «дай мне пакет апельсинового сока», а «фабрика» возвращает вам требуемый пакет. Как? Всё это решает сама фабрика, например «копирует» уже существующий эталон. Основное предназначение «фабрики» в том, чтобы можно было при необходимости изменять процесс «появления» пакета сока, а самому потребителю ничего об этом не нужно было сообщать, чтобы он запрашивал его как и прежде. Как правило, одна фабрика занимается «производством» только одного рода «продуктов». Не рекомендуется «фабрику соков» создавать с учетом производства автомобильных покрышек. Как и в жизни, паттерн «фабрика» часто создается «одиночкой».

Abstract class AnimalAbstract { protected $species; public function getSpecies() { return $this->species; } } class Cat extends AnimalAbstract { protected $species = "cat"; } class Dog extends AnimalAbstract { protected $species = "dog"; } class AnimalFactory { public static function factory($animal) { switch ($animal) { case "cat": $obj = new Cat(); break; case "dog": $obj = new Dog(); break; default: throw new Exception("Animal factory could not create animal of species "" . $animal . """, 1000); } return $obj; } } $cat = AnimalFactory::factory("cat"); // object(Cat)#1 echo $cat->getSpecies(); // cat $dog = AnimalFactory::factory("dog"); // object(Dog)#1 echo $dog->getSpecies(); // dog $hippo = AnimalFactory::factory("hippopotamus"); // This will throw an Exception

Хочется обратить внимание, что метод factory также представляет собой паттерн, его называют Factory method (фабричный метод).

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

Итак, мы уже поняли, что «Фабрика» — это автомат по продаже напитков, в нем уже есть всё готовое, а Вы только говорите что вам нужно. «Строитель» — это завод, который производит эти напитки и содержит в себе все сложные операции и может собирать сложные объекты из более простых (упаковка, этикетка, вода, ароматизаторы и т.п.) в зависимости от запроса.

Class Bottle { public $name; public $liters; } /** * все строители должны */ interface BottleBuilderInterface { public function setName(); public function setLiters(); public function getResult(); } class CocaColaBuilder implements BottleBuilderInterface { private $bottle; public function __construct() { $this->bottle = new Bottle(); } public function setName($value) { $this->bottle->name = $value; } public function setLiters($value) { $this->bottle->liters = $value; } public function getResult() { return $this->bottle; } } $juice = new CocaColaBuilder(); $juice->setName("Coca-Cola Light"); $juice->setLiters(2); $juice->getResult();

Prototype (прототип)

Напоминая «фабрику», он также служит для создания объектов, однако с немного другим подходом. Представьте себя в баре, Вы пили пиво и оно у Вас закончивается, Вы говорите бармену - сделай мне еще одно такое же. Бармен в свою очередь смотрит на пиво, которое Вы пьете и делает копию, как Вы попросили. В php уже есть реализация такого паттерна, она называется .

$newJuice = clone $juice;

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

Например, начальник видит список отчетов по разным видам деятельности и думает, что эти отчеты уже есть, но на самом выводятся только названия отчетов, а сами отчеты еще не сформированы, и будут формироваться только по приказу (например по нажатию кнопки Просмотреть отчет). Частный случай ленивой инициализации — создание объекта в момент обращения к нему. На википедии можно найти интересный, но , т.к. согласно теории , правильным примером в php будет например функция

Adapter или Wrapper (адаптер, обертка)

Данный паттерн полностью соответствует своему названию. Чтобы заставить работать «советскую» вилку через евро-розетку требуется переходник. Именно это и делает «адаптер», - служит промежуточным объектом между двумя другими, которые не могут работать напрямую друг с другом. Не смотря на определение, в практике я все же вижу разницу между Adapter и Wrapper.

Class MyClass { public function methodA() {} } class MyClassWrapper { public function __construct(){ $this->myClass = new MyClass(); } public function __call($name, $arguments){ Log::info("You are about to call $name method."); return call_user_func_array(array($this->myClass, $name), $arguments); } } $obj = new MyClassWrapper(); $obj->methodA();

Dependency injection (внедрение зависимости)

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

Class AppleJuice {} // этот метод представляет собой примитивную реализацию паттерна Dependency injection и дальше Вы в этом убедитесь function getBottleJuice(){ $obj = new AppleJuice AppleJuice ){ return $obj; } } $bottleJuice = getBottleJuice();

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

Class AppleJuice {} Class OrangeJuice {} // этот метод реализовывает Dependency injection function getBottleJuice(){ $obj = new OrangeJuice ; // проверим объект, а то вдруг нам подсунули пиво (пиво ведь не сок) if($obj instanceof OrangeJuice ){ return $obj; } }

Как видите, нам пришлось изменить не только вид сока, но и проверку на вид сока, не очень то удобно. Гораздо правильнее использовать принцип Dependency inversion:

Interface Juice {} Class AppleJuice implements Juice {} Class OrangeJuice implements Juice {} function getBottleJuice(){ $obj = new OrangeJuice; // проверим объект, а то вдруг нам подсунули пиво (пиво ведь не сок) if($obj instanceof Juice ){ return $obj; } }

Dependency inversion иногда путают с Dependency injection, но путать их не нужно, т.к. Dependency inversion это принцип, а не паттерн.

Service Locator (локатор служб)

«Локатор служб» является методом реализации «внедрения зависимости». Он возвращает разные типы объектов в зависимости от кода инициализации. Пускай задача стоит доставить наш пакет сока, созданный строителем, фабрикой или ещё чем, куда захотел покупатель. Мы говорим локатору «дай нам службу доставки», и просим службу доставить сок по нужному адресу. Сегодня одна служба, а завтра может быть другая. Нам без разницы какая это конкретно служба, нам важно знать, что эта служба доставит то, что мы ей скажем и туда, куда скажем. В свою очередь службы реализуют интерфейс «Доставить <предмет> на <адрес>».

Если говорить о реальной жизни, то наверное хорошим примером Service Locator-а может быть php-расширение PDO, т.к. сегодня мы работаем с базой данных MySQL, а завтра можем работать с PostgreSQL. Как Вы уже поняли, нашему классу не важно в какую базу данных отправлять свои данные, важно, что он может это делать.

$db = new PDO("mysql :dbname=test;host=localhost", $user, $pass); $db = new PDO("pgsql :dbname=test host=localhost", $user, $pass);

Отличие Dependency injection от Service Locator

Если Вы еще не заметили, то хочется пояснить. Dependency injection в результате возвращает не сервис (которым можно что-то куда-то доставить) а объект, данные которого использует.

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

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

Начало начал

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

Но не так быстро. Для его работы нужно капнуть еще чуть глубже. Наша система представляет иерархию, а любая иерархическая система имеет начало: точка монтирование в Linux, локальный диск в Windows, система государства, компании, учебного заведения и т.д. Каждый элемент такой системы кому-то подчинен и может иметь несколько подчиненных, а для обращения к своим соседям и их подчиненным использует вышестоящих или само начало. Хорошим примером иерархической системы является генеалогическое дерево: выбирается точка отсчета — какой-либо предок и понеслась. В нашей системе нам также нужна точка отсчета, из которой мы будем растить ветви — модули, плагины и т.д. Нам нужен некий интерфейс, через который будут «общаться» все наши модули. Для дальнейшей работы нам нужно познакомиться с понятием «шаблон проектирования» и парочка их реализаций.

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

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

Шаблоны проектирования также часто называют паттернами проектирования или просто паттернами (от английского слова pattern, в переводе означающего «шаблон»). Дальше в статьях, говоря о паттернах, я буду подразумевать именно шаблоны проектирования.

Из огромного списка всевозможных страшных (и не очень) названий паттернов нас интересуют пока всего два: реестр (registry) и одиночка (singleton).

Реестр (или регистр ) — паттерн, который оперирует неким массивом, в который можно добавить и удалить некое множество объектов и получать доступ к любому из них и его возможностям.

Одиночка (или синглтон ) — паттерн, который гарантирует что может существовать только один экземпляр класса. Его невозможно скопировать, усыпить или разбудить (речь о магии PHP: __clone(), __sleep(), __wakeup()). Синглтон имеет глобальную точку доступа.

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

Что нам это даст
  • У нас будет гарантировано единственный экземпляр реестра, в который мы в любой момент можем добавлять объекты и использовать их из любого места в коде;
  • его невозможно будет скопировать и воспользоваться прочей нежелательной (в этом случае) магией языка PHP.

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

Ну хватит уже слов, давайте творить!

Первые строки

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

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

Так как мы защищаем что-то подключаемое, константу обзовем _PLUGSECURE_ :

If (!defined("_PLUGSECURE_")) { die("Прямой вызов модуля запрещен!"); }

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

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

Interface StorableObject { public static function getClassName(); }

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

Пришла пора самого класса нашего реестра-одиночки. Начнем мы с объявления класса и некоторых его переменных:

Class Registry implements StorableObject { //имя модуля, читаемое private static $className = "Реестр"; //экземпляр реестра private static $instance; //массив объектов private static $objects = array();

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

Public static function singleton() { if(!isset(self::$instance)) { $obj = __CLASS__; self::$instance = new $obj; } return self::$instance; }

Буквально: функция проверяет, существует ли экземпляр нашего реестра: если нет, создает его и возвращает, если уже существует — просто возвращает. В таком случае, некоторая магия нам ненужна, для защиты объявим ее приватной:

Private function __construct(){} private function __clone(){} private function __wakeup(){} private function __sleep() {}

Теперь нам нужна функция добавления объекта в наш реестр — такая функция называется сеттер (setter) и я решил реализовать ее двумя способами, чтобы показать как можно использовать магию и предоставить альтернативный способ добавления объекта. Первый метод — стандартная функция, второй — через магию __set() выполняет первый.

//$object - путь к подключаемому объекту //$key - ключ доступа к объекту в регистре public function addObject($key, $object) { require_once($object); //создаем объект в массиве объектов self::$objects[$key] = new $key(self::$instance); } //альтернативный метод через магию public function __set($key, $object) { $this->addObject($key, $object); }

теперь, чтобы добавить объект в наш реестр можно воспользоваться двумя видами записи (допустим у нас уже создан экземпляр реестра $registry и мы хотим добавить файл config.php):

$registry->addObject("config", "/core/config.php"); //обычный метод $registry->config = "/core/config.php"; //через магическую функцию PHP __set()

Обе записи выполнят одну и туже функцию — подключат файл, создадут экземпляр класса и поместят его в регистр с ключом. Тут есть один важный момент, в дальнейшем про него нельзя забывать: ключ объекта в регистре должен совпадать с именем класса в подключаемом объекте. Если еще раз посмотрите код, поймете почему.

Какую запись использовать — решать вам. Мне больше нравится запись через магический метод — она «красивее» и короче.

Так, с добавлением объекта разобрались, теперь нужна функция доступа к подключенному объекту по ключу — геттер (getter). Его я реализовал так же двумя функциями, аналогично сеттеру:

//получаем объект из регистра //$key - ключ в массиве public function getObject($key) { //проверяем является ли переменная объектом if (is_object(self::$objects[$key])) { //если да, то возвращаем этот объект return self::$objects[$key]; } } //аналогичный метод через магию public function __get($key) { if (is_object(self::$objects[$key])) { return self::$objects[$key]; } }

Как и с сеттером, для получения доступа к объекту у нас будет 2 равносильных записи:

$registry->getObject("config"); //обычный метод $registry->config; //через магическую функцию PHP __get()

Внимательный читатель сразу задаст вопрос: почему в магической функции __set() я просто вызываю обычную (не магическую) функцию добавления объекта, а в геттере __get() я копирую код функции getObject() вместо такого же вызова? Честно говоря, я не могу достаточно точно ответить на этот вопрос, скажу лишь, что у меня возникали проблемы при работе с магией __get() в других модулях, но при переписывании кода «в лоб» никаких подобных проблем нет.

Может быть потому я часто встречал в статьях упреки в сторону магических методов PHP и советы избегать их использовать.

"All magic comes with a price." © Rumplestiltskin

На данном этапе основной функционал нашего реестра уже готов: мы можем создать единственный экземпляр реестра, добавлять объекты и обращаться к ним как обычными методами, так и через магические методы языка PHP. «А как же удаление?» — пока что эта функция нам не понадобится, да и я не уверен что в будущем что-то изменится. В конце концов, мы всегда можем добавить нужный функционал. Но если сейчас попробовать создать экземпляр нашего реестра,

$registry = Registry::singleton();

мы получим ошибку:

Fatal error : Class Registry contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (StorableObject::getClassName) in …

Все потому, что мы забыли написать обязательную функцию. Помните, я в самом начале говорил о функции, возвращающей имя модуля? Вот ее-то для полной работоспособности и осталось добавить. Она простая:

Public static function getClassName() { return self::$className; }

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

Public function getObjectsList() { //массив который будем возвращать $names = array(); //получаем имя каждого объекта из массива объектов foreach(self::$objects as $obj) { $names = $obj->getClassName(); } //дописываем в массив имя модуля регистра array_push($names, self::getClassName()); //и возвращаем return $names; }

Вот и все. На этом реестр закончен. Давайте проверим его работу? При проверке нам нужно будет что-то подключить — пусть будет файл конфигурации. Создайте новый файл core/config.php и добавьте в него минимальное содержимое, которое требует наш реестр:

//не забываем проверять константу if (!defined("_PLUGSECURE_")) { die("Прямой вызов модуля запрещен!"); } class Config { //имя модуля, читаемое private static $className = "Конфиг"; public static function getClassName() { return self::$className; } }

Как-то вот так. Теперь приступаем к самой проверке. В корне нашего проекта создаем файл index.php и пишем в него такой код:

Define("_PLUGSECURE_", true); //определили константу для защиты от прямого доступа к объектам require_once "/core/registry.php"; //подключили регистр $registry = Registry::singleton(); //создали экземпляр-синглтон регистра $registry->config = "/core/config.php"; //подключаем наш, пока бесполезный, конфиг //выводим имена подключенных модулей echo "Подключено"; foreach ($registry->

  • " . $names . "
  • "; }

    Или, если все же избегать магию, то 5-ю строку можно заменить на альтернативный метод:

    Define("_PLUGSECURE_", true); //определили константу для защиты от прямого доступа к объектам require_once "/core/registry.php"; //подключили регистр $registry = Registry::singleton(); //создали экземпляр-синглтон регистра $registry->addObject("config", "/core/config.php"); //подключаем наш, пока бесполезный, конфиг //выводим имена подключенных модулей echo "Подключено"; foreach ($registry->getObjectsList() as $names) { echo "

  • " . $names . "
  • "; }

    Теперь открываем браузер, и пишем в адресную строку адрес http://localhost/index.php или просто http://localhost/ (актуально, если вы используете стандартные настройки Open Server или аналогичного веб-сервера)

    В результате мы должны увидеть что-то подобное:

    Как видите — ошибок нет, а значит все работает, с чем я вас и поздравляю 🙂

    Сегодня на этом мы остановимся. В следующей статье мы вернемся к базе данных и напишем класс для работы с СУДБ MySQL, подключим в реестр и проверим работу на практике. До встречи!



    error: Контент защищен !!