Na navigaci | Klávesové zkratky

Nabušené DI srdce pro vaše aplikace

Jednou z nejzajímavějších částí Nette, kterou vychvalují i uživatelé jiných frameworků, je Dependency Injection Container (dále Nette DI). Podívejte se, jak snadno jej můžete použít kdekoliv, i mimo Nette.

Mějme aplikaci pro rozesílání newsletterů. Kód jednotlivých tříd jsem zjednodušil na dřeň. Máme tu objekt představující email:

class Mail
{
	public $subject;
	public $message;
}

Někoho, kdo ho umí odeslat:

interface Mailer
{
	function send(Mail $mail, $to);
}

Přidáme podporu pro logování:

interface Logger
{
	function log($message);
}

A nakonec třídu, která rozesílání newsletterů zajišťuje:

class NewsletterManager
{
	private $mailer;
	private $logger;

	function __construct(Mailer $mailer, Logger $logger)
	{
		$this->mailer = $mailer;
		$this->logger = $logger;
	}

	function distribute(array $recipients)
	{
		$mail = new Mail;
		...
		foreach ($recipients as $recipient) {
			$this->mailer->send($mail, $recipient);
		}
		$this->logger->log(...);
	}
}

Kód respektuje Dependency Injection, tj. že každá třída pracuje pouze s proměnnými, které jsme jí předali. Také máme možnost si Mailer i Logger implementovat po svém, třeba takto:

class SendMailMailer implements Mailer
{
	function send(Mail $mail, $to)
	{
		mail($to, $mail->subject, $mail->message);
	}
}

class FileLogger implements Logger
{
	private $file;

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

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

DI kontejner je nejvyšší architekt, který umí stvořit jednotlivé objekty (v terminologii DI označované jako služby) a poskládat a nakonfigurovat je přesně podle naší potřeby.

Kontejner pro naši aplikaci by mohl vypadat třeba takto:

class Container
{
	private $logger;
	private $mailer;

	function getLogger()
	{
		if (!$this->logger) {
			$this->logger = new FileLogger('log.txt');
		}
		return $this->logger;
	}

	function getMailer()
	{
		if (!$this->mailer) {
			$this->mailer = new SendMailMailer;
		}
		return $this->mailer;
	}

	function createNewsletterManager()
	{
		return new NewsletterManager($this->getMailer(), $this->getLogger());
	}
}

Implementace vypadá takto, aby:

  • se jednotlivé služby vytvářely, až když je potřeba (lazy)
  • dvojí volání createNewsletterManager využívalo stále stejný objekt loggeru a maileru

Vytvoříme instanci Container, necháme ji vyrobit managera a můžeme se pustit do spamování uživatelů newslettery:

$container = new Container;
$manager = $container->createNewsletterManager();
$manager->distribute(...);

Podstatné na Dependency Injection je, že žádná třída nemá závislost na kontejneru. Tudíž jej můžeme klidně nahradit za jiný. Třeba za kontejner, který nám vygeneruje Nette DI.

Nette DI

Nette DI je totiž generátor kontejnerů. Instruujeme ho (zpravidla) pomocí konfiguračních souborů a třeba tato konfigurace vygeneruje cca totéž, jako byla třída Container:

services:
	- FileLogger( log.txt )
	- SendMailMailer
	- NewsletterManager

Zásadní výhodou je stručnost zápisu. Navíc jednotlivým třídám můžeme přidávat další a další závislosti často bez nutnosti do konfigurace zasahovat.

Nette DI vygeneruje skutečně PHP kód kontejneru. Ten je proto extrémně rychlý, programátor přesně ví, co dělá, a může ho třeba i krokovat.

Kontejner může mít v případě velkých aplikací desetitisíce řádků a udržovat něco takového ručně by už nejspíš ani nebylo možné.

Nasazení Nette DI do naší aplikace je velmi snadné. Nejprve jej nainstalujeme Composerem (protože stahování zipů je tááák zastaralé):

composer require nette/di

Výše uvedenou konfiguraci uložíme do souboru config.neon a pomocí třídy Nette\DI\ContainerLoader vytvoříme kontejner:

$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp');
$class = $loader->load(function($compiler) {
	$compiler->loadConfig(__DIR__ . '/config.neon');
});
$container = new $class;

a pak jej opět necháme vytvořit objekt NewsletterManager a můžeme rozesílat emaily:

$manager = $container->getByType('NewsletterManager');
$manager->distribute(['john@example.com', ...]);

Ale ještě na chvíli zpět ke ContainerLoader. Uvedený zápis je podřízen jediné věci: rychlosti. Kontejner se vygeneruje jednou, jeho kód se zapíše do cache (adresář __DIR__ . '/temp') a při dalších požadavcích se už jen odsud načítá. Proto je načítání konfigurace umístěno do closure v metodě $loader->load().

Během vývoje je užitečné aktivovat auto-refresh mód, kdy se kontejner automaticky přegeneruje, pokud dojde ke změně jakékoliv třídy nebo konfiguračního souboru. Stačí v konstruktoru ContainerLoader uvést jako druhý argument true.

Jak vidíte, použití Nette DI rozhodně není limitované na aplikace psané v Nette, můžete jej pomocí pouhých 3 řádků kódu nasadit kdekoliv. Zkuste si s ním pohrát, celý příklad je dostupný na GitHubu.

Komentáře

