Интересные разделы:

Шаблон Dependency Injection

Обнавлено: 04.02.2020

В этой статье подробно описан механизм работы патерна Dependency Injection

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

Задача

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

class AppointmentMaker
{
    public function makeAppointment()
    {
        $encoder = new BloggsApptEncoder();
        return $encoder->encode();
    }
}

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

$factory = new TerrainFactory(
    new EarthSea(),
    new EarthPlains,
    new EarthForest
);

Вызов класса TerrainFactory – это шаг в правильном направлении. Для получения экземпляра требуются универсальные типы Sea, Plains и Forest. Этот класс позволяет клиентскому коду выбирать реализацию для предоставления. Но как ты это делаешь?

Решение

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

class AppointmentMaker2
{ 
    private $encoder;
    public function __construct(ApptEncoder $encoder)
    {
        $this->encoder = $encoder;
    }
    
    public  function makeAppointment()
    {
        return $this->encoder->encode();
    }
}

Таким образом, мы достигаем желаемой гибкости благодаря тому, что класс AppointmentMaker2 возвращает элемент управления – объект типа BloggsApptEncoder 8 больше не создается. Но как работает логика создания объектов и где новые вдохновляющие операторы? Для выполнения этих задач вам понадобится коллекторный компонент. Как правило, файл конфигурации используется для этой цели, где определенные реализации определены для получения экземпляров. И хотя здесь можно использовать вспомогательные инструменты, эта книга посвящена тому, как самостоятельно выбраться из таких ситуаций. Поэтому сначала мы попытаемся создать очень простую реализацию, начиная с несложного файла конфигурации в формате XML, который описывает отношения между классами в разрабатываемой системе и конкретными типами данных, которые должны быть переданы вашим дизайнерам.

<object>
    <class name="TerrainFactory">
        <arg num="0" inst="EarthSea" />
        <arg num="1" inst="MarsPlains" />
        <arg num="2" inst="EarthForest" />
    </class>
    
    <class>
        <arg num="0" inst="BloggsApptEncoder"></arg>
    </class>
</object>

Здесь описаны два класса, TerrainFactory и AppointmentMaker2, упомянутые в этой главе. Вы должны получить экземпляр класса TerrainFactory, используя объекты типа EarthSea, MarsPlains и EarthForest, и чтобы объект типа BloggsApptEn-coder был передан в класс AppointmentMaker2. Ниже приведен очень простой пример класса коллекции, который считывает данные конфигурации и извлекает экземпляры объектов по запросу.

class ObjectAssemЫer
{
    private $components = [];

    public function __construct(string $conf)
    {
        $this->configure($conf);
    }

    private function configure(string $conf)
    {
        $data = simplexml_load_file($conf);
        foreach ($data->class as $class) {
            $args = [];
            $name = (string)$class['name'];
            foreach ($class->arg as $arg) {
                $argclass = (string)$arg['inst'];
                $args[(int)$arg['num']] = $argclass;
            }
            ksort($args);
            $this->components[$name] = function () use ($name, $args) {
                $expandedargs = [];
                foreach ($args as $arg) {
                    $expandedargs[] = new $arg();
                }
                $rclass = new \ReflectionClass($name);
                return $rclass->newinstanceArgs($expandedargs);
            };
        }
    }

    public function getComponent(string $class)
    {
        if (!isset($this->components[$class])) {
            throw new \Ехсерtiоn("Неизвестный компонент '$class'");
        }
        $ret = $this->components[$class] ();
        return $ret;
    }
}

