Замаскированные зависимости

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

<?php
class Something
{
    private $collaborator;

    public function __construct( Collaborator $collaborator )
    {
        $this->collaborator = $collaborator;
    }
    
    // ...
}

Этим мы делегируем задачу создания сотрудника куда-нибудь в другое место. А значит, нам не придётся озаботиться созданием сотрудничающего объекта или его зависимостей.

Таким образом, мы можем переместить почти всё создание объекта в одно место: в фабрику. Существуют исключения, например, когда вы не хотите, или вам вовсе не нужно, чтобы фабрика создавала объект. Как правило, объекты-значения и доменные объекты создаются на месте.

Теперь у нас есть фабрика, которая может создавать все наши объекты. Для создания каждого объекта у неё будет по одному методу:

<?php
class Factory
{
    public function createSomething()
    {
        return new Something( $this->createCollaborator() );
    }
    
    private function createCollaborator()
    {
        return new Collaborator;
    }
    
    // ...
}

Чтобы создать объект, фабрике необходимо предоставить свои зависимости. В приведённом выше примере объект Collaborator подаётся тогда, когда создаётся объект Something. Это работает действительно хорошо, когда существует одна фабрика. Но если вы создаёте множество фабрик, вам придётся иметь дело с фабриками, зависящими от других фабрик, потому что одним нужны другие для создания объектов. Это становится очень сложно и запутанно, поэтому не делайте так. Создайте одну фабрику. (Есть способы разбивки фабрик без указанных недостатков, но это выходит за рамки данной статьи.)

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

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

<?php
class HttpPostRequestRouter
{
    public function route( HttpPostRequest $request )
    {
        switch ($request->getParameter( 'command' )) {
            case 'createAccount':
                return new CreateAccountCommandHandler( /* ... */ );
        }

        // ...
    }
}

Этот пример упрощён, но он иллюстрирует тезис. Но подождите: мы не можем создать обработчик команды здесь, потому что тогда нам придётся иметь дело с его зависимостями, которые могли бы включать AccountRepository, например. Значит, мы должны внедрить фабрику?

<?php
class HttpPostRequestRouter
{
    private $factory;
    
    public function __construct( Factory $factory )
    {
        $this->factory = $factory;
    }

    public function route( HttpPostRequest $request )
    {
        switch ($request->getParameter( 'command' )) {
            case 'createAccount':
                return $this->factory->createCreateAccountCommandHandler(
                    /* ... */
                );
        }

        // ...
    }
}

Сделав так, мы делегировали бы ответственность создания CreateAccountCommandHandler и нам бы не пришлось больше иметь дел с его зависимостями. Но раздача фабрики считается плохой практикой. Причина проста: когда имеется доступ к фабрике, можно создать любой объект. Слишком много власти для одного разработчика. У вас нет доступа к базе данных? Ну, просто имейте фабрику, которая создаёт другое соединение для вас и всё.
Но это ещё хуже. Вместо того чтобы раздать фабрику, многие разработчики раздают service locator, который выглядит примерно так:

<?php
class ServiceLocator
{
    private $factory;
    
    public function __construct( Factory $factory )
    {
        $this->factory = $factory;
    }
    
    public function getService( $identifier )
    {
        switch ( $identifier )  {
            case 'createAccountCommandHandler':
                return $this->factory->createCreateAccountCommandHandler();

            // ...
        }
    }
}

Здесь неявный API. Для выборки «службы», вы должны передать строку. Метод getService() не имеет вменяемый тип возврата (аннотацию), потому что может возвращать произвольные объекты (ибо они были объявлены как службы). Это означает отсутствие автоматического завершения в IDE, если, конечно, ваша IDE не делает серьёзное волшебство за кулисами.

Было бы гораздо лучше сделать API явным и иметь несколько методов, каждый из которых имел бы только один явный тип возврата:

<?php
class ServiceLocator
{
    private $factory;
    
    public function __construct( Factory $factory )
    {
        $this->factory = $factory;
    }

    public function getCreateAccountCommandHandler()
    {
        return $this->factory->createCreateAccountCommandHandler();
    }
    
    // ...
}

Так мы избегаем уродливого длинного switch case и помогаем IDE предложить автозавершение, потому что ясно, что будет возвращено. Негативная сторона: теперь существует много методов. Ну и публичное API большое. Вот как бы мы использовали локатор:

<?php
class HttpPostRequestRouter
{
    private $serviceLocator;
    
    public function __construct( ServiceLocator $serviceLocator )
    {
        $this->serviceLocator = $serviceLocator;
    }

    public function route( HttpPostRequest $request )
    {
        switch ($request->getParameter( 'command' ))  {
            case 'createAccount':
                return $this->serviceLocator
                            ->getCreateAccountCommandHandler(
                                /* ... */
                            );
        }

        // ...
    }
}

Service locator абстрагирует наш маршрутизатор от создания объекта, чем занимается фабрика. Но это решение до сих пор страдает от той же проблемы: API слишком большое. Вы можете определить (и, таким образом, создать) любую службу. И это слишком простой способ сделать почти любой объект службой.

Несомненно, выбор обработчика команды должен быть решением во время выполнения маршрутизатора. Но API локатора службы является слишком большим, чтобы передать его экземпляр. Оно также нарушает Принцип разделения интерфейса («I» в SOLID), потому что вынуждает маршрутизатор зависеть от довольного многих неиспользуемых методов API.

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

Но почему мы должны создать только один локатор службы? Меньшее, что можно было бы сделать для маршрутизатора:

<?php
class HttpPostRequestRouter
{
    private $commandHandlerLocator;
    
    public function __construct( CommandHandlerLocator $commandHandlerLocator )
    {
        $this->commandHandlerLocator = $commandHandlerLocator;
    }

    public function route( HttpPostRequest $request )
    {
        return $this->commandHandlerLocator->locateCommandHandlerFor(
            $request->getParameter( 'command' )
        );
    }
}

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

Давайте посмотрим на другой пример. Допустим, где-то мы должны выбрать представление:

<?php
class Something
{
    private $viewLocator;
    
    public function __construct( ViewLocator $viewLocator )
    {
        $this->viewLocator = $viewLocator;
    }

    public function doWork( )
    {
        // ...
        
        $this->viewLocator->locateViewFor( $result );

        // ...
    }
}

Для целей этого примера опустим то, каков $result. Это может быть в результате объект, передаваемый обратно из обработчика команд.
Всё работает, потому что мы строго отделяем проблемы, следуя принципу единственной ответственности («S» в SOLID): локатор выбирает объекты, а фабрика создаёт их. Мы можем разделить один большой локатор службы, получив меньшие (и безопасные) локаторы. Но мы не можем сделать этого с самой фабрикой, потому что получили бы проблему зависимостей типа фабрика-требующая-другой-фабрики.

Всё это сводится к вопросу, когда по-вашему объекты нужно создавать.

Примечание

Это авторский перевод статьи «thePHP.cc — Dependencies in Disguise».

Комментарии

  1. Пётр пишет:

    Не очень удачная попытка изобрести Service locator. Ну и принципы DI в общем-то не соблюдаются.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *