Статьи

Управление зависимостями классов: введение в внедрение зависимостей, локаторы служб и фабрики, часть 1

Посмотрим правде в глаза: хорошо это или плохо, ООП активно пробуривает глубокие дыры в почве PHP в последние несколько лет, поэтому его повсеместное присутствие сейчас совсем не новость. Кроме того, хотя эта неуклонная постепенная «объективация» в местности языка обычно считается выгодным сдвигом в сторону новых и более перспективных горизонтов, для тех, кто все еще укоренен в процессуальной парадигме, процесс был далеко не безобидным.

Есть несколько веских аргументов в поддержку этого мышления: во-первых, природа ООП неуклюжа и по своей сути сложна, изобилует нюансами, которые могут приручить буквально годы. Кроме того, определение API-интерфейсов, которые OO-компоненты используют для взаимодействия друг с другом, иногда может быть довольно обременительным, особенно когда речь идет о разработке систем, основа которых опирается на средства, предоставляемые большими и запутанными графами объектов .

С точки зрения программиста, процесс проектирования легко потребляемых API-интерфейсов при сохранении приличного уровня развязки между участвующими классами всегда является сложным компромиссом, поскольку чем более выразительны зависимости, тем сложнее работа, которую необходимо выполнить. за кулисами для подключения сотрудников в их правильной последовательности. Этот факт сам по себе является дилеммой, которая давным-давно преследует умы разработчиков, и со временем, конечно, становится все труднее переваривать. Поскольку приложения становятся все более раздутыми, размещая в своих границах все большее число классов, создание чистых декларативных API-интерфейсов уже не просто бесполезная практика, которая со временем может время от времени внедряться в основной язык. Это просто необходимость.

В связи с этим возникает несколько интересных вопросов: как обеспечить класс своими зависимостями, не загромождая его API или, что еще хуже, не связывая его с самими зависимостями? Просто, обращаясь к преимуществам инъекции зависимости ? Через правления введенного Сервисного Локатора ?

По понятным причинам не существует единого подхода, который бы подходил для решения всей проблемы, даже если у всех вышеупомянутых подходов есть что-то общее. Да, в некоторой степени все они используют какую-то форму Inversion of Control, которая не только позволяет отделить зависимости от класса, но также поддерживает аккуратный уровень изоляции между интерфейсами зависимостей и реализацией, делая проверку / тестирование легкий ветерок.

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

(Надеюсь) Исчезающая Чума — «новые» операторы в конструкторах

Мантра, конечно, несколько старая, но ее утверждение все еще звучит громко и ясно: «помещать« новые »операторы в конструкторы — просто зло». Все мы знаем это сейчас, и даже принимаем как должное, что мы никогда не совершим такого смертный грех Но это был буквально метод по умолчанию, который в течение многих лет использовался классом для поиска зависимостей до того, как Dependency Injection достигло мира PHP. Для полноты картины давайте воссоздадим этот неуклюжий старомодный сценарий и напомним себе, почему мы должны избавиться от такой вредной чумы.

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

<?php namespace LibraryEncoder; class Serializer implements Serializable { public function serialize($data) { if (is_resource($data)) { throw new InvalidArgumentException( "PHP resources are not serializable."); } if (($data = serialize($data)) === false) { throw new RuntimeException( "Unable to serialize the supplied data."); } return $data; } public function unserialize($data) { if (!is_string($data)|| empty($data)) { throw new InvalidArgumentException( "The data to be unserialized must be a non-empty string."); } if (($data = @unserialize($data)) === false) { throw new RuntimeException("Unable to unserialize the supplied data."); } return $data; } } 

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

 <?php namespace LibraryFile; use LibraryEncoderSerializer; class FileStorage { const DEFAULT_STORAGE_FILE = "data.dat"; protected $serializer; protected $file; public function __construct($file = self::DEFAULT_STORAGE_FILE) { $this->setFile($file); $this->serializer = new 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 сохранению и FileStorage данных из предварительно определенного целевого файла. Но, казалось бы, доброжелательная природа класса — не что иное, как иллюзия, поскольку он явно создает экземпляр сериализатора в своем конструкторе!

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

 <?php $fileStorage = new FileStorage(); $fileStorage->write("This is a sample string."); echo $fileStorage->read(); 

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

Есть несколько дополнительных подходов, которые стоит изучить, которые могут улучшить взаимодействие двух предыдущих классов друг с другом. Например, мы могли бы быть немного смелее и внедрить фабрику прямо во внутренние компоненты FileStorage и позволить ей при необходимости создать объект сериализатора, таким образом делая зависимость класса немного более явной.

Отсрочка создания зависимостей через введенную фабрику

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

Если вы любопытный тип, как я, вам будет интересно, как использовать преимущества, предоставляемые фабрикой, для улучшения того, как более FileStorage класс FileStorage ищет своего соавтора, избавляясь от печально известного оператора «new» в своем конструкторе. На самом базовом уровне процесс поиска можно переформулировать с помощью следующего фабричного класса:

 <?php namespace LibraryDependencyInjection; interface SerializerFactoryInterface { public function getSerializer(); } 
 <?php namespace LibraryDependencyInjection; use LibraryEncoderSerializer; class SerializerFactory implements SerializerFactoryInterface { public function getSerializer() [ static $serializer; if ($serializer === null) { $serializer = new Serializer; } return $serializer; } } 

Имея под рукой динамическую фабрику, на которую возложена задача создания сериализатора по требованию, теперь класс FileStorage можно реорганизовать для принятия фабричного разработчика:

 <?php namespace LibraryFile; use LibraryDependencyInjectionSerializerFactoryInterface; class FileStorage { const DEFAULT_STORAGE_FILE = "data.dat"; protected $factory; protected $file; public function __construct(SerializerFactoryInterface $factory, $file = self::DEFAULT_STORAGE_FILE) { $this->setFile($file); $this->factory = $factory; } 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->factory->getSerializer()->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->factory->getSerializer()->serialize($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } } 

Функциональность FileStorage остается практически такой же, но тесная связь с сериализатором была нарушена путем внедрения реализации SerializerFactoryInterface . Этот простой поворот превращает класс в гибкое и тестируемое существо, которое предоставляет более выразительный API, поскольку теперь из внешнего мира легче увидеть, какие зависимости ему нужны. Код ниже показывает результат этих улучшений с точки зрения клиентского кода:

 <?php $fileStorage = new FileStorage(new SerializerFactory()); $fileStorage->write("This is a sample string."); echo $fileStorage->read(); 

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

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

Заключительные замечания

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

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

Изображение через SvetlanaR / Shutterstock