Co je Dependency Injection?

před 12 lety od David Grudl  

Dependency Injection je prostá a skvělá technika, která vám pomůže psát mnohem srozumitelnější a předvídatelnější kód.

Kód téměř vždy píšeme pro jiné: spolupracovníky, uživatele našich open source knihoven nebo o pár let starší sebe sama. Abychom předešli nepříjemným WTF momentům při jeho používání, je dobré dbát na srozumitelnost. Ať už v pojmenování identifikátorů, výřečnosti chybových zpráv nebo návrhu rozhraní tříd. A ke srozumitelnosti bych přidal ještě předvídatelnost. Schválně, očekávali byste, že volání $b->hello() v této ukázce může nějak změnit stav zcela nezávislého opodál stojícího objektu $a?

$a = new A;

$b = new B;
$b->hello();

To by bylo divné, že? Jo, kdyby oba objekty byly nějak explicitně propojeny, třeba kdybychom volali $b->hello($a) (tj. s argumentem $a) nebo předtím nastavili $b->setA($a), tak by mezi oběma objekty existovala vazba a dalo by se očekávat, že $b může něco provádět s $a. Ale bez toho by to bylo nečekané, nesportovní a matoucí…

Říkáte si, že to je přece jasné? Že jen blázen by takový magický kód psal? Tak se podívejte na následující příklad, který v různých obměnách najdete v řadě příruček „blog in 15 minutes with our amazing framework“:

$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();

Třída Article reprezentuje článek na blogu a metoda save() nám jej uloží. Kam? Asi do databázové tabulky. Skutečně? Co když ho uloží do souboru na disk? A pokud do databáze, tak do jaké tabulky? K jaké databázi se vlastně připojí? Ostré nebo testovací? K SQLite nebo k Mongu? Pod jakým účtem?

Jde o stejný případ, jako v předchozí ukázce, jen pod $a si představte (neviditelný) objekt reprezentující databázové spojení a $b->hello() nahraďte za $article->save(). Co se však nezměnilo, je nepředvídatelnost a nesrozumitelnost kódu.

Museli bychom se podívat, jak je implementovaná metoda save(), abychom zjistili, kam se data ukládají. Zjistili bychom, že si šahá do nějaké globální proměnné udržující databázové spojení. Museli bychom pátrat dál, kde se v kódu databázové spojení vytváří, a pak bychom teprve měli obrázek o tom, jak vše funguje.

Nicméně, i kdybychom pochopili, jak je vše provázané, byl by oříšek do toho zasáhnout. Jak třeba za účelem testování uložit článek jinam? Asi by to vyžadovalo změnit nějakou statickou proměnnou. Ale nerozbili bychom tím něco jiného?

Jaj, statické proměnné jsou zlo. Vytvářejí skryté závislosti, kvůli kterým nemáme kód pod kontrolou. Kód má pod kontrolou nás ☹

Řešení je Dependency Injection

Dependency Injection (dále jen DI) neboli zřejmé předávání závislostí říká: odeberte třídám zodpovědnost za získávání objektů, které potřebují ke své činnosti.

Budete-li psát třídu vyžadující ke své činnosti databázi, nevymýšlejte uvnitř jejího těla, odkud ji získat (tj. ze žádné globální proměnné, statické metody, singletonu, registru atd.), ale požádejte o ni v konstruktoru nebo jiné metodě. Popište závislosti svým API. Nebudete muset tolik přemýšlet a získáte srozumitelný a předvídatelný kód.

A to je vše. To je celé slavné DI.

Pojďme si to ukázat v praxi. Začneme u nešťastné implementace třídy Article:

class Article
{
	public $id;
	public $title;
	public $content;

	function save()
	{
		// uložíme do databáze
		// …ale kde databázové spojení seženu?
		// GlobalDb::getInstance()->query() ?
	}
}

Autor metody save() musel řešit nelehkou otázku, kde vzít připojení k databázi. Kdyby použil DI, nemusel by nad ničím uvažovat (a to mají programátoři rádi), neboť DI dává jasnou odpověď: pokud potřebuješ databázi, ať ti ji někdo dodá. Jinými slovy: nic nesháněj, ať se postará někdo jiný.

