В предыдущей части этой серии, состоящей из двух частей, я приступил к разработке нескольких простых примеров, в которых достаточно доступным образом исследуется пара новых для PHP методологий, когда дело доходит до обработки зависимостей классов. В этом учебнике я рассмотрел включение в конструкторы греховных «новых» операторов, метод, который должен быть быстро брошен в мусорную корзину без каких-либо следов вины, а также использование инъекционных фабрик.
Хотя было бы справедливо признать, что фабрики действительно занимают определенную нишу в ряде особых случаев использования, я не настолько беспощаден, чтобы осуждать локаторов услуг и простую инъекцию зависимостей в несправедливую ссылку. В этой заключительной части мы подробнее рассмотрим реализацию этих популярных шаблонов, чтобы вы могли выбрать тот, который наилучшим образом соответствует вашим потребностям.
Средний Человек — Получение Коллабораторов Класса через Локатор Сервиса
В то время как сервисный локатор во многих случаях считается причудливым, умопомрачительным подходом в мире PHP, правда в том, что шаблон с некоторыми творческими наклонностями, конечно, прожил долгую и почтенную жизнь в области языка. По своей сути, Service Locator — это не что иное, как централизованный реестр, в большинстве случаев статичный (хотя в некоторых популярных средах появляются динамические), заполненный множеством объектов. Ни больше ни меньше.
Как обычно, один дидактический подход к пониманию того, что происходит под капотом Service Locator, является примером. Если мы хотим обратиться к достоинствам шаблона для предоставления объекту FileStorage
с прошлого раза его зависимости, локатор может быть реализован так:
<?php namespace LibraryDependencyInjection; interface ServiceLocatorInterface { public function set($name, $service); public function get($name); public function has($name); public function remove($name); public function clear(); }
<?php namespace LibraryDependencyInjection; class ServiceLocator implements ServiceLocatorInterface { protected $services = array(); public function set($name, $service) { if (!is_object($service)) { throw new InvalidArgumentException( "Only objects can be registered with the locator."); } if (!in_array($service, $this->services, true)) { $this->services[$name] = $service; } return $this; } public function get($name) { if (!isset($this->services[$name])) { throw new RuntimeException( "The service $name has not been registered with the locator."); } return $this->services[$name]; } public function has($name) { return isset($this->services[$name]); } public function remove($name) { if (isset($this->services[$name])) { unset($this->services[$name]); } return $this; } public function clear() { $this->services = array(); return $this; } }
с<?php namespace LibraryDependencyInjection; class ServiceLocator implements ServiceLocatorInterface { protected $services = array(); public function set($name, $service) { if (!is_object($service)) { throw new InvalidArgumentException( "Only objects can be registered with the locator."); } if (!in_array($service, $this->services, true)) { $this->services[$name] = $service; } return $this; } public function get($name) { if (!isset($this->services[$name])) { throw new RuntimeException( "The service $name has not been registered with the locator."); } return $this->services[$name]; } public function has($name) { return isset($this->services[$name]); } public function remove($name) { if (isset($this->services[$name])) { unset($this->services[$name]); } return $this; } public function clear() { $this->services = array(); return $this; } }
Примите мое мнение как форму катарсиса, если хотите, но я должен признаться, что я довольно неохотно использую локатор служб вместо простого внедрения зависимостей, даже если локатор динамический, а не статический реестр, страдающий от изменяемого глобального доступа проблемы. В любом случае стоит посмотреть и посмотреть, как его можно передать в класс FileStorage
. Вот так:
<?php namespace LibraryFile; use LibraryDependencyInjectionServiceLocatorInterface; class FileStorage { const DEFAULT_STORAGE_FILE = "data.dat"; protected $serializer; protected $file; public function __construct(ServiceLocatorInterface $locator, $file = self::DEFAULT_STORAGE_FILE) { $this->setFile($file); $this->serializer = $locator->get("serializer"); } public function setFile($file) { if (!is_file($file)) { throw new InvalidArgumentException( "The file $file does not exist."); } if (!is_readable($file) || !is_writable($file)) { if (!chmod($file, 0644)) { throw new InvalidArgumentException( "The file $file is not readable or writable."); } } $this->file = $file; return $this; } public function read() { try { return $this->serializer->unserialize( @file_get_contents($this->file)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } public function write($data) { try { return file_put_contents($this->file, $this->serializer->serialize($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } }
Чтобы прояснить ситуацию, я снова FileStorage
класс FileStorage
сверху вниз, поскольку это, вероятно, облегчает понимание того, как его логика управления остается нетронутой в отношении его методов read()
/ write()
. Конструктор, безусловно, является наиболее релевантным блоком, так как он использует локатор, который затем отвечает за получение объекта сериализатора.
Хотя его реализация прямолинейна, этот подход далек от того, чтобы быть невинным. Во-первых, FileStorage
теперь сильно зависит от локатора, даже когда можно обойти его различные реализации. Во-вторых, поскольку локатор по своей природе является промежуточным поставщиком зависимости класса, он также нарушает закон Деметры в некоторой точке. Это неизбежный артефакт, связанный с корнями шаблона. мы должны либо научиться жить с этой проблемой, либо просто забыть о модели в целом. Здесь нет середины, чтобы задуматься над этим.
Вот код, который показывает, как добиться успеха с помощью локатора:
<?php $locator = new ServiceLocator; $locator->set("serializer", new Serializer()); $fileStorage = new FileStorage($locator); $fileStorage->write("This is a sample string."); echo $fileStorage->read();
Хотя этот пример довольно примитивен, пример показывает, что локатор в какой-то момент напоминает структуру базового контейнера внедрения зависимостей (DIC). Основное отличие состоит в том, что локатор обычно вводится или статически потребляется внутри клиентских классов, в то время как DIC всегда живет и дышит вне их.
До сих пор мы рассмотрели приличное количество общих подходов, используемых для управления зависимостями классов. Тем не менее, мы не плавали в водах простейшего из всех … да, сладкий ручей сырых инъекций зависимости!
Величайший и самый простой финал — использование простого внедрения зависимостей
Я знаю, это может показаться очевидным, но самый эффективный и самый простой способ предоставить FileStorage
с объектом сериализатора — это простой метод Dependency Injection, что позволяет избежать любых проблем со связыванием или нарушить заповеди, налагаемые Законом Деметры. Конечно, я предполагаю, что вы достаточно умны и уже знали это с самого начала. Тем не менее, не мешало бы показать, как будет выглядеть рассматриваемый класс при подключении к этому подходу:
<?php namespace LibraryFile; class FileStorage { const DEFAULT_STORAGE_FILE = "data.dat"; protected $serializer; protected $file; public function __construct(Serializable $serializer, $file = self::DEFAULT_STORAGE_FILE) { $this->setFile($file); $this->serializer = $serializer; } // the remaining methods go here } $fileStorage = new FileStorage(new Serializer); $fileStorage->write("This is a sample string."); echo $fileStorage->read();
Это до смешного легко ассимилировать. В этом случае весь граф объектов настолько анемичен, что обращение к гайкам и болтам внешнего DIC для его создания было бы просто излишним. Однако от имени поучительной причины мы могли бы создать примитивный контейнер, похожий на быстрый и легкий Pimple
, и в один миг увидеть, как его использовать для подключения всех объектов, составляющих модуль хранения файлов:
<?php namespace LibraryDependencyInjection; interface ContainerInterface { public function set($name, $service); public function get($name, array $params = array()); public function has($name); public function remove($name); public function clear(); }
<?php namespace LibraryDependencyInjection; class Container implements ContainerInterface { protected $services = array(); public function set($name, $service) { if (!is_object($service)) { throw new InvalidArgumentException( "Only objects can be registered with the container."); } if (!in_array($service, $this->services, true)) { $this->services[$name] = $service; } return $this; } public function get($name, array $params = array()) { if (!isset($this->services[$name])) { throw new RuntimeException( "The service $name has not been registered with the container."); } $service = $this->services[$name]; return !$service instanceof Closure ? $service : call_user_func_array($service, $params); } public function has($name) { return isset($this->services[$name]); } public function remove($name) { if (isset($this->services[$name])) { unset($this->services[$name]); } return $this; } public function clear() { $this->services = array(); } }
с<?php namespace LibraryDependencyInjection; class Container implements ContainerInterface { protected $services = array(); public function set($name, $service) { if (!is_object($service)) { throw new InvalidArgumentException( "Only objects can be registered with the container."); } if (!in_array($service, $this->services, true)) { $this->services[$name] = $service; } return $this; } public function get($name, array $params = array()) { if (!isset($this->services[$name])) { throw new RuntimeException( "The service $name has not been registered with the container."); } $service = $this->services[$name]; return !$service instanceof Closure ? $service : call_user_func_array($service, $params); } public function has($name) { return isset($this->services[$name]); } public function remove($name) { if (isset($this->services[$name])) { unset($this->services[$name]); } return $this; } public function clear() { $this->services = array(); } }
Сходства между DIC и кодировщиком сервисов, ранее закодированным, не случайны. Первый, тем не менее, реализует ряд дополнительных функций, поскольку он способен хранить и вызывать замыкания по запросу, что близко имитирует силы, стоящие за Pimple.
С этим наивным DIC на месте весь граф объекта хранения файлов может быть собран по требованию:
<?php $container = new Container(); $container->set("filestorage", function() { return new FileStorage(new Serializer()); }); $fileStorage = $container->get("filestorage"); $fileStorage->write("This is a sample string."); echo $fileStorage->read();
Понятно, что DIC является (или теоретически таковым должен быть) элементом, выходящим за границы клиентских классов, которые совершенно не зависят от своего довольно скрытого и бесшумного существования. Эта форма неосведомленности, возможно, является одним из самых больших различий, которые существуют между DIC и локатором службы, даже если возможно внедрить DIC в другие классы также посредством своего рода «рекурсивного» внедрения.
На мой взгляд, этот процесс не только неоправданно снижает уровень DIC до уровня простого сервисного локатора, но и портит его естественное «постороннее» состояние. Как правило, независимо от того, используете ли вы DIC или локатор службы, убедитесь, что элементы будут играть ту роль, которую они должны играть, не наступая друг на друга.
Заключительные замечания
Кажется, что старые плохие времена, когда управление классовыми зависимостями было просто вопросом сброса нескольких «новых» операторов в жирные, раздутые конструкторы, наконец, исчезают. В отличие от этого, появляющаяся комбинация шаблонов, в которой Dependency Injection является лидером, активно продвигается к каждому углу PHP, смена парадигмы, которая уже оказала благотворное влияние на качество нескольких существующих кодовых баз.
Тем не менее, большой вопрос продолжает циркулировать по кругу: DIC, сервисные локаторы, вводимые фабрики … что в конечном итоге отвечает всем требованиям? Как я уже говорил, принятие правильного решения во многом зависит от того, с чем вы имеете дело в первую очередь. В любом случае, это всего лишь варианты Inversion of Control, украшенные приятными уточнениями и причудливыми сочетаниями. И вы знаете, что IoC — это путь полиморфизма, а значит, и тестируемости. Пусть ваши личные потребности будут вашими самыми уверенными советниками; они не разочаруют вас.
Изображение через SvetlanaR / Shutterstock