Co je Dependency Injection?

Tato kapitola vás seznámí se základními programátorskými postupy, které byste měli dodržovat při psaní všech aplikací. Jde o základy nutné pro psaní čistého, srozumitelného a udržitelného kódu.

Pokud si tyto pravidla osvojíte a budete je dodržovat, bude vám Nette v každém kroku vycházet vstříc. Bude za vás řešit rutinní úlohy a poskytne vám maximální pohodlí, abyste se mohli soustředit na samotnou logiku.

Principy, které si zde ukážeme, jsou přitom celkem prosté. Nemusíte se ničeho obávat.

Pamatujete na svůj první program?

Netušíme sice, v jakém jazyce jste ho psali, ale kdyby to bylo PHP, nejspíš by vypadal nějak takto:

function soucet(float $a, float $b): float
{
	return $a + $b;
}

echo soucet(23, 1); // vypíše 24

Pár triviálních řádků kódu, ale přitom se v nich skrývá tolik klíčových konceptů. Že existují proměnné. Že se kód člení do menších jednotek, což jsou kupříkladu funkce. Že jim předáváme vstupní argumenty a ony vracejí výsledky. Chybí tam už jen podmínky a cykly.

To, že funkci předáme vstupní data a ona vrátí výsledek, je perfektně srozumitelný koncept, který se používá i v jiných oborech, jako je třeba v matematice.

Funkce má svoji signaturu, kterou tvoří její název, přehled parametrů a jejich typů, a nakonec typ návratové hodnoty. Jako uživatele nás zajímá signatura, o vnitřní implementaci obvykle nepotřebujeme nic vědět.

Teď si představte, že by signatura funkce vypadala takto:

function soucet(float $x): float

Součet s jedním parametrem? To je divné… A co třeba takto?

function soucet(): float

Tak to už je opravdu hodně divné, že? Jak se funkce asi používá?

echo soucet(); // co asi vypíše?

Při pohledu na takový kód bychom byli zmateni. Nejen že by mu nerozuměl začátečník, takovému kódu nerozumí ani zdatný programátor.

Přemýšlíte, jak by vlastně taková funkce vypadala uvnitř? Kde vezme sčítance? Zřejmě by si je nějakým způsobem obstarala sama, třeba takto:

function soucet(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}

V těle funkce jsme objevili skryté vazby na další globální funkce či statické metody. Abychom zjistili, odkud se skutečně sčítance berou, musíme pátrat dál.

Tudy ne!

Návrh, který jsme si právě ukázali, je esencí mnoha negativních rysů:

  • signatura funkce se tvářila, že nepotřebuje sčítance, což nás mátlo
  • vůbec nevíme, jak přimět funkci sečíst jiná dvě čísla
  • museli jsme se podívat do kódu, abychom zjistili, kde sčítance bere
  • objevili jsme skryté vazby
  • pro plné pochopení je třeba prozkoumat i tyto vazby

A je vůbec úkolem sčítací funkce obstarávat si vstupy? Samozřejmě, že není. Její zodpovědností je pouze samotné sčítání.

S takovým kódem se nechceme setkat, a rozhodně ho nechceme psát. Náprava je přitom jednoduchá: vrátit se k základům a prostě použít parametry:

function soucet(float $a, float $b): float
{
	return $a + $b;
}

Pravidlo č. 1: nech si to předat

Nejdůležitější pravidlo zní: všechna data, která funkce nebo třídy potřebují, jim musí být předána.

Místo toho, abyste vymýšleli skryté způsoby, pomocí kterých by se k nim mohly nějak dostat sami, jednoduše parametry předejte. Ušetříte čas potřebný na vymýšlení skrytých cest, které rozhodně váš kód nevylepší.

Pokud budete toto pravidlo vždy a všude dodržovat, jste na cestě ke kódu bez skrytých vazeb. Ke kódu, který je srozumitelný nejen autorovi, ale i každému, kdo jej po něm bude číst. Kde je vše pochopitelné ze signatur funkcí a tříd a není třeba pátrat po skrytých tajemstvích v implementaci.

