Передача зависимостей
Аргументы, или «зависимости» в терминологии DI, могут быть переданы классам следующими основными способами:
- passing by constructor
- passing by method (called a setter)
- by setting a property
- методом, аннотацией или атрибутом inject
Теперь мы проиллюстрируем различные варианты на конкретных примерах.
Внедрение через конструктор
Зависимости передаются в качестве аргументов конструктору при создании объекта:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Эта форма полезна для обязательных зависимостей, которые абсолютно необходимы классу для функционирования, так как без них экземпляр не может быть создан.
Начиная с версии PHP 8.0, мы можем использовать более короткую форму обозначения (constructor property promotion), которая функционально эквивалентна:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Начиная с версии PHP 8.1, свойство может быть помечено флагом
readonly
, который объявляет, что содержимое свойства не будет
изменяться:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
DI контейнер передает зависимости в конструктор автоматически, используя autowiring. Аргументы, которые нельзя передавать таким образом (например, строки, числа, булевы) записать в конфигурации.
Конструкторский ад
Термин ад конструктора относится к ситуации, когда дочерний класс наследуется от родительского класса, конструктор которого требует зависимостей, и дочерний класс тоже требует зависимостей. Он также должен принять и передать зависимости родительского класса:
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ CONSTRUCTOR HELL
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
Проблема возникает, когда мы хотим изменить конструктор класса
BaseClass
, например, когда добавляется новая зависимость. Тогда мы
должны изменить все конструкторы дочерних классов. Что превращает
такую модификацию в ад.
Как предотвратить это? Решение заключается в приоритете композиции над наследованием.
Поэтому давайте спроектируем код по-другому. Мы будем избегать абстрактных
классов Base*
. Вместо того чтобы MyClass
получал некоторую
функциональность, наследуя от BaseClass
, ему будет передаваться эта
функциональность как зависимость:
final class SomeFunctionality
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass
{
private SomeFunctionality $sf;
private Database $db;
public function __construct(SomeFunctionality $sf, Database $db) // ✅
{
$this->sf = $sf;
$this->db = $db;
}
}
Внедрение через сеттеры
Зависимости передаются путем вызова метода, который хранит их в
приватном свойстве. Обычно эти методы именуются set*()
, поэтому их
называют сеттерами, но, конечно, они могут называться и по-другому.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Этот метод полезен для необязательных зависимостей, которые не нужны для функционирования класса, поскольку не гарантируется, что объект действительно получит их (т. е. что пользователь вызовет метод).
В то же время, этот метод позволяет неоднократно вызывать сеттер для
изменения зависимости. Если это нежелательно, добавьте проверку в
метод, или, начиная с PHP 8.1, пометьте свойство $cache
флагом
readonly
.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if ($this->cache) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
The setter call is defined in the DI container configuration in section setup. Also here the automatic passing of dependencies is used by autowiring:
services:
- create: MyClass
setup:
- setCache
Внедрение через свойства
Зависимости передаются непосредственно в свойство:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Этот метод считается неприемлемым, поскольку свойство должно быть
объявлено как public
. Следовательно, мы не можем контролировать,
будет ли переданная зависимость действительно иметь указанный тип
(это было верно до версии PHP 7.4), и мы теряем возможность реагировать на
новую назначенную зависимость своим собственным кодом, например,
чтобы предотвратить последующие изменения. В то же время, свойство
становится частью публичного интерфейса класса, что может быть
нежелательно.
Настройка переменной определяется в конфигурации контейнера DI в разделе section setup:
services:
- create: MyClass
setup:
- $cache = @\Cache
Инъекция
В то время как предыдущие три метода в целом применимы во всех объектно-ориентированных языках, инъектирование методом, аннотацией или атрибутом inject специфично для презентаторов Nette. Они рассматриваются в отдельной главе.
Какой путь выбрать?
- конструктор подходит для обязательных зависимостей, которые необходимы классу для функционирования
- сеттер, с другой стороны, подходит для необязательных зависимостей, или зависимостей, которые могут быть изменены
- публичные переменные не рекомендуются