class Article
{
	public $id;
	public $title;
	public $content;

	function save(Nette\Database\Connection $connection)
	{
		$connection->table('articles')->insert(array(
			'title' => $this->title,
			'content' => $this->content,
		));
	}
}

Takže aplikace principů DI znamená jen to, že jsme předali $connection jako parametr metodě? Jako vážně? Ano. Jako vážně.

Užití třídy Article se pochopitelně nepatrně změní:

$article = new Article;
$article->title = ...
$article->content = ...
$article->save($connection);

Díky této změně je nyní z kódu naprosto zřejmé, že se článek uloží do databáze, a taky do které databáze.

Řešení pomocí DI tak přestavuje win-win situaci: autor třídy Article nemusel řešit, kde objekt-databázi sežene, její uživatel nemusel pátrat, kde ho programátor sehnal. Z kódu je nyní zřejmé, že článek se uloží do databáze a lze velmi snadno nechat jej uložit do databáze jiné.

Můžete ale přijít s celou řadou námitek. Kde se třeba vezme v posledním příkladu proměnná $connection? DI opakuje: „ať se postará někdo jiný“. Databázové spojení zkrátka dodá ten, kdo zavolá uvedený kód.

Nojo, ale teď to vypadá, že používání DI značně zkomplikuje kód, protože kvůli vytvoření instance Article musíte uchovávat a předávat databázové spojení. Navíc časem může ve třídě Article vzniknout potřeba nějaká data formátovat a v souladu s DI bude potřeba předávat ještě další objekty. Komplikovalo by nám to například kontrolery:

class ArticlePresenter
{
	function __construct(Connection $connection, TextFormatter $formatter, ...)
	{
		$this->connection = $connection;
		$this->formatter = $formatter;
		...
	}

	function createArticle()
	{
		return new Article($this->connection, $this->formatter, ...);
	}
}

Když bude mít presenter co do činění s dalšími podobnými třídami jako je Article, bude mít haldu závislostí. Ba co víc, Article by měla projít refaktoringem, kdy nahradíme databázi obecnějším úložištěm ArticleStorage, nebo jí zodpovědnosti za ukládání sebe sama úplně zbavíme a delegujeme to na novou třídu ArticleRepository. To by znamenalo upravit aplikaci na mnoha místech; přinejmenším všude, kde se vytváří instance Article. Co s tím?

Elegantní řešení jsou továrničky. Místo ručního (tj. operátorem new) vytváření objektů Article si je necháme vyrábět továrničkou. A místo všech závislostí třídy Article si budeme předávat jen jeho továrničku:

class ArticleFactory
{
	function __construct(Connection $connection, TextFormatter $formatter, ...)
	{
		$this->connection = $connection;
		$this->formatter = $formatter;
		...
	}

	function create()
	{
		return new Article($this->connection, $this->formatter, ...);
	}
}

Původní ArticlePresenter se nám nejen krásně zjednoduší a zároveň bude jeho API lépe vystihovat podstatu, tedy že ArticlePresenter nepotřebuje žádnou databázi, chce prostě jen pracovat s články. Z takového refaktoringu má člověk vyloženě dobrý pocit:

class ArticlePresenter
{
	function __construct(ArticleFactory $articleFactory)
	{
		$this->articleFactory = $articleFactory;
	}

	function createArticle()
	{
		return $this->articleFactory->create();
	}
}

V praxi se ukazuje, že každá třída mívá jen několik málo závislostí, které ji předáváme. Důsledné používání DI tak navzdory obavám kód nijak nekomplikuje a výhody, jako je srozumitelnost, jednoznačně převažují nad tou troškou psaní navíc v konstruktorech.

Lze také namítnout, že netransparentní chování původní třídy Article, která článek uložila neznámo kam, vlastně vůbec nevadí, pokud ho metoda load() zase bude umět načíst. Vtip je v tom, že nad kódem navrženým podle DI principu vždycky můžeme takto fungující obálku vytvořit. Ale naopak toho docílit nelze.

Dependency Injection je technika z rodiny Inversion of Control (IoC), do které patří i Service locator. Ten bývá zmiňován jako jakési zlé dvojče. Proč se mu vyhnout si řekneme v dalším článku DI versus Service Locator.