Této technice se odborně říká dependency injection. A těm datům se říká závislosti. Přitom je to prachobyčejné předávání parametrů, nic víc.

Nezaměňujte prosím dependency injection, což je návrhový vzor, s „dependency injection container“, což je zase nástroj, tedy něco diametrálně odlišného. Kontejnerům se budeme věnovat později.

Od funkcí ke třídám

A jak s tím souvisí třídy? Třída je komplexnější celek než jednoduchá funkce, nicméně pravidlo č. 1 platí bezezbytku i tady. Jen existuje víc možností, jak argumenty předat. Kupříkladu docela podobně jako v případě funkce:

class Matematika
{
	public function soucet(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Matematika;
echo $math->soucet(23, 1); // 24

Nebo pomocí jiných metod, či přímo konstruktoru:

class Soucet
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function spocti(): float
	{
		return $this->a + $this->b;
	}

}

$soucet = new Soucet(23, 1);
echo $soucet->spocti(); // 24

Obě ukázky jsou zcela v souladu s dependency injection.

Reálné příklady

V reálném světe nebudete psát třídy pro sčítání čísel. Pojďme se přesunout k příkladům z praxe.

Mějme třídu Article reprezentující článek na blogu:

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

	public function save(): void
	{
		// uložíme článek do databáze
	}
}

a použití bude následující:

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

Metoda save() uloží článek do databázové tabulky. Implementovat ji za pomoci Nette Database bude hračka, nebýt jednoho zádrhelu: kde má Article vzít připojení k databázi, tj. objekt třídy Nette\Database\Connection?

Zdá se, že máme spoustu možností. Může jej vzít odněkud ze statické proměnné. Nebo dědit od třídy, která spojení s databází zajistí. Nebo využít tzv. singletonu. Nebo tzv. facades, které se používají v Laravelu:

use Illuminate\Support\Facades\DB;

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

	public function save(): void
	{
		DB::insert(
			'INSERT INTO articles (title, content) VALUES (?, ?)',
			[$this->title, $this->content],
		);
	}
}

Skvělé, problém jsme vyřešili.

Nebo ne?

Připomeňme pravidlo č. 1: nech si to předat: všechny závislosti, které třída potřebuje, jí musí být předány. Protože pokud pravidlo porušíme, nastoupili jsme cestu ke špinavému kódu plného skrytých vazeb, nesrozumitelnosti, a výsledkem bude aplikace, kterou bude bolest udržovat a vyvíjet.

Uživatel třídy Article netuší, kam metoda save() článek ukládá. Do databázové tabulky? Do které, ostré nebo testovací? A jak to lze změnit?

Uživatel se musí podívat, jak je implementovaná metoda save(), a najde použití metody DB::insert(). Takže musí pátrat dál, jak si tato metoda obstarává databázové spojení. A skryté vazby mohou tvořit docela dlouhý řetězec.

V čistém a dobře navrženém kódu se nikdy nevyskytují skryté vazby, Laravelovské facades nebo statické proměnné. V čistém a dobře navrženém kódu se předávají argumenty:

