DI a předávání závislostí

před 12 lety od David Grudl  

Víte, že Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída otevřeně hlásí ke svým závislostem, místo toho, aby je někde pokoutně získávala. Otázka zní, jak se k nim hlásit a jak je předávat.

K předávání závislostí můžeme využít konstruktor:

class Foobar
{
	private $httpRequest, $router, $session;

	function __construct(HttpRequest $httpRequest, Router $router, Session $session)
	{
		$this->httpRequest = $httpRequest;
		$this->router = $router;
		$this->session = $session;
	}

}

$foobar = new Foobar($hr, $router, $session);

Nebo metody:

class Foobar
{
	private $httpRequest, $router, $session;

	function setHttpRequest(HttpRequest $httpRequest)
	{
		$this->httpRequest = $httpRequest;
	}

	function setRouter(Router $router)
	{
		$this->router = $router;
	}

	function setSession(Session $session)
	{
		$this->session = $session;
	}
}

$foobar = new Foobar;
$foobar->setSession($session);
$foobar->setHttpRequest($hr);
$foobar->setRouter($router);

Nebo přímo naplnit jednotlivé proměnné:

class Foobar
{
	/** @var HttpRequest */
	public $httpRequest;

	/** @var Router */
	public $router;

	/** @var Session */
	public $session;
}

$foobar = new Foobar;
$foobar->session = $session;
$foobar->httpRequest = $hr;
$foobar->router = $router;

Které řešení je nejlepší? Aby článek nebyl neúměrně dlouhý, odkážu se na Předávání závislostí od Vaška Purcharta, tak si jej přečtěte, protože budu navazovat tam, kde končí.

Takže znovu: které řešení je nejlepší? Kdyby byly ekvivalentní, bylo by nejspíš to poslední, protože kód třídy je nejkratší a kratší kód minimalizuje možnost vzniku chyby a šetří čas při psaní i čtení. Nicméně řešení ekvivalentní nejsou. Jsou spíše diametrálně odlišené.

Immutability tedy neměnnost

Immutable object je objekt, který nemění svůj stav od chvíle, co byl vytvořen. Nevěřili byste, kolik problémů objektového návrhu se dá vyřešit jen tím, že se objekty stanou neměnné. Ale to je téma na jiný článek.

Prakticky vždy budeme chtít, aby závislosti objektu byly neměnné. A v tomto směru se jednotlivé varianty předávání liší. Veřejné (public) proměnné můžeme změnit kdykoliv a změnu nelze detekovat, což je zcela diskvalifikuje ze hry a dále už s touto variantou nebudu vůbec počítat. A to ani nemluvím o chybějící typové kontrole. (Viz také úvaha nad tím, jak by se dalo property injection řešit.)

Mohlo by vás napadnout nahradit public za private a vložit do nich závislosti některým z nízkoúrovňových triků (třeba pomocí reflexe), ale takové obcházení vlastností jazyka do obecných úvah o DI nepatří. Privátní proměnné nejsou součástí veřejného API třídy a nelze se jimi hlásit k závislostem. A také nehackujme jazyk, dokud to není nutné.

Neměnnost bychom si u metod mohli zajistit sami:

	function setRouter(Router $router)
	{
		if ($this->router) {
			throw new InvalidStateException('Router has already been set.');
		}
		$this->router = $router;
	}

A protože metoda není klasický obecný setter, tj. lze ji volat jen jednou, nelze očekávat existenci getteru a můžeme její volání považovat za povinné, mohla by používat jiné názvosloví. Například prefix inject, v tomto případě injectRouter().

Vytvořili bychom tedy pro větší srozumitelnost konvenci, že závislosti předáváme metodami inject.

(Musím zdůraznit, že se bavíme o konvenci užitečné pro programátora, o žádných DI kontejnerech v článku nepadlo ani slovo. Pochopitelně by se jí dalo s úspěchem využít i v kontejnerech, nicméně je naprosto zásadní uvědomit si, co je příčinou a co důsledkem.)

Používání metod pro injektáž má svá úskalí:

  • musíme sami zajistit neměnnost
  • špatně se odhaluje okamžik, kdy jsou nastaveny všechny závislosti, abychom provedli inicializaci objektu
  • měli bychom také ověřovat, že se některé závislosti neopomněly nastavit
  • režijní kód bude poměrně ukecaný a dlouhý

Všechny tyto nedostatky řeší už z principu injektáž přes konstruktor, proto vychází jako nejvhodnější.

(…Tedy, ehm, neřeší… Ale k tomu se hnedle dostaneme.)

Constructor hell

Nenápadný problém předávání závislostí přes konstruktor tkví v tom, že nemáme žádné vodítko, v jakém pořadí jsou parametry uvedeny. Napadá mě snad leda řadit je abecedně (divné, co?). Pokud by dvě závislosti byly stejného typu, potom v pořadí source, destination apod.

Byť nám s tímto problémem může pomoci napovídání v IDE nebo automaticky generované kontejnery, nic to nemění na tom, že metoda s nejasnými parametry snižuje srozumitelnost kódu.

Jakožto líny člověk neoblibuji ani ty strojově se opakující přiřazování v těle konstruktoru. Jako zkratku lze použít:

class Foobar
{
	private $httpRequest, $router, $session;

	function __construct(HttpRequest $httpRequest, Router $router, Session $session)
	{
		list($this->httpRequest, $this->router, $this->session) = func_get_args();
	}

}

