Внедрение зависимостей (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, контейнер изучает какие параметры каких типов (классов) передаются в конструктор и так определяет какие зависимости требуются для создания экземпляра конкретного класса.