class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Ještě praktičtější, jak uvidíme dále, to bude konstruktorem:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function save(): void
	{
		$this->db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Pokud jste zkušený programátor, možná si říkáte, že Article by vůbec neměl mít metodu save(), měl by představovat čistě datovou komponentu a o ukládání by se měl starat oddělený repozitář. To dává smysl. Ale tím bychom se dostali hodně daleko nad rámec tématu, kterým je dependency injection, a snaze uvádět jednoduché příklady.

Budete-li psát třídu vyžadující ke své činnosti např. databázi, nevymýšlejte, odkud ji získat, ale nechte si ji předat. Třeba jako parametr konstruktoru nebo jiné metody. Přiznejte závislosti. Přiznejte je v API vaší třídy. Získáte srozumitelný a předvídatelný kód.

A co třeba tato třída, která loguje chybové zprávy:

class Logger
{
	public function log(string $message)
	{
		$file = LOG_DIR . '/log.txt';
		file_put_contents($file, $message . "\n", FILE_APPEND);
	}
}

Co myslíte, dodrželi jsme pravidlo č. 1: nech si to předat?

Nedodrželi.

Klíčovou informaci, tedy adresář se souborem s logem, si třída obstarává sama z konstanty.

Podívejte se na příklad použití:

$logger = new Logger;
$logger->log('Teplota je 23 °C');
$logger->log('Teplota je 10 °C');

Bez znalosti implementace, dokázali byste zodpovědět otázku, kam se zprávy zapisují? Napadlo by vás, že pro fungování je potřeba existence konstanty LOG_DIR? A dokázali byste vytvořit druhou instanci, která bude zapisovat jinam? Určitě ne.

Pojďme třídu opravit:

class Logger
{
	public function __construct(
		private string $file,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

Třída je teď mnohem srozumitelnější, konfigurovatelnější a tedy užitečnější.

$logger = new Logger('/cesta/k/logu.txt');
$logger->log('Teplota je 15 °C');

Ale to mě nezajímá!

„Když vytvořím objekt Article a zavolám save(), tak nechci řešit databázi, prostě chci, aby se uložil do té kterou mám nastavenou v konfiguraci.“

„Když použiju Logger, tak prostě chci, aby se zpráva zapsala, a nechci řešit kam. Ať se použije globální nastavení.“

To jsou správné připomínky.

Jako příklad si ukážeme třídu rozesílající newslettery, která zaloguje, jak to dopadlo:

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('Emaily byly rozeslány');

		} catch (Exception $e) {
			$logger->log('Došlo k chybě při rozesílání');
			throw $e;
		}
	}
}

Vylepšený Logger, který již nepoužívá konstantu LOG_DIR, vyžaduje v konstruktoru uvést cestu k souboru. Jak tohle vyřešit? Třídu NewsletterDistributor vůbec nezajímá, kam se zprávy zapisují, chce je jen zapsat.

Řešením je opět pravidlo č. 1: nech si to předat: všechna data, která třída potřebuje, jí předáme.

Takže to znamená, že si skrze konstruktor předáme cestu k logu, kterou pak použijeme při vytváření objektu Logger?

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ TAKHLE NE!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

Takhle ne! Cesta totiž nepatří mezi data, která třída NewsletterDistributor potřebuje; ty totiž potřebuje Logger. Vnímáte ten rozdíl? Třída NewsletterDistributor potřebuje logger jako takový. Takže ten si předáme:

class NewsletterDistributor
{
	public function __construct(
		private Logger $logger, // ✅
	) {
	}

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('Emaily byly rozeslány');

		} catch (Exception $e) {
			$this->logger->log('Došlo k chybě při rozesílání');
			throw $e;
		}
	}
}

Nyní je ze signatur třídy NewsletterDistributor jasné, že součástí její funkčnosti je i logování. A úkol vyměnit logger za jiný, třeba kvůli testování, je zcela triviální. Navíc pokud by se konstruktor třídy Logger změnil, nebude to mít na naši třídu žádný vliv.

Pravidlo č. 2: ber, co tvé jest

Nenechte se zmást a nenechte si předávat závislosti svých závislostí. Nechte si předávat jen své závislosti.

Díky tomu bude kód využívající jiné objekty zcela nezávislý na změnách jejich konstruktorů. Jeho API bude pravdivější. A hlavně bude triviální tyto závislosti vyměnit za jiné.

Nový člen rodiny

Ve vývojářském týmu padlo rozhodnutí vytvořit druhý logger, který zapisuje do databáze. Vytvoříme tedy třídu DatabaseLogger. Takže máme dvě třídy, Logger a DatabaseLogger, jedna zapisuje do souboru, druhá do databáze … nezdá se vám na tom pojmenování něco divného? Nebylo by lepší přejmenovat Logger na FileLogger? Určitě ano.

Ale uděláme to chytře. Pod původním názvem vytvoříme rozhraní:

interface Logger
{
	function log(string $message): void;
}

… které budou oba loggery implementovat:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

A díky tomu nebude potřeba nic měnit ve zbytku kódu, kde se logger využívá. Například konstruktor třídy NewsletterDistributor bude stále spokojen s tím, že jako parametr vyžaduje Logger. A bude jen na nás, kterou instanci mu předáme.