Это очень просто, хотя на первый взгляд немного сложно понять реализацию, поэтому давайте кратко рассмотрим ее. Основное действие происходит в методе configure (), которому передается путь к файлу конфигурации, полученному от производителя. Он анализирует расширение sirnplexml для анализа файла конфигурации .xml. Если в реальном проекте требуется более глубокая обработка ошибок, в этом примере мы полностью доверяем содержимому проанализированного файла конфигурации XML. сначала полностью извлекается из каждого элемента разметки
конкретное имя класса, которое хранится в переменной $ name. Затем аргументы, соответствующие именам требуемых отдельных классов, извлекаются из подчиненных элементов разметки . Они хранятся в массиве $ args и сортируются по значению аргумента разметки xml num. Полученные данные собираются в анонимной функции, хранящейся в свойстве $ components. В этой функции получается экземпляр запрошенного класса и всех требуемых им объектов, и поэтому он вызывается только тогда, когда вызывается метод getComponent () с правильным именем класса. По этой причине довольно маленький фрагмент собранной информации может храниться в классе ObjectAssember. Обратите внимание на применение закрытия. В частности, анонимная функция обращается к переменным $ name и $ args, объявленным в области видимости при ее создании, с помощью ключевого слова use. Это, конечно, экспериментальный код. В дополнение к улучшенной обработке ошибок для ее надежной реализации необходимо будет учитывать, что сами объекты, включенные в компонент, могут требовать аргументов. Или вам может понадобиться решить проблемы с кэшированием. Например, вы должны получать экземпляр объекта каждый раз или только один раз?

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

$assemЬler = new ObjectAssemЬler("objects.xml"); 
$apptmaker = $assemЬler->getComponent("AppointmentMaker2"); 
$out = $apptmaker->makeAppointment(); 
print $out; 

С классом ObjectAssemЬler одной инструкции достаточно для получения экземпляра объекта. Класс AppointmentMaker2 теперь свободен от старой жестко заданной зависимости для экземпляра класса ApptEncoder. Кроме того, разработчик может использовать файл конфигурации, отслеживать классы, используемые во время теста, … и даже тестировать класс AppointmentMaker2 отдельно от остальной системы в более широком контексте.

Заключение

Мы рассмотрели две возможности создания объектов. С одной стороны, класс AppConfig послужил примером использования модели Service Locator, предоставляя возможность обнаруживать компоненты или службы от имени вашего клиента. Конечно, внедрение зависимости помогает писать более элегантный код. Итак, в классе AppointmentMaker2 ничего не известно о стратегиях создания объектов. Он просто выполняет свои обязанности, что, конечно, идеально подходит для любого класса. Мы стремимся разрабатывать классы, которые могут максимально сосредоточиться на своих обязанностях, отдельно от остальной системы. Но такая чистота не достигается бесплатно, потому что для этого необходим отдельный компонент коллектора для выполнения всех необходимых действий. Это можно считать «черным ящиком», доверяющим ему создавать объекты, как будто с помощью магии от нашего имени. И если эта магия работает правильно, то мы достаточно счастливы. Но в то же время «будет нелегко отладить неожиданное поведение такого черного ящика». С другой стороны, модель Service Locator является более простой, хотя она требует включения компонентов в остальную часть системы. Однако модель Service Locator не усложняет тестирование и не делает систему гибкой при правильном использовании. Идентификатор службы Service Locator может быть настроен для обслуживания произвольных компонентов в целях тестирования или в соответствии с указаниями. Но закодированный вызов идентификатора службы делает компонент зависимым от него. И поскольку этот вызов выполняется в теле метода, связь между клиентским кодом и целевым компонентом, предоставляемым моделью локатора службы, несколько скрыта. Ведь такое соотношение указано в подписи метода построения. Какой подход лучше выбрать? В какой-то степени это вопрос личных предпочтений. Лично я предпочитаю начинать с самого простого решения, постепенно усложняя его по мере необходимости. По этой причине я обычно выбираю модель Service Locator, создаю класс с использованием модели Registry в несколько строк кода и, следовательно, повышаю его гибкость в соответствии с конкретными требованиями. Компоненты, которые я разрабатываю, больше осведомлены друг о друге, чем хотелось бы, но, поскольку я редко переношу классы из одной системы в другую, я не особенно страдаю от эффекта внедрения. Например, после перемещения системного класса в отдельную библиотеку я не обнаружил трудностей в устранении зависимости, полученной из модели Service Locator. Модель Dependency Injection обеспечивает чистоту, но требует некоторого включения и заставляет вас полагаться на коллектор. Если вы используете фреймворк, в котором подобная функциональность уже предусмотрена, вам вряд ли стоит отказываться от его услуг. Например, Symfony DI предоставляет гибридное решение для поиска шаблонов услуг.