Внедрение зависимостей

Внедрение зависимостей (Dependency injection, или DI) — это принцип настройки объекта, при котором поля объекта задаются внешним объектом (сущностью) в противопоставление самонастройке объектов.

Разрешение зависимостей внутри класса

Рассмотрим пример:

class PersonData {

    protected DataSource $dataSource;

    public readData(int $primaryKey) {
        $this->dataSource = 
            new DataSourceImplemenation("driver", "host", "user", "password");
        // Далее получаем данные из источника
        ...
    }

}

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

Внедряем зависимости через конструктор

Теперь немного изменим код:

class PersonData {

    protected DataSource $dataSource;

    public function __contruct($driver, $host, $user, $password) {
        $this->dataSource = 
            new DataSourceImplemenation($driver, $host, $user, $password);
    }

    public readData(int $primaryKey) {
        // Получаем данные из источника
        ...
    }

}

Класс уже не разрешает зависимости. Эту задачу теперь выполняет вызывающий код, который передаст нужные параметры. Зависимости внедряются через конструктор. Это и есть внедрение зависимостей в простейшем виде. Теперь уже можно сменить источник без изменения класса PersonData. Внедрять зависимости конечно можно не только через конструктор, но и через методы-сеттеры, например.

Избавляемся от лишних зависимостей

Но класс PersonData все еще зависит от класса DataSourceImplemenation и от интерфейса DataSource. Хотя ему нужно знать только о последнем. Вместо внедрения четырех параметров мы можем внедрить только лишь DataSource.

class PersonData {

    protected DataSource $dataSource;

    public function __contruct(DataSource $dataSource) {
        $this->dataSource = $dataSource;
    }

    public readData(int $primaryKey) {
        // Получаем данные из источника
        ...
    }

}

Теперь мы может передавать в PersonData любую реализацию DataSource и наш класс не зависит от DataSourceImplemenation и 4 строковых параметров.

Цепное внедрение зависимостей

Допустим наш класс PersonData используется классом PersonReport

class PersonReport {

    public function getReport(Person $person) {
        $personData = 
            new PersonData(new DataSourceImplemenation("driver", "host", "user", "password"));
        $data = $personData->readData($person.getId());
        // Далее процесс отображения данных
        ...
    }
}

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

Чтобы справиться с этими проблемами необходимо продолжить внедрение зависимостей по всем слоям абстракции.

class PersonReport {

    protected PersonData $personData;

    public function __constructor(PersonData $personData) {
        $this->personData = $personData;
    }

    public function getReport(Person $person) {
        $data = $this->personData->readData($person.getId());
        // Далее процесс отображения данных
        ...
    }
}

Такой паттерн внедрения должен идти через все слои.

Контейнер Dependency Injection Container

В реальной системе есть множество компонентов, которые имеют свои зависимости. И нам требуется сущность, которая бы знала какие зависимости внедрять в каждый класс. Она будет представлять собой нечто вроде фабрики, но с возможностью дальнейшего управления жизненным циклом объектов, которых он создал. Например, для освобождения ресурсов после использования. Такую сущность называют контейнером (Dependency Injection Container или DIC)

Существуют различные реализации таких контейнеров. Например, в Laravel имеется свой контейнер.

Выгоды от использования DI и DIC

Благодаря использованию DI и DIC мы получаем следующие выгоды

  • Меньше зависимостей у отдельных компонентов. А чем меньше зависимостей, тем меньше возникает необходимость изменений компонента
  • Меньше перенос зависимостей, и в связи с этим меньше кода
  • Код удобнее читать, так как зависимости перенесены в интерфейс компонента. Нет нужды просматривать весь код компонента
  • Проще тестировать код, так как легко можно подменять реализацию на mock-объекты и компоненты меньше связаны
  • Проще переиспользовать компонент в другом контексте

Как работает контейнер в Laravel

В Laravel функцию контейнера Dependency Injection выполняет Service Container. Соответствующий код можно посмотреть в файле vendor/laravel/framework/src/Illuminate/Container/Container.php в методе build().

Для определения зависимостей используются подсказки типов (type hints) в коде. Хорошо описывается работа данного кода в статье Dependency Injection in Laravel на Medium. Если кратко, то, используя Reflection API в PHP, контейнер изучает какие параметры каких типов (классов) передаются в конструктор и так определяет какие зависимости требуются для создания экземпляра конкретного класса.

Полезные ссылки

Leave a Reply

Ваш адрес email не будет опубликован.