Dependency Injection je prostá a skvělá technika, která vám pomůže psát mnohem srozumitelnější a předvídatelnější kód.
Kód téměř vždy píšeme pro jiné: spolupracovníky, uživatele našich
open source knihoven nebo o pár let starší sebe sama. Abychom předešli
nepříjemným WTF momentům při jeho používání, je dobré dbát na
srozumitelnost. Ať už v pojmenování identifikátorů, výřečnosti
chybových zpráv nebo návrhu rozhraní tříd. A ke srozumitelnosti bych
přidal ještě předvídatelnost. Schválně, očekávali byste, že volání
$b->hello()
v této ukázce může nějak změnit stav zcela
nezávislého opodál stojícího objektu $a
?
$a = new A;
$b = new B;
$b->hello();
To by bylo divné, že? Jo, kdyby oba objekty byly nějak explicitně
propojeny, třeba kdybychom volali $b->hello($a)
(tj.
s argumentem $a
) nebo předtím nastavili
$b->setA($a)
, tak by mezi oběma objekty existovala vazba a dalo
by se očekávat, že $b
může něco provádět s $a
.
Ale bez toho by to bylo nečekané, nesportovní a matoucí…
Říkáte si, že to je přece jasné? Že jen blázen by takový magický kód psal? Tak se podívejte na následující příklad, který v různých obměnách najdete v řadě příruček „blog in 15 minutes with our amazing framework“:
$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();
Třída Article
reprezentuje článek na blogu a metoda
save()
nám jej uloží. Kam? Asi do databázové tabulky.
Skutečně? Co když ho uloží do souboru na disk? A pokud do databáze, tak
do jaké tabulky? K jaké databázi se vlastně připojí? Ostré nebo
testovací? K SQLite nebo k Mongu? Pod jakým účtem?
Jde o stejný případ, jako v předchozí ukázce, jen pod $a
si představte (neviditelný) objekt reprezentující databázové spojení a
$b->hello()
nahraďte za $article->save()
. Co se
však nezměnilo, je nepředvídatelnost a nesrozumitelnost kódu.
Museli bychom se podívat, jak je implementovaná metoda save()
,
abychom zjistili, kam se data ukládají. Zjistili bychom, že si šahá do
nějaké globální proměnné udržující databázové spojení. Museli bychom
pátrat dál, kde se v kódu databázové spojení vytváří, a pak bychom
teprve měli obrázek o tom, jak vše funguje.
Nicméně, i kdybychom pochopili, jak je vše provázané, byl by oříšek do toho zasáhnout. Jak třeba za účelem testování uložit článek jinam? Asi by to vyžadovalo změnit nějakou statickou proměnnou. Ale nerozbili bychom tím něco jiného?
Jaj, statické proměnné jsou zlo. Vytvářejí skryté závislosti, kvůli kterým nemáme kód pod kontrolou. Kód má pod kontrolou nás ☹
Řešení je Dependency Injection
Dependency Injection (dále jen DI) neboli zřejmé předávání závislostí říká: odeberte třídám zodpovědnost za získávání objektů, které potřebují ke své činnosti.
Budete-li psát třídu vyžadující ke své činnosti databázi, nevymýšlejte uvnitř jejího těla, odkud ji získat (tj. ze žádné globální proměnné, statické metody, singletonu, registru atd.), ale požádejte o ni v konstruktoru nebo jiné metodě. Popište závislosti svým API. Nebudete muset tolik přemýšlet a získáte srozumitelný a předvídatelný kód.
A to je vše. To je celé slavné DI.
Pojďme si to ukázat v praxi. Začneme u nešťastné implementace třídy
Article
:
class Article
{
public $id;
public $title;
public $content;
function save()
{
// uložíme do databáze
// …ale kde databázové spojení seženu?
// GlobalDb::getInstance()->query() ?
}
}
Autor metody save()
musel řešit nelehkou otázku, kde vzít
připojení k databázi. Kdyby použil DI, nemusel by nad ničím uvažovat (a
to mají programátoři rádi), neboť DI dává jasnou odpověď: pokud
potřebuješ databázi, ať ti ji někdo dodá. Jinými slovy: nic nesháněj,
ať se postará někdo jiný.
class Article
{
public $id;
public $title;
public $content;
function save(Nette\Database\Connection $connection)
{
$connection->table('articles')->insert(array(
'title' => $this->title,
'content' => $this->content,
));
}
}
Takže aplikace principů DI znamená jen to, že jsme předali
$connection
jako parametr metodě? Jako vážně? Ano.
Jako vážně.
Užití třídy Article
se pochopitelně nepatrně změní:
$article = new Article;
$article->title = ...
$article->content = ...
$article->save($connection);
Díky této změně je nyní z kódu naprosto zřejmé, že se článek uloží do databáze, a taky do které databáze.
Řešení pomocí DI tak přestavuje win-win situaci: autor třídy Article nemusel řešit, kde objekt-databázi sežene, její uživatel nemusel pátrat, kde ho programátor sehnal. Z kódu je nyní zřejmé, že článek se uloží do databáze a lze velmi snadno nechat jej uložit do databáze jiné.
Můžete ale přijít s celou řadou námitek. Kde se třeba vezme
v posledním příkladu proměnná $connection
? DI opakuje: „ať
se postará někdo jiný“. Databázové spojení zkrátka dodá ten, kdo
zavolá uvedený kód.
Nojo, ale teď to vypadá, že používání DI značně zkomplikuje kód,
protože kvůli vytvoření instance Article
musíte uchovávat a
předávat databázové spojení. Navíc časem může ve třídě
Article
vzniknout potřeba nějaká data formátovat a v souladu
s DI bude potřeba předávat ještě další objekty. Komplikovalo by nám to
například kontrolery:
class ArticlePresenter
{
function __construct(Connection $connection, TextFormatter $formatter, ...)
{
$this->connection = $connection;
$this->formatter = $formatter;
...
}
function createArticle()
{
return new Article($this->connection, $this->formatter, ...);
}
}
Když bude mít presenter co do činění s dalšími podobnými třídami
jako je Article, bude mít haldu závislostí. Ba co víc, Article
by měla projít refaktoringem, kdy nahradíme databázi obecnějším
úložištěm IArticleStorage
, nebo jí zodpovědnosti za
ukládání sebe sama úplně zbavíme a delegujeme to na novou třídu
ArticleRepository
. To by znamenalo upravit aplikaci na mnoha
místech; přinejmenším všude, kde se vytváří instance
Article
. Co s tím?
Elegantní řešení jsou továrničky. Místo ručního (tj. operátorem
new
) vytváření objektů Article
si je necháme
vyrábět továrničkou. A místo všech závislostí třídy
Article
si budeme předávat jen jeho továrničku:
class ArticleFactory
{
function __construct(Connection $connection, TextFormatter $formatter, ...)
{
$this->connection = $connection;
$this->formatter = $formatter;
...
}
function create()
{
return new Article($this->connection, $this->formatter, ...);
}
}
Původní ArticlePresenter
se nám nejen krásně zjednoduší a
zároveň bude jeho API lépe vystihovat podstatu, tedy že ArticlePresenter
nepotřebuje žádnou databázi, chce prostě jen pracovat s články.
Z takového refaktoringu má člověk vyloženě dobrý pocit:
class ArticlePresenter
{
function __construct(ArticleFactory $articleFactory)
{
$this->articleFactory = $articleFactory;
}
function createArticle()
{
return $this->articleFactory->create();
}
}
V praxi se ukazuje, že každá třída mívá jen několik málo závislostí, které ji předáváme. Důsledné používání DI tak navzdory obavám kód nijak nekomplikuje a výhody, jako je srozumitelnost, jednoznačně převažují nad tou troškou psaní navíc v konstruktorech.
Lze také namítnout, že netransparentní chování původní třídy
Article
, která článek uložila neznámo kam, vlastně vůbec
nevadí, pokud ho metoda load()
zase bude umět načíst. Vtip je
v tom, že nad kódem navrženým podle DI principu vždycky můžeme takto
fungující obálku vytvořit. Ale naopak toho docílit nelze.
Dependency Injection je technika z rodiny Inversion of Control (IoC), do které patří i Service locator. Ten bývá zmiňován jako jakési zlé dvojče. Proč se mu vyhnout si řekneme v dalším článku DI versus Service Locator.
Článek volně vychází z dokumentace Dependency Injection na stránkách Nette Framework a byl aktualizován 24. 9. 2012.
Řada lidí namítala, že zvolený příklad je nevhodný, že by
třída Article
neměla být takto provázaná s databází. Zcela
s nimi souhlasím a o to víc mi zvolený příklad vyhovuje. Ukazuje totiž,
jak DI mimoděk upozorňuje na chybný návrh, protože zdůrazňuje vazby,
které byly dříve skryté.
Všechny části:
- Co je Dependency Injection? (právě čtete)
- Dependency Injection versus Service Locator
- Dependency Injection a předávání závislostí
- Dependency Injection a property injection
- Dependency Injection versus Lazy loading
Komentáře
Peter #1
Pokud budeme všude důsledně aplikovat DI, nestává se metoda getContext() zbytečná? Například použití zde je z vnějšího pohledu dost překvapivé: https://api.nette.org/…rol.php.html#105 ; stejně tak i použití getContext() uvnitř vlastních presenterů či controlů. To bychom pak mohli každému objektu dát celý DI kontainer a ať se v tom prohrabe… nebo ne?
David Grudl #2
#1 Petere, Ano, metoda getContext() je kontaproduktivní, protože jejím použitím třída své závislosti zamlžuje, dalo by se říci tají. Framework směřuje k jejímu úplnému vymýcení, ale kvůli zpětné kompatibilitě a zatím malému pochopení DI mezi programátory tam ještě nějakou dobu strašit bude.
knyttl #3
Já asi měl pořád za to, že smyslem Dependency Injection je modularita a nezávislost.
Na těch příkladech to ale zřejmé není, naopak, ukládání je závislé na Nette\Database\Connection. Z toho, jak chápu DI, by spíš ukládání mělo být zajišťováno přes interface jako třeba ISavingService, pro které by existovala implementace pracující s Nette\Database\Connection.
Už třeba jen proto, že bych mohl chtít nějaký MockConnection pro testování.
Žiju v přesvědčení, že klíčové je to, abych mohl vzít kód a použít ho bez omezení někde jinde.
Borek Bernard #4
Samostatný článek o DI rozhodně neplánuji :), ale něco jako následující kód IMO to nejdůležitější zachycuje:
https://gist.github.com/2025561
Borek Bernard #5
#3 knyttle, To už je dobrý design, DI v článku ale skutečně DI je :)
David Grudl #6
#3 knyttle, jak píše Borek, tohle skutečně s DI nesouvisí. Ale díky moc za komentář, pomohl zase o něco víc ozřejmit, proč mají programátoři s DI takový problém. Vidí v tom něco komplexnějšího (dobrý design, testovatelnost), než to ve skutečnosti je, a tudíž z toho mají zbytečné obavy. Proto taky tento článek na vše kromě DI víceméně kašle. K dobrému návrhu vede cesta po menších krůčcích.
Borek Bernard #7
#6 Davide Grudle, Co říkáš na ten Gist? IMO tam to nejdůležitější je a příklad se zbytečně nezanáší doménovými objekty, drobnou kontroverzí kolem persistence apod., což odvádí pozornost.
David Grudl #8
#7 Borku Bernarde, příklad použití DI
withDI(IoCContainer::getInstance(...))
porušuje DI 😉knyttl #9
#5 Borku Bernarde, #6 David Grudl Oukej, já jsem asi omezený tím dobrým designem a nevidím mezistupně 🙂
knyttl #10
Jen mě napadá, že by možná stálo za to v článku rozvést, co ještě tedy DI je a co už není.
Borek Bernard #11
#8 Davide Grudle, Ten Gist je celá aplikace a závislost se někde vytvořit musí (komentář je patrně zavádějící, „příklad použití“ ty dva řádky nevystihuje moc dobře). Článek malým Gistem nenahradíš, bylo to spíš pro představu, že bych si vysvětlení DI uměl představit i na jednodušším příkladu.
Borek Bernard #12
#9 knyttle, #10 knyttl DI je pouze o předávání závislostí. Příklad patrně mohl být jednodušší.
Jenda #13
Podle diskuse to vypadá, že to všichni chápou, já teda asi budu ten jediný člověk co to nepochopil.
V tom prvním případě mi přijde dobré, že Article sám se stará o to kam se ty články budou ukládat, pokud se třída Article rozhodne, že články bude ukládat do databáze, tak je uloží do databáze, pokud se rozhodne, že je bude ukládat na disk, tak si je uloží na disk. Nikomu po tom přece nic není.
Okolí od něho chce jenom uložit článek a nic jiného. Proč by se mělo starat o to kam ten článek uloží a předávat mu připojení do databáze? Až se jednoho dne rozhodnu předělat ukládání místo do databáze na disk, tak budu muset procházet všechny místa, kde se ta třída používá a měnit vytváření instance?
Navíc ta cache, vždyť okolí chce vytáhnout (nebo uložit) článek proč by se mělo starat jestli k tomu třída Article používá cache, o to by se neměly starat, ať si to zařídí ten Article sám.
Mně teda přijde ten první příklad jako nejlepší.
Jakub Vrána #14
Ve Phabricatoru (něco na způsob GitHubu používané ve Facebooku) jsem řešil tuto úlohu:
Na mnoha místech se zobrazují odkazy na soubory v repozitářích. Tyto odkazy mají nějaké svoje závislosti (např. z repozitáře potřebují získat jeho absolutní cestu), takže je vyrábí třída, které se tyto závislosti předávají. Odkazy se občas zobrazují s nějakými dalšími informacemi v tabulce, to řeší další třída, které se tyto závislosti taky předávají a ona je posílá dál. Podle mě učebnicové DI.
No a já jsem chtěl všechny odkazy upravit tak, aby se spolu s nimi zobrazoval i odkaz pro jejich přímou editaci (Nette používá protokol
editor:
). Jenže editační protokol si každý uživatel může nastavit podle svého (aby se např. dal přímo používat protokoltxmt:
nebo abych si mohl nastavit cestu k repozitáři na lokále). Takže ta třída má další závislost – buď jí musím předat tuto konfiguraci nebo celého uživatele, ze kterého si to vytáhne sama.Předání této závislosti jsem tedy musel přidat na všechna místa, kde se odkaz vyrábí. Když jsem v některé třídě přístup k uživateli neměl, tak jsem jí zase musel přidat závislost a na všech místech, kde se ta třída používá, tuto závislost zase předat.
Podle rady v tomto článku jsem to mohl vyřešit tak, že bych všem třídám, které budou odkazy vytvářet, předal továrničku na jejich výrobu. Tím bych si ale moc nepomohl – jednak bych kódu musel přepsat ještě víc (což bych mohl vzít jako investici do budoucna), jednak bych si ale tuhle továrničku musel probublat všemi třídami, které nakonec někde vespod odkaz vytváří.
Kolikrát jsem si říkal, jaká by to byla pohoda mít možnost v odkazové třídě zavolat prostě
Context::getUser()
, zdokumentovat to pomocí/** @uses Context::getUser() */
a při testech podstrčit testovacího uživatele pomocíContext::setUser()
.Nějak pořád nevím, čemu nerozumím. Aplikace navržená podle DI a nezdá se mi, že nějak špatně, ale kvůli jednoduché změně musím projít a změnit spousty kódu, doplnit několik metod a na správném místě je zavolat.
A výhoda? Že závislost vidím v parametru (zde v metodě, protože se používá setter injection) místo v dokumentačním komentáři. Na jinou nějak nemůžu přijít.
Davide, Honzo, další experti na DI, co dělám špatně? Rád si poslechu, že jsem to měl celé udělat úplně jinak a že jsem tím získal ty a ty výhody. Já to prostě nevidím.
Mým cílem je při dělání takovéhle změny soustředit se jen na tu změnu – tady máme třídu na vyrábění odkazů, tak k ní přidám ještě odkaz pro otevření v editoru. Hotovo. I úprava konfiguračního souboru na více než jednom řádku by mi vadila.
Kód popisované změny je i na GitHubu.
Filip Procházka (@HosipLan) #15
#14 Jakube Vráno, Jakube, vy nemáte žádný DI Container? Dle mého, by to správně mělo fungovat takto
Pokud se budeš dívat zvenku, nikoliv zevnitř, tvůj problém ti odpadne.
Když vytváříš všechno v DIC, tak je ti jedno, když třídě, která je v grafu závislostí zakopaná někde hluboko, přidáš něco dalšího. Takže si pak vždy můžu upravit závislosti třídy
Three
a předat jí další závislost.Ale to ty jistě víš ;)
Jan Tichý #16
#13 Jendo, Jendo, představ si, že v aplikaci nemáš jenom Article, ale dalších padesát entit. A teď si představ, že je budeš chtít všechny začít ukládat jinam, třeba místo databáze na disk. Budeš muset přepsat vnitřek všech padesáti entit.
Naopak pokud to budeš mít vyřešené přes nějakou obecnou storage, tak jenom do celé aplikace předáváš jinou storage (třeba něco jako FileStorage namísto DatabaseStorage). A není to právě vůbec tak, že bys musel procházet všechna místa, kde se ta třída používá, ale stačí pro to změnit jeden jediný řádek v konfiguraci aplikace. Konkrétně v konfiguraci DI kontejneru – o něm by měl být další navazující díl tohoto Davidova seriálu, ale pokud chceš už teď předbíhat, tak viz https://zdrojak.cz/…n-kontejner/
Jan Tichý #17
#14 Jakube Vráno, Jakube, přiznám se, že jsem kód Phabricatoru dosud podrobně nezkoumal, ale z toho, jak to popisuješ, na mě zablikala jedna věc:
„Předání této závislosti jsem tedy musel přidat na všechna místa, kde se odkaz vyrábí. “
Zatím bez vhledu do konkrétního kódu bych z tohohle s vysokou mírou pravděpodobnosti, že nepoužíváš důsledný inverse of control, kdy by se služba, co ti vyrábí ty odkazy, injectovala už příslušně nakonfigurovaná shora, ale že se ti ta inverze zastavila někde v půlce a že si tu konkrétní službu vyrábíš někde uprostřed aplikace na mnoha různých místech. Kdybys ji injectoval úplně shora (například z DI kontejneru), stačilo by Ti změnit její konfiguraci a sestavení jen na jednom jediném místě – v konfiguraci DI kontejneru (nebo obecně jiného místa, kde si v aplikaci služby sestavuješ), bez jakéhokoliv dopadu na zbytek aplikace.
Jenda #18
#16 Jane Tichý, Honzo, nerad bych předbíhal a o DI kontejneru moc nevím (spíš nic). Je to zásadní pro pochopení tohoto článku?
K tomu přehození storage: to nemůže být tak úplně jednoduché. Vždyť ta třída jasně říká kam to ukládá přímo v hlavičce té metody:
takže tam nemůžu nacpat žádnou jinou storage ani FileStorage ani nic jiného. Navíc všude kde se to ukládá to snad musím uvést ne? Případně do toho konstruktoru, pokud je to v konstruktoru.
Asi mi něco zásadního uniká.
Daniel Milde #19
#13 Jendo, DI Ti naopak tu změnu úložiště velmi usnadní. Pokud bys měl implementovanou logiku persistence přímo v metodě save v každé entitě (Article, User, Comment apod.), musel bys pak upravit všechny entity, aby se začaly ukládat nějakým jiným způsobem. Pokud použiješ DI a budeš mít aspoň trochu dobrý návrh aplikace (minimum je ta továrna na Article), bude to ve výsledku znamenat, že veškeré předávání závislostí (jako třeba Connection) může provádět DI kontejner. Nikde v kódu nebude natvrdo napsáno $article->save($connection). Změna úložiště Article pak bude znamenat jen změnit jeden řádek konfigurace!
Pavel Kouřil #20
#18 Jendo, IMHO jde o trochu nešťastně podaný příklad. Samozřejmě by neměl Article mít natvrdo „save(Nette\Database\Connection $connection)“, nicméně nějaký interface ve stylu „IArticlePersistor“. A měl bys následně např. „ArticleDatabasePersistor“ nebo „ArticleFileStoragePersistor“.
Teprve až např. „ArticleDatabasePersistor“ by dostával Nette\Database\Connection.
PS: Tuším, že slovo „Persistor“/„Persister“ neexistuje, ale nenapadlo mě jiný lepší název pro toho, kdo vykonává persistenci. :)
Filip Procházka (@HosipLan) #21
#18 Jendo, Kdyby jsi četl článek pořádně a přečetl si i zbytek diskuze, tak bys věděl, že David vysvětluje co je to DI, nikoliv jak správně navrhovat aplikaci. Příklad je zjednodušený.
Jakub Vrána #22
#15 Filipe Procházko (@HosipLan), #17 Jan Tichý Díky, trochu se mi to ujasnilo. Ta místa, kde se odkazy vyrábí, jsou controllery, které se tady do značné míry berou jako začátek aplikace. Když by tam bylo o patro navíc a controllery by továrnu na odkazy dostávaly svrchu, tak by se uživatel nastavil na jednom místě, což by mi vyhovovalo.
Pak už by jen bylo potřeba říct, které controllery tuhle továrnu mají dostávat.
Jan Tichý #23
#18 Jendo, „K tomu přehození storage: to nemůže být tak úplně jednoduché. Vždyť ta třída jasně říká kam to ukládá přímo v hlavičce té metody: save(Nette\Database\Connection $connection)“
Ano, protože je to nešťastně zvolený příklad. Zkus si místo toho představit, že v tom článku je něco jako save(IArticleStorage $storage). A že tam pak jenom vyměňuješ různé implementace tohoto rozhraní.
Jan Tichý #24
#22 Jakube Vráno, Super, přesně tak! A to místo o patro výš právě zpravidla bývá DI kontejner. A na controllery se můžeš dívat jako na jakoukoliv jinou třídu – prostě každý má své závislosti, které se do něj standardním způsobem vstřikují jako kamkoliv jinam.
Jenda #25
#20 Pavle Kouřile, #23 Jan Tichý Snad to začínám chápat. Díky za vaše komentáře. Vypadá to, že bez toho DI kontejneru se neobejdu, tak snad David napíše ten článek brzo.
Jinak podle mé zkušenosti se nejčastěji mění místo ukládání jenom jedné entity a nikoliv všech. Sice se tím straší mladí programátoři, ale nikdy jsem nezažil, že by se aplikace migrovala z MySQL do Postgres. O dost častěji jsem zažil, že se jedna entita místo do MySQL ukládá jinam. Třeba session data se ukládaly v jedné aplikaci už asi na tři různá místa.
Vašek Purchart #26
#25 Jendo, „Jinak podle mé zkušenosti se nejčastěji mění místo ukládání jenom jedné entity a nikoliv všech.“
To opět není pro DI problém – prostě zvenčí v konfiguraci do jedné třídy, která se stará o ukládání článků (třeba ArticleRepository) strčím FileStorage, do ostatních budu dál strkat DatabaseStorage. Důležité je, že budu měnit opravdu jen ten konfigurák a ne přepisovat cokoli v aplikaci.
Zároveň můžu mít libovolné množtví instancí FileStorage, které jsou opět libovolně nakonfigurované (jedna ukládá třeba do /sessions, druhá do /data) a můžu se rozhodnout, kterou kam předám – bez toho aby to výslednou třídu nějak zajímalo. Nic mi nebrání mít i více připojení do databáze, což se občas uvádí jako příklad, ale to je zhruba potřebné stejně často jako migrování z mysql na postgres.
K. #27
Tharos #28
#27 K., DI je plně kompatibilní s funkcionálním programováním, není vázané na nějaký konkrétní objektový model. DI je obecný princip, který lze aplikovat leckde.
Petr #29
V php uz delsi dobu nedelam, ale tento clanek se mi velice libi.
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.