Dependency Injection
Podstatou dependency injection (DI) je odebrat třídám zodpovědnost za získávání objektů, které potřebují ke své činnosti (tzv. služeb) a místo toho jim služby předávat při vytváření. Řekneme si:
- co je principem dependency injection?
- jak vytvářet DI kontejnery
Co je to dependency injection?
Narovinu: dependency injection (DI) není nic tajemného nebo nepochopitelného. Celé se dá shrnout do jedné sobecké věty:
„nic nesháněj, ať se postará někdo jiný.“ Nic víc, nic míň. Převeďme to do řeči programátorů. Máme
třídu Article
reprezentující článek na blogu:
class Article
{
public $id;
public $title;
public $content;
function save()
{
// uložíme 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()
nám uloží článek do databázové tabulky articles
. 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
?
Nejspíš bychom si poradili, můžeme jej uložit někde do globální proměnné $GLOBALS['database']
. Říkali
vám, že používání globálních proměnných je špatné a že máte raději používat statické proměnné tříd? No měli
pravdu, globální proměnné jsou zlo, jenže statické proměnné tříd jsou zcela totéž. To je jako říci: „Nejezte
hamburgery, tloustne se po nich, dejte si raději cheeseburger.“
Kde tedy seženeme připojení k databázi? DI má odpověď: „nic nesháněj, ať se postará někdo jiný.“ Jinými slovy, pokud potřebuji databázi, ať mi ji někdo dodá, já to řešit nehodlám. Cha, to je vychytralé, milé DI! Tak do toho:
class Article
{
public $id;
public $title;
public $content;
private $database;
function __construct(Nette\Database\Connection $database)
{
$this->database = $database;
}
function save()
{
$this->database->query('INSERT INTO articles', [
'title' => $this->title,
'content' => $this->content,
]);
}
}
Použití třídy Article
se pochopitelně trošku změní:
$article = new Article($database);
$article->title = ...
$article->content = ...
$article->save();
Ptáte se, kde tento kód vezme $database
? DI dává jasnou odpověď: „ať se postará někdo jiný“.
Databázové spojení zkrátka dodá ten, kdo volá uvedený kód. A tak dále, a tak dále. Jistě si říkáte, že přece
nelze delegovat zodpovědnost do nekonečna. Že musí být nějaký počátek všehomíra. A máte pravdu. Úplně na začátku
je stvořitel, který už nic nedeleguje a objekty tvoří. Říká se mu DI kontejner a dočtete se o něm za chvíli.
Proč jsou globální proměnné zlo?
Dobrá otázka. Zvídavému programátorovi nestačí si přečíst, že to či ono je zlo, chce znát důvod. Třída
Article
tak jako tak databázové připojení potřebuje. Jenže z prvního příkladu užití není vůbec patrné,
odkud a jak ho získá. Uživatel takové kódu může být až překvapen, že se vůbec článek uloží, a ptá se „kam se
vlastně uložil?“ Naopak v druhém příkladu používajícím DI je kód samovysvětlující.
Představte si, že zkoumáte nějakou knihovnu pro platební brány a napíšete si příklad:
$cc = new CreditCard('4461510140804839', 12, 2013);
$cc->pay(1000, CreditCard::DOLLARS);
Kód spustíte, s číslem své karty, a po nějaké době zjistíte, že vám to skutečně z účtu strhlo peníze!
Šokovaný zíráte na výpis a lamentujete: „kde jsou mé peníze, jak je to mohlo strhnout, vždyť jsem to s žádnou
platební bránou nepropojil!“ Třída CreditCard
se s ní propojila nějak sama, získala ji odněkud
z globální proměnné, podobně tajemně, jako si původní Article
získal připojení k databázi. Takovou věc
z kódu nejen že nevydedukujete, ale ani nevíte, jak změnit platební bránu na jinou, třeba testovací.
Továrny
Můžete namítnout, že používání DI znamená víc psaní, že kvůli vytvoření instance Article
musíte
uchovávat a předávat databázové spojení a podobně. To je pravda, nicméně nezapomeňte, že posledně vás „méně
psaní“ připravilo o $1000! Ne, nechceme to zlehčovat. Připomínka je zcela korektní a my ještě přidáme jednu
závažnější: časem může ve třídě Article
vzniknout potřeba nějaká data cachovat a v souladu s DI bude
požadovat předání ještě objektu představujícího úložiště cache. To by znamenalo upravit aplikaci na mnoha místech:
přinejmenším všude, kde se vytváří instance Article
.
class UnhappyClassWorkingWithArticle
{
private $database;
private $cacheStorage;
function __construct(
Nette\Database\Connection $database,
Nette\Caching\Storage $cacheStorage,
...
)
{
$this->database = $database;
$this->cacheStorage = $cacheStorage;
...
}
function doSomething()
{
$article = new Article($this->database, $this->cacheStorage, ...);
...
}
}
Co s tím? Věc má řešení. Místo ručního vytváření objektů Article
si vytvoříme továrnu
ArticleFactory
, tedy objekt, který bude objekty Article
vyrábět. Když Article
změní
konstruktor, upraví se jen továrna, nic víc. A odkud onu továrnu v našem kódu získáme? Vždyť víte… o to ať se
postará někdo jiný :-).
class ArticleFactory
{
private $database;
private $cacheStorage;
function __construct(
Nette\Database\Connection $database,
Nette\Caching\Storage $cacheStorage,
...
)
{
$this->database = $database;
$this->cacheStorage = $cacheStorage;
...
}
function create(): Article
{
return new Article($this->database, $this->cacheStorage, ...);
}
}
class HappyClassWorkingWithArticle
{
private $articleFactory;
function __construct(ArticleFactory $articleFactory)
{
$this->articleFactory = $articleFactory;
}
function doSomething()
{
$article = $this->articleFactory->create();
...
}
}
DI kontejner a služby
Termínem DI kontejner označujeme prvotní továrnu, která vytváří všechny objekty nutné pro spuštění a chod
aplikace. Těmto objektům se říká služby. Co jsou to služby? Obyčejné objekty, jako třeba instance zmíněného
Nette\Database\Connection
. Jen v souvislosti s DI kontejnery jim říkáme služby.
Příkladem může být kontejner, který vytvoří objekt ArticleFactory
, ale také jím požadované připojení
k databázi:
class Container
{
function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection('mysql:', 'root', '***');
}
function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->createDatabase());
}
}
Použití by vypadalo následovně:
$container = new Container;
$database = $container->createDatabase();
Výhoda je zřejmá, nemusíme se starat o to, jak objekt instancovat, to je záležitostí továrny. Nicméně řešení má zatím dva nedostatky. Jednak jsou přihlašovací údaje natvrdo zadrátované do kódu, proto je vyčleníme do proměnné:
class Container
{
private $parameters;
function __construct(array $parameters)
{
$this->parameters = $parameters;
}
function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->createDatabase());
}
}
$container = new Container([
'dsn' => 'mysql:',
'user' => 'root',
'password' => '***',
]);
$database = $container->createDatabase();
Závažnějším nedostatkem je, že když zavoláme createArticleFactory()
vícekrát, vytvoří se pokaždé
nové připojení k databázi. Tomu je třeba zabránit. Přidáme proto metodu getService
, která bude uchovávat
jednou vytvořené služby pro příští použití:
class Container
{
private $parameters;
private $services = [];
function __construct(array $parameters)
{
$this->parameters = $parameters;
}
function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->getService('Database'));
}
function getService(string $name)
{
if (!isset($this->services[$name])) {
// getService('Database') bude volat createDatabase()
$method = 'create' . $name;
$this->services[$name] = $this->$method();
}
return $this->services[$name];
}
}
A máme plně funkční DI kontejner. Jak vidíte, napsat jej není nic složitého. Za připomenutí stojí, že samotné služby neví, že je vytváří nějaký kontejner, tím pádem je možné takto vytvářet jakýkoliv objekt v PHP bez zásahu do jeho zdrojového kódu.
Nette\DI\Container
Třída Nette\DI\Container je pružná implementace univerzálního DI kontejneru. Vlastní kontejnery můžeme vytvářet buď staticky, tj. poděděním této třídy, nebo si je nechat automaticky generovat, což je velmi elegantní řešení.
Názvy továrních funkcí dodržují jednotnou konvenci, jsou tvořeny předponou createService
+ názvem služby
začínajícím velkým písmenem. Všimněte si, že kontejner už má definované pole $parameters
pro
uživatelské parametry a také metodu getService()
.
class MyContainer extends Nette\DI\Container
{
function createServiceDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
function createServiceArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->getService('database'));
}
}
Vytvoříme instanci kontejneru a předáme parametry, které se zapíší do pole $this->parameters
:
$container = new MyContainer([
'dsn' => 'mysql:',
'user' => 'root',
'password' => '***',
]);
Službu získáme metodu getService
:
// první písmeno názvu služby píšeme malé
$database = $container->getService('database');
Generovaný kontejner
Knihovna Nette DI je ale především generátor kontejnerů. To je její nejsilnější stránka. Instruujeme ho (zpravidla) pomocí konfiguračních souborů.
Třeba tato konfigurace vygeneruje cca totéž, jako byla třída MyContainer
výše:
parameters:
dsn: 'mysql:'
user: root
password: '***'
services:
database: Nette\Database\Connection( %dsn%, %user%, %password% )
articleFactory: ArticleFactory
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 díky autowiringu.
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é.
Výše uvedenou konfiguraci uložíme do konfiguračního souboru 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__ . '/common.neon');
});
$container = new $class;
a pak jej opět necháme vytvořit objekt Nette\Database\Connection
:
$database = $container->getService('database');
$database->query('...');
Nebo místo jména služby použijeme její typ:
$database = $container->getByType(Nette\Database\Connection::class);
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.
Související články na blogu
Services don't need names
I love Nette Framework's dependency injection solution. I really do. This post is here to share this passion, explaining why I think it is the best…
Dependency Injection: úvod (1/6)
Pamatujete si na svůj první program? Netuším sice, v jakém jazyce byl napsaný, ale kdyby to bylo PHP 7, nejspíš by vypadal nějak takto: function…
Ako automaticky zaregistrovať triedy do DIC
Veľa z vás to možno nevedelo, no Nette 3 má zabudované rozšírenie na automatické registrovanie tried do DI kontajnera. Takéto rozšírenie dokáže …
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…