Transmiterea dependențelor
Argumentele, sau în terminologia DI „dependențele”, pot fi transmise claselor în următoarele moduri principale:
- transmitere prin constructor
- transmitere prin metodă (așa-numitul setter)
- setarea variabilei
- prin metodă, adnotare sau atribut inject
Acum vom arăta fiecare variantă cu exemple concrete.
Transmitere prin constructor
Dependențele sunt transmise în momentul creării obiectului ca argumente ale constructorului:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Această formă este potrivită pentru dependențele obligatorii, de care clasa are neapărat nevoie pentru funcționarea sa, deoarece fără ele instanța nu va putea fi creată.
Începând cu PHP 8.0 putem folosi o formă mai scurtă de notație (constructor property promotion), care este funcțional echivalentă:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Începând cu PHP 8.1, variabila poate fi marcată cu flag-ul readonly
, care declară că conținutul variabilei
nu se va mai schimba:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
Containerul DI transmite constructorului dependențele automat folosind autowiring. Argumentele care nu pot fi transmise astfel (de ex. șiruri, numere, booleeni) le scriem în configurație.
Constructor hell
Termenul constructor hell desemnează situația în care un descendent moștenește de la o clasă părinte al cărei constructor necesită dependențe, și în același timp descendentul necesită dependențe. În acest caz, trebuie să preia și să transmită și pe cele părintești:
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;
}
}
Problema apare în momentul în care dorim să schimbăm constructorul clasei BaseClass
, de exemplu când se
adaugă o nouă dependență. Atunci este necesar să modificăm și toți constructorii descendenților. Ceea ce face o astfel
de modificare un iad.
Cum să prevenim asta? Soluția este să preferăm compoziția în detrimentul moștenirii.
Deci vom proiecta codul altfel. Vom evita clasele abstracte Base*
.
În loc ca MyClass
să obțină o anumită funcționalitate prin moștenirea de la BaseClass
, își va
lăsa această funcționalitate să-i fie transmisă ca dependență:
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;
}
}
Transmitere prin setter
Dependențele sunt transmise prin apelarea unei metode care le stochează într-o variabilă privată. Convenția obișnuită
de denumire a acestor metode este forma set*()
, de aceea li se spune setteri, dar pot fi, desigur, numite oricum
altfel.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Acest mod este potrivit pentru dependențele opționale, care nu sunt necesare pentru funcționarea clasei, deoarece nu este garantat că obiectul va primi efectiv dependența (adică că utilizatorul va apela metoda).
În același timp, acest mod permite apelarea repetată a setterului și astfel modificarea dependenței. Dacă acest lucru nu
este dorit, adăugăm o verificare în metodă sau, începând cu PHP 8.1, marcăm proprietatea $cache
cu flag-ul
readonly
.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
Apelarea setterului o definim în configurația containerului DI în cheia setup. Și aici se utilizează transmiterea automată a dependențelor prin autowiring:
services:
- create: MyClass
setup:
- setCache
Setarea variabilei
Dependențele sunt transmise prin scrierea directă în variabila membru:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Acest mod este considerat nepotrivit, deoarece variabila membru trebuie declarată ca public
. Și, prin urmare, nu
avem control asupra faptului că dependența transmisă va fi într-adevăr de tipul dat (valabil înainte de PHP 7.4) și pierdem
posibilitatea de a reacționa la dependența nou atribuită cu cod propriu, de exemplu, pentru a preveni modificarea ulterioară.
În același timp, variabila devine parte a interfeței publice a clasei, ceea ce poate să nu fie de dorit.
Setarea variabilei o definim în configurația containerului DI în secțiunea setup:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
În timp ce cele trei moduri anterioare sunt valabile în general în toate limbajele orientate pe obiecte, injectarea prin metodă, adnotare sau atribut inject este specifică exclusiv presenterilor din Nette. Despre acestea se discută într-un capitol separat.
Ce mod să alegem?
- constructorul este potrivit pentru dependențele obligatorii, de care clasa are neapărat nevoie pentru funcționarea sa
- setterul este, dimpotrivă, potrivit pentru dependențele opționale sau dependențele care pot fi modificate ulterior
- variabilele publice nu sunt potrivite