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 dynamické a statické DI kontejnery
- jak na lazy loading služeb
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 jedno
zádrhelu: kde má Article vzít připojení k databázi, tj.
objekt třídy Connection?
Nejspíš bychom si poradili, můžeme jej uložit někde do globální
proměnné $GLOBALS['connection']. Ří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 $connection;
function __construct(Nette\Database\Connection $connection)
{
$this->connection = $connection;
}
function save()
{
$this->connection->table('articles')->insert(array(
'title' => $this->title,
'content' => $this->content,
));
}
}
Použití třídy Article se pochopitelně trošku změní:
$article = new Article($connection);
$article->title = ...
$article->content = ...
$article->save();
Ptáte se, kde tento kód vezme $connection? 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
systémový kontejner. A je mu věnovaná samostatná kapitola.
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í.
DI znamená víc psaní
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.
Co s tím? Věc má řešení. Místo ručního vytváření objektů
Article si vytvoříme továrničku, tedy funkci, která bude
objekty Article vyrábět. Když Article změní
konstruktor, upraví se jen továrnička, nic víc. A odkud onu továrničku
v našem kódu získáme? Vždyť víte… o to ať se postará někdo
jiný :-).
DI kontejner a služby
Termínem DI kontejner označujeme právě onu továrničku. Přesněji
řečeno, jde o objekt obsahující libovolné množství továrniček, každou
pro jinou službu. Co jsou to služby? Obyčejné objekty, jako třeba instance
zmíněného Connection. Jen v souvislosti s DI kontejnery jim
říkáme služby. Zřejmě to vymysleli konzultanti, kteří chtějí, aby DI
vypadalo složitě a oni mohli konzultovat.
Příkladem může být kontejner, který vytvoří objekt
Article, ale také jím požadované připojení k databázi:
class Container
{
function createConnection()
{
return new Nette\Database\Connection('mysql:', 'root', '***');
}
function createArticle()
{
return new Article($this->createConnection());
}
}
Použití by vypadalo následovně:
$container = new Container;
$article = $container->createArticle();
Výhoda je zřejmá, nemusíme se starat o to, jak Article
instancovat, to je záležitostí továrničky. 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 createConnection()
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
function createArticle()
{
return new Article($this->createConnection());
}
}
$container = new Container(array(
'dsn' => 'mysql:',
'user' => 'root',
'password' => '***',
));
$article = $container->createArticle();
Závažnějším nedostatkem je, že vždy, když požádáme o vytvoření
Article, vytvoří se i nové připojení k databázi. Tomu je
třeba zabránit. Přidáme proto metodu getConnection, která bude
uchovávat jednou vytvořenou službu pro příští použití:
class Container
{
private $parameters;
private $services = array();
function __construct(array $parameters)
{
$this->parameters = $parameters;
}
function createConnection()
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
function getConnection()
{
if (!isset($this->services['connection'])) {
$this->services['connection'] = $this->createConnection();
}
return $this->services['connection'];
}
function createArticle()
{
return new Article($this->getConnection());
}
}
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 zdrojového kódu.
Nette\DI\Container
Třída Nette\DI\Container je pružná implementace univerzálního DI kontejneru. Automaticky zajišťuje, že instance služeb se vytváří jen jednou.
Vlastní kontejnery můžeme vytvářet buď staticky, tj. poděděním této třídy, nebo dynamicky, kdy továrničky přidáme jako closury nebo callbacky.
Statický kontejner
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. Pokud nemají být dostupné zvenčí, lze jim viditelnost snížit
na protected. Všimněte si, že kontejner už má definované pole
$parameters pro uživatelské parametry.
class MyContainer extends Nette\DI\Container
{
protected function createServiceConnection()
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
protected function createServiceArticle()
{
return new Article($this->connection);
}
}
Vytvoříme instanci kontejneru a předáme parametry:
$container = new MyContainer(array(
'dsn' => 'mysql:',
'user' => 'root',
'password' => '***',
));
Službu získáme metodu getService nebo zkratkou:
$article = $container->getService('article');
// nebo rovnou:
$article = $container->article;
Jak bylo řečeno, všechny služby v rámci kontejneru se vytváří jen
jednou, nám by se ale spíš hodilo, kdyby kontejner pokaždé vygeneroval
novou instanci Article. Toho lze dosáhnout snadno: namísto
továrničky pro službu article vytvoříme obyčejnou metodu
createArticle:
class MyContainer extends Nette\DI\Container
{
function createServiceConnection()
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
function createArticle()
{
return new Article($this->connection);
}
}
$container = new MyContainer(...);
$article = $container->createArticle();
Z volání $container->createArticle() je zřejmé, že se
vytváří pokaždé nový objekt. Jde tedy o konvenci na straně
programátora.
Dynamický kontejner
Do kontejneru Nette\DI\Container lze služby přidávat i za
běhu pomocí metody addService. Továrničky lze zapisovat jako
PHP callback nebo closure. Všimněte si, že jako parametr je jim předáván
samotný kontejner, mohou tak snadno přistupovat k parametrům a jiným
službám.
$container = new Nette\DI\Container;
$container->addService('connection', function($container) {
return new Nette\Database\Connection(
$container->parameters['dsn'],
$container->parameters['user'],
$container->parameters['password']
);
});
Pokud vytvoření služby spočívá v triviálním vytvoření instance,
lze metodě addService jako druhý parametr předat rovnou název
třídy. A pokud již objekt vytvořený máme, můžeme předat
přímo ho.
Kromě metody addService máme k dispozici ještě
hasService ověřující existenci služby a
removeService sloužící k jejímu odstranění. Opět včetně
zkratek jako isset($container->connection) a
unset($container->connection).
Zmrazení
Kontejner je možné zmrazit a poté ho již nelze měnit:
$container->freeze();
$container->addService(...); // vyhodí výjimku
Rozmrazíme jej vytvořením klonu:
$dolly = clone $container;
Aliasy
Jednu službu můžeme uložit pod více názvy (aliasy):
$container->addService('alias', $container->getService('originalName'));
Pokud ovšem chceme zachovat lazy-loading, tj. kopírovat služby, které zatím nejsou vytvořené, bez toho, aniž bychom je vytvářeli, uděláme to následovně:
$container->addService('alias', function($container) {
return $container->getService('originalName');
});
Tímto způsobem můžeme kopírovat služby mezi více kontejnery:
$containerDest->addService('connection', function() use ($containerSrc){
return $containerSrc->getService('connection');
});
Kontejner je také možné klonovat. Klon obsahuje všechny služby jako vzor a pochopitelně lze do něj přidávat nové.
Meta-informace
Při uložení jakéhokoliv objektu můžeme u něj uvést doplňující meta-informace. Mezi ně patří i tzv. tagy:
$container->addService('name', ..., array(
Nette\DI\Container::TAGS => array('debugPanel' => TRUE),
));
A poté můžeme v kontejneru vyhledat všechny služby, které mají daný tag:
// vrací pole názvů služeb, tj. řetězců
$list = $container->findByTag('tag1')
Tag nemusí být je řetězec, ale může obsahovat další libovolné atributy:
$container->addService('name', ..., array(
Nette\DI\Container::TAGS => array(
'tag1' => array('priority' => 12),
'tag2' => array('...'),
),
));
$list = $container->findByTag('tag1')
// vrací pole array('name' => array('priority' => 12))
Jan Tvrdík | 10. 12. 2011, 20:13 | comment
Viz také populární vysvětlení DI od HosipLana.