Proto nikdy nedáváme názvům rozhraní příponu Interface nebo předponu I. Jinak by nebylo možné kód takto hezky rozvíjet.

Houstone, máme problém

Zatímco v celé aplikaci si můžeme vystačit s jedinou instancí loggeru, ať už souborového nebo databázového, a jednoduše jej předáváme všude tam, kde se něco loguje, docela jinak je tomu v případě třídy Article. Její instance totiž vytváříme dle potřeby, klidně vícekrát. Jak se vypořádat s vazbou na databázi v jejím konstruktoru?

Jako příklad může sloužit kontroler, který po odeslání formuláře má uložit článek do databáze:

class EditController extends Controller
{
	public function formSubmitted($data)
	{
		$article = new Article(/* ... */);
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Možné řešení se přímo nabízí: necháme si objekt databáze předat konstruktorem do EditController a použijeme $article = new Article($this->db).

Stejně jako v předchozím případě s Logger a cestou k souboru, tohle není správný postup. Databáze není závislost EditController, ale Article. Předávat si databázi tedy jde proti pravidlu č. 2: ber, co tvé jest. Když se změní konstruktor třídy Article (přibude nový parametr), bude nutné upravit také kód na všech místech, kde se vytváří instance. Ufff.

Houstone, co navrhuješ?

Pravidlo č. 3: nech to na továrně

Tím, že jsme zrušili skryté vazby a všechny závislosti předáváme jako argumenty, získali jsme konfigurovatelnější a pružnější třídy. A tudíž potřebujeme ještě cosi dalšího, co nám ty pružnější třídy vytvoří a nakonfiguruje. Budeme tomu říkat továrny.

Pravidlo zní: pokud má třída závislosti, nech vytváření jejich instancí na továrně.

Továrny jsou chytřejší náhrada operátoru new ve světě dependency injection.

Nezaměňujte prosím s návrhovým vzorem factory method, který popisuje specifický způsob využití továren a s tímto tématem nesouvisí.

Továrna

Továrna je metoda nebo třída, která vyrábí a konfiguruje objekty. Třídu vyrábějící Article nazveme ArticleFactory a mohla by vypadat například takto:

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Její použití v kontroleru bude následující:

class EditController extends Controller
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function formSubmitted($data)
	{
		// necháme továrnu vytvořit objekt
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Když se v tuto chvíli změní signatura konstruktoru třídy Article, jediná část kódu, která na to musí reagovat, je samotná továrna ArticleFactory. Veškerého dalšího kódu, který s objekty Article pracuje, jako například EditController, se to nijak nedotkne.

Možná si teď klepete na čelo, jestli jsme si vůbec pomohli. Množství kódu narostlo a celé to začíná vypadat podezřele komplikovaně.

Nemějte obavy, za chvíli se dostaneme k Nette DI kontejneru. A ten má řadu es v rukávu, kterými budování aplikací používajících dependency injection nesmírně zjednoduší. Tak kupříkladu místo třídy ArticleFactory bude stačit napsat pouhý interface:

interface ArticleFactory
{
	function create(): Article;
}

Ale to předbíháme, ještě vydržte :-)

Shrnutí

Na začátku této kapitoly jsme slibovali, že si ukážeme postup, jak navrhovat čistý kód. Stačí třídám

  1. předávat závislosti, které potřebují
  2. a naopak nepředávat, co přímo nepotřebují
  3. a že objekty se závislostmi se nejlépe vyrábí v továrnách

Nemusí se to tak na první pohled zdát, ale tyhle tři pravidla mají dalekosáhlé důsledky. Vedou k radikálně jinému pohledu na návrh kódu. Stojí to za to? Programátoři, kteří zahodili staré zvyky a začali důsledně používat dependency injection, považují tento krok za zásadní moment v profesním životě. Otevřel se jim svět přehledných a udržitelných aplikací.

Co když ale kód důsledně dependency injection nepoužívá? Co když je postaven na statických metodách nebo singletonech? Přináší to nějaké problémy? Přináší a velmi zásadní.

verze: 3.x 2.x