Предаване на зависимости
Аргументите, или в терминологията на 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;
// ⛔ 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 (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. За тях се разказва в отделна глава.
Кой метод да изберем?
- конструкторът е подходящ за задължителни зависимости, от които класът непременно се нуждае за своята функция
- сетърът, напротив, е подходящ за незадължителни зависимости или зависимости, които може да се наложи да се променят по-нататък
- публичните променливи не са подходящи