Передача залежностей
Аргументи, або в термінології DI «залежності», можна передавати в класи такими основними способами:
- передача конструктором
- передача методом (так званим сеттером)
- встановленням змінної
- методом, анотацією чи атрибутом 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. Аргументи, які неможливо передати таким чином (наприклад, рядки, числа, логічні значення), ми записуємо в конфігурації.
Пекло конструкторів
Термін constructor hell (пекло конструкторів) позначає ситуацію, коли нащадок успадковує від батьківського класу, конструктор якого вимагає залежностей, і водночас нащадок також вимагає залежностей. При цьому він повинен прийняти та передати також батьківські:
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ ПЕКЛО КОНСТРУКТОРІВ
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 (isset($this->cache)) {
throw new RuntimeException('Залежність вже встановлена');
}
$this->cache = $cache;
}
}
Виклик сеттера ми визначаємо в конфігурації DI-контейнера в ключі setup. Тут також використовується автоматична передача залежностей за допомогою autowiring:
services:
- create: MyClass
setup:
- setCache
Встановленням змінної
Залежності передаються записом безпосередньо в змінну-член:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Цей спосіб вважається недоречним, оскільки змінна-член повинна бути
оголошена як public
. А отже, ми не маємо контролю над тим, що
передана залежність буде дійсно зазначеного типу (це було актуально до
PHP 7.4), і втрачаємо можливість реагувати на новопризначену залежність
власним кодом, наприклад, запобігти подальшій зміні. Водночас змінна
стає частиною публічного інтерфейсу класу, що може бути небажаним.
Встановлення змінної ми визначаємо в конфігурації DI-контейнера в секції setup:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
Хоча попередні три способи застосовуються загалом у всіх об'єктно-орієнтованих мовах, ін'єкція методом, анотацією чи атрибутом inject є специфічною виключно для презентерів у Nette. Про них йдеться в окремому розділі.
Який спосіб обрати?
- конструктор підходить для обов'язкових залежностей, які клас неодмінно потребує для своєї роботи
- сеттер, навпаки, підходить для необов'язкових залежностей або залежностей, які можна буде змінювати надалі
- публічні змінні не підходять