Predajanje odvisnosti
Argumente ali v terminologiji DI „odvisnosti“ lahko v razrede predajamo na naslednje glavne načine:
- predajanje s konstruktorjem
- predajanje z metodo (t.i. setterjem)
- nastavitev spremenljivke
- z metodo, anotacijo ali atributom inject
Zdaj si bomo posamezne variante pokazali na konkretnih primerih.
Predajanje s konstruktorjem
Odvisnosti se predajajo v trenutku ustvarjanja objekta kot argumenti konstruktorja:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Ta oblika je primerna za obvezne odvisnosti, ki jih razred nujno potrebuje za svoje delovanje, saj brez njih instance ne bo mogoče ustvariti.
Od PHP 8.0 lahko uporabimo krajšo obliko zapisa (constructor property promotion), ki je funkcionalno ekvivalentna:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Od PHP 8.1 lahko spremenljivko označimo z zastavico readonly
, ki deklarira, da se vsebina spremenljivke ne bo
več spremenila:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
DI vsebnik preda konstruktorju odvisnosti samodejno s pomočjo autowiringa. Argumente, ki jih na ta način ni mogoče predati (npr. nizi, števila, booleani) zapišemo v konfiguraciji.
Constructor hell
Izraz constructor hell označuje situacijo, ko potomec deduje od starševskega razreda, katerega konstruktor zahteva odvisnosti, in hkrati potomec zahteva odvisnosti. Pri tem mora prevzeti in predati tudi starševske:
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;
}
}
Težava nastane v trenutku, ko bomo želeli spremeniti konstruktor razreda BaseClass
, na primer ko se doda nova
odvisnost. Potem je namreč treba prilagoditi tudi vse konstruktorje potomcev. Kar iz takšne prilagoditve naredi pekel.
Kako temu preprečiti? Rešitev je dajati prednost kompoziciji pred dedovanjem.
Torej bomo kodo zasnovali drugače. Izogibali se bomo abstraktnim
Base*
razredom. Namesto da bi MyClass
pridobival določeno funkcionalnost s tem, da deduje od
BaseClass
, si bo to funkcionalnost pustil predati kot odvisnost:
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;
}
}
Predajanje s setterjem
Odvisnosti se predajajo s klicem metode, ki jih shrani v zasebno spremenljivko. Običajna konvencija poimenovanja teh metod
je oblika set*()
, zato se jim reče setterji, vendar se lahko seveda imenujejo kakorkoli drugače.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Ta način je primeren za neobvezne odvisnosti, ki niso nujne za delovanje razreda, saj ni zagotovljeno, da bo objekt odvisnost dejansko prejel (tj. da bo uporabnik metodo poklical).
Hkrati ta način dopušča ponavljajoče klicanje setterja in s tem spreminjanje odvisnosti. Če to ni zaželeno, dodamo
v metodo preverjanje ali od PHP 8.1 označimo lastnost $cache
z zastavico 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;
}
}
Klic setterja definiramo v konfiguraciji DI vsebnika v ključu setup. Tudi tukaj se uporablja samodejno predajanje odvisnosti s pomočjo autowiringa:
services:
- create: MyClass
setup:
- setCache
Nastavitev spremenljivke
Odvisnosti se predajajo z zapisom neposredno v člansko spremenljivko:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Ta način se šteje za neprimernega, ker mora biti članska spremenljivka deklarirana kot public
. In zato nimamo
nadzora nad tem, da bo predana odvisnost dejansko danega tipa (veljalo pred PHP 7.4) in izgubimo možnost reagirati na novo
dodeljeno odvisnost z lastno kodo, na primer preprečiti nadaljnjo spremembo. Hkrati spremenljivka postane del javnega vmesnika
razreda, kar morda ni zaželeno.
Nastavitev spremenljivke definiramo v konfiguraciji DI vsebnika v sekciji setup:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
Medtem ko prejšnji trije načini veljajo na splošno v vseh objektno usmerjenih jezikih, je vbrizgavanje z metodo, anotacijo ali atributom inject specifično izključno za presenterje v Nette. O njih govori samostojno poglavje.
Kateri način izbrati?
- konstruktor je primeren za obvezne odvisnosti, ki jih razred nujno potrebuje za svoje delovanje
- setter je nasprotno primeren za neobvezne odvisnosti ali odvisnosti, ki jih je mogoče še naprej spreminjati
- javne spremenljivke niso primerne