Ale pokud by byla poslední závislost nepovinná, mohlo by to skončit u Notice: Undefined offset.

Uvažuji nad sepsáním RFC pro PHP, aby bylo možné používat zápis:

class Foobar
{
	private $httpRequest, $router, $session;

	function __construct(HttpRequest $this->httpRequest, Router $this->router, Session $this->session)
	{
	}

}

Nicméně tohle jsou jen syntaktické libůstky oproti kruciálnímu problému s dědičností.

Co se stane, když vytvoříme potomka:

class Barbar extends Foobar
{
	private $logger;

	function __construct(HttpRequest $httpRequest, Router $router, Session $session, Logger $logger)
	{
		parent::__construct($httpRequest, $router, $session);
		$this->logger = $logger;
	}

}

Jak vidno, konstruktor potomka musí:

  • vyjmenovat závislosti rodiče
  • zavolat rodičovský konstruktor

To je v pořádku, závislosti rodiče jsou i jeho dědictvím. Jenže neexistuje mechanismus, kterým by se dalo volání rodičovského konstruktoru vynutit. Jednou z nejprotivnějších chyb se tak stane opomenutí volání parent::__construct. Takže předpoklad, že konstruktor už z principu vynucuje předání závislostí, je vlastně chybný. Konstruktor se dá snadno obejít.

Bez zajímavosti není, že zdáním je i neměnnost, protože nic nebrání zavolat na hotovém objektu $barbar->__construct(...) a protlačit mu jiné závislosti. Měl by tedy konstruktor testovat, zda není volán podruhé? Kašlete na to, konstruktor se prostě znovu volat nesmí. Otázka konvence.

Největší průšvih nastane ale ve chvíli, kdy provedu refactoring třídy Foobar, jehož důsledkem bude změna závislostí. Bude nutné přepsat konstruktory všech potomků. Jistě, je to logické, ale v praxi může jít o fatální zádrhel. Pokud například rodičem bude třída z frameworku (např. Presenter), jejíž potomky píše každý uživatel frameworku, fakticky se tak znemožní její vývoj, protože zásah do závislostí by byl kolosálním BC breakem.

Řada z výhod konstruktorové injektáže se rozplynula jak pára nad hrncem. Pokud se zdálo, že volání konstruktoru je vynuceno jazykem (silné a bezpečné), zatímco volání metod inject jen konvencí (opomenutelné), tak najednou se ukazuje, že to není zcela pravda.

Další možnosti

Možností, která částečně obchází problém konstruktoru a dědičnosti, je použití třídy FooDependencies zmíněné v článku Dependency Injection versus Service Locator:

class FoobarDependencies
{
	function __construct(HttpRequest $httpRequest, Router $router, Session $session)
}

class Foobar
{
	function __construct(FoobarDependencies $deps)
}

class Barbar extends Foobar
{
	function __construct(FoobarDependencies $deps, Logger $logger)
	{
		parent::__construct($deps);
		$this->logger = $logger;
	}
}

Když se změní závislosti rodičovské třídy Foobar, nemusí to nutně rozbít všechny potomky, protože se předávají v jedné proměnné. Běda ale, pokud ji předat zapomenou… Navíc tento způsob vyžaduje největší množství režijního kódu (dokonce celou režijní třídu).

Nebo lze závislosti rodičovské třídy Foobar předávat metodami a konstruktor uvolnit pro potomky. Rodič by se pak fakticky inicializoval až po volání těchto metod, takže konstruktor potomka by se volal nad neinicializovaným objektem. To není dobré.

A co obráceně, závislosti rodičovské třídy Foobar předávat konstruktorem a potomka metodami? To eliminuje všechny problémy, krom toho, že se těžko odhalí okamžik, kdy jsou nastaveny všechny závislosti (kvůli inicializaci objektu) a zda jsou vůbec nastaveny.

A co kdyby se všechny závislosti potomka nastavily jedinou metodou inject()? To by nejspíš vyřešilo všechny komplikace.

Nicméně stále jde jen o dvojstupňový případ rodič – potomek. Pro každého dalšího potomka by bylo třeba přijít s novou injektovací metodou a byl by problém zajistit, aby byly volány ve správném pořadí.

Dovedu si proto představit, že by vzniklo nové čisté řešení využívající nějaké PHP magie uvnitř třídy, která by ušetřila psaní režijního kódu, elegantně exponovala závislosti a předávala je do proměnných. Ty by mohly být označené třeba anotací @inject, nicméně šlo by o anotaci určenu pro tuto vnitřní implementaci, nikoliv o hint pro DI kontejner. Efekt by to mělo ve chvíli, kdyby se z toho stala obecněji uznávaná konvence, jinak to bude jen magie.

tl;dr

Předávání závislostí různými cestami má svá úskalí. Použití metod vyžaduje velké množství režijního kódu. Není od věci tyto metody pojmenovávat jiným prefixem, než obecné settery, kupříkladu lze použít inject. Poskytne to totiž důležitou informaci pro programátora, sekundárně ji může využít i DI kontejner.

Pokud nepoužíváte dědičnost, je zpravidla nejšikovnější závislosti předat skrze konstruktor a PHP by mohlo v příštích verzích syntaxi ještě o něco zjednodušit. Pokud ale do hry vstoupí dědičnost, je najednou všechno jinak. Ukazuje se, že dokonalý obecný mechanismus asi ani neexistuje. Možná by nebylo od věci zkusit nějaký, byť za využití PHP magie, vymyslet.