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
različica: 3.x