  1. Petr Sládek #1

    avatar

    proč to ty dvě služby udělalo jako lazyload, ale toho Newsletter managera jako továrničku?

    nemělo by to v tom config.neon být spáš v sekci factories?

    před 9 lety | reagoval [2] David Grudl
  2. David Grudl #2

    avatar

    #1 Petře Sládku, Nette DI ke všem službám přistupuje pomocí „lazy load“, sekce factories už se nepoužívá. Kód třídy Container byl jen příklad, jak takový kontejner může vypadat.

    před 9 lety | reagoval [6] Aleš Roubíček
  3. Richard Suchý #3

    avatar

    $class = $loader->load('', function($compiler) { – ten prázdný řetězec vypadá krajně podezřele… co je to za parametr a jak jej využít?

    před 9 lety | reagoval [4] David Grudl
  4. David Grudl #4

    avatar

    #3 Richarde Suchý, první parametr je klíč, metoda load() je obdobná té z cache. V jednom adresáři tak může být (a často bývá) uložených víc kontejnerů, jeden třeba pro vývoj a druhý pro produkční režim, lišících se v různých parametrech, od připojení k DB až po aktivaci ladících lišt atd.

    před 9 lety
  5. Ivan #5

    avatar

    Pises Kód respektuje Dependency Injection, tj. že každá třída pracuje pouze s proměnnými, které jsme jí předali.

    V kodu ale vidim:

    function distribute(array $recipients)
        {
            $mail = new Mail;

    Jak sis predal tridu Mail?

    před 9 lety | reagoval [6] Aleš Roubíček [7] David Grudl
  6. Aleš Roubíček #6

    avatar

    #5 Ivane, Mail není služba, ale data. Ne vše je nutné předávat, to by program nikdy nefungoval a nedělal co potřebujeme. Typicky se předávají jen věci, které z nějakého důvodu potřebujeme abstrahovat (závislosti na cizí knihovny, požadavek na polymorfní chování, nehotová implemetace modulu…)

    #2 Davide Grudle, Container v ukázce je ve skutečnosti ServiceLocator, není třeba dávat mu jiný název. :)

    před 9 lety | reagoval [7] David Grudl
  7. David Grudl #7

    avatar

    #5 Ivane, zkusím to říct jinak: „třída pracuje s daty, které jsme ji předali; jiné si nezískává.“ Daty chápej třeba konfiguraci (maileru, loggeru), databázové připojení atd. Naopak deterministické funkce (což je i operátor new) takovou informací nejsou.

    Nicméně bylo by možné vytvořit továrnu na objekty Mail a tu předat třídě NewsletterManager, takže místo $mail = new Mail by volala $mail = $this->mailFactory->create() – to je ok. Z pohledu Dependency Injection jsou obě řešení v pořádku, představují jen jiný návrh.

    #6 Aleši Roubíčku, název Container nemám rád, protože není výstižný, nicméně Locator se sem nehodí už vůbec, neboť účelem třídy není poskytovat lookup API ke službám, ale naopak služby vytvářet a konfigurovat. Třeba ServiceFactory by bylo výstižnější.

    před 9 lety | reagoval [8] Aleš Roubíček
  8. Aleš Roubíček #8

    avatar

    #7 Davide Grudle, když se podívám na Bliki o ServiceLocatoru, tak moc velký rozdíl oproti tvému Containeru nevidím. :) Žádný lookup, ale sada factory metod poskytující jednotlivé služby.

    před 9 lety | reagoval [9] Filip Procházka [10] David Grudl
  9. Filip Procházka #9

    avatar

    #8 Aleši Roubíčku, Je velký rozdíl v tom, když máš na vstupu do aplikace jedno vytažení z containeru jedné služby, kterou pak aplikaci spustíš a když tím máš posetou celou aplikaci a v modelech vyžaduješ container, nikoliv konkrétní služby. Pokud v aplikaci jako takové používáš container přímo, tak ho degraduješ na service locator. Ale není přece možné inicializovat to vzduchem magicky, minimálně nějaký front controller na kterém zavoláš run, vždy vytáhnout musíš.

    před 9 lety
  10. David Grudl #10

    avatar

    #8 Aleši Roubíčku, podívej se lépe. ServiceLocator (a vystihuje to i název) funguje jako singleton registry, neumí objekty vyrábět, jen je vracet. Tedy někdo jej musí instancemi naplnit. Naopak úkolem Containeru je služby vyrábět (metody getLogger() a getMailer() mohly klidně být private).

    před 9 lety
  11. nepovím #11

    avatar

    Díky, pro začátečníky jako já super čtení.

    před 9 lety
  12. lenoch #12

    avatar

    A na ServiceLocatoru je špatně to, že nepředávám výslovně potřebné objekty skrz konstruktor jako u DI?
    Mě totiž přijde jako klíčová featura to, že si můžu někde nakonfigurovat jaké objekty se budou čemu předávat, což umožňuje jak DIContainer, tak ServiceLocator (imho).
    To že se objekt „hlásí“ ke svým závislostem, aspoň mě osobně zas tak významné nepřijde. Stejně musím (v případě DI) bádat v nějakém kontejneru nebo konfiguračním souboru, co tam vlastně odkud probublává, ne?

    před 9 lety | reagoval [13] David Grudl [14] Taco
  13. David Grudl #13

    avatar

    #12 lenochu, mám tu o tom celou řadu článků. Stručně:

    • Service Locator je globální repozitář všech služeb a jednotlivé objekty si sami z něj služby vytahují. Vazby jsou tedy přímo zadrátované v kódu tříd (a to je špatně).
    • Service Locator může být i objekt, který předávám dalším objektům, aby si z něj vytahovali služby. Tím porušuji Law of Demeter (a to je taky špatně).
    • DI container: jednotlivým objektům jsou služby předávány zvenčí (jakkoliv), vazby jsou tedy věcí továren, hlavní továrnou je DI container. Žádný Service Locator není potřeba.
    před 9 lety
  14. Taco #14

    avatar

    #12 lenochu, Nevědomky sis odpověděl sám. Díky tomu, že se objekt hlásí ke svým závislostem, tak bádat nemusíš (výjimka mě napadá, kdy máš více různých implementací téhož, ale to bude IMHO opravdu zřídka).

    před 9 lety
  15. Igor Hlina #15

    avatar

    Davide Grudle mohol by si sa trocha rozpisat o tom preco je $loader->load() pouziva anonymnu funkciu?

    <cite>
    Uvedený zápis je podřízen jediné věci: rychlosti. Kontejner se vygeneruje jednou, jeho kód se zapíše do cache a při dalších požadavcích se už jen odsud načítá. Proto je načítání konfigurace umístěno do closure v metodě.
    </cite>

    Ako presne toto funguje?

    před 9 lety | reagoval [16] David Grudl
  16. David Grudl #16

    avatar

    #15 Igore Hlino, Nemusí to být anonymní funkce, může to být i metoda. Jde jen o tom, že kód ve funkci se vykoná pouze jednou, vygeneruje se kód kontejneru, uloží do cache a při dalších požadavcích se už používá cache.

    před 9 lety | reagoval [17] Kubo2
  17. Kubo2 #17

    avatar

    #16 Davide Grudle, Takže vlastne ide iba o to, aby sa nevytváral objekt $compiler, pokiaľ to nie je potrebné?

    před 9 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.


phpFashion © 2004, 2024 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.