Nette Documentation Preview

Dependency Injection

Tato kapitola vás seznámí se základními programátorskými postupy, na kterých stojí celý framework Nette a které byste měli dodržovat při psaní vlastních 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 framework 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ší funkce (či statické metody) a 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: používej parametry

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

Když tohle pravidlo porušíme, nebude možné dosáhnout toho, aby byl kód srozumitelný, čistý a dlouhodobě udržitelný.

Když ho budeme dodržovat, jsme 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 předávání argumentů se odborně říká dependency injection.

(Nezaměňujte dependency injection s „dependency injection container“, jde o něco diametrálně odlišného a kontejnerum se budeme věnovat v druhé kapitole.)

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
{
	private float $a;
	private float $b;

	public function __construct(float $a, float $b)
	{
		$this->a = $a;
		$this->b = $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: používej parametry: všechna data, která třída potřebuje, jim musíme předat. Protože pokud to neuděláme a 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(), kde 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
{
	private Nette\Database\Connection $db;

	public function __construct(Nette\Database\Connection $db)
	{
		$this->db = $db;
	}

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

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: používej parametry?

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
{
	private string $file;

	public function __construct(string $file)
	{
		$this->file = $file;
	}

	public function log(string $message)
	{
		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;
		}
	}
}

Jenže nový 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: používej parametry: všechna data, která třída potřebuje, jim předáme.

Takže předáme do konstruktoru cestu k logu, kterou pak použijeme při vytváření objektu Logger? Nikoliv. Cesta totiž nejdou data, která třída NewsletterDistributor potřebuje; ty potřebuje Logger. Třída potřebuje logger jako takový. A ten si předáme:

class NewsletterDistributor
{
	private Logger $logger;

	public function __construct(Logger $logger)
	{
		$this->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 máte možnost vyměnit logger za jiný.

Zatímco v celé aplikaci si můžeme vystačit s jedinou instancí loggeru a předávat ji všude tam, kde se něco loguje, jinak je tomu v případě třídy Article. Její instance budeme chtít vytvářet vícekrát. Jak se vypořádat s vazbou na databázi v konstruktoru? Jako příklad si ukážeme kontroler, který po odeslání formuláře má uložit článek do databáze:

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

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

Stejně jako v předchozím případě, tohle není správný postup. Databáze není závislost UserController, ale Article. Navíc ve chvíli, kdy se nějak změní konstruktor třídy Article (přibude nový parametr), budeme muset upravit i kód na všech místech, kde se vytváří instance.

Řešením jsou továrny.

Pravidlo č. 2: používej továrny

Tím, že jsme zrušili skryté vazby a všechna data předáváme jako argumenty, získali jsme konfigurovatelnější a pružnější třídy. A proto potřebujeme ještě něco, 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.

Továrna

Továrna je třída, která vyrábí a konfiguruje objekty. Továrnu vyrábějící Article nazveme ArticleFactory a její použití v kontroleru bude následující:

class UserController extends Controller
{
	private ArticleFactory $articleFactory;

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

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

Implementace továrny může vypadat takto:

class ArticleFactory
{
	private Nette\Database\Connection $db;

	public function __construct(Nette\Database\Connection $db)
	{
		$this->db = $db;
	}

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

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 UserController, se to nijak nedotkne.

Shrnutí

Na začátku této kapitoly jsme slibovali, že si ukážeme prostý princip, jak navrhovat aplikace. Ačkoliv princip samotný prostý je (předávej třídám data, které potřebují), to co z něj vyplývá už vyžaduje víc přemýšlení. Klidně si tuto kapitolu přečtěte několikrát.

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í.

Nyní si ukážeme, co je to Dependency Injection Container.