Předávání služeb do Presenteru pomocí DI
- jantichy
- Člen | 24
Ahoj všem, jsem si jistý, že umím obhájit, proč varianta s injectováním „systémových“ závislostí přes settery a ponechání konstruktoru zcela volného pro libovolné použití je nejlepším možným řešením. Plus i proč tahle varianta řeší daleko lépe všechny výše nastíněné problémy s autowiringem a propagací rozhraní. Je to ale na dlouho a psal bych to asi tak hodinu. Nechcete se radši sejít osobně v CurryHouse dneska na večeři nebo zítra na pozdní oběd či večeři, že bych to všechno vysvětlil podrobně osobně a sem bychom pak shrnuli jen závěr a hlavní body? Jestli ne, tak si na to udělám dneska večer čas a celé to sem sepíšu.
- jantichy
- Člen | 24
Zkusím to sem ještě jinak. Sepsal jsem si svoji představu, co by se mi líbilo od presenteru v takhle obecném frameworku, jako je Nette. Jsou to nějaké moje premisy, ze kterých od samého začátku vycházím a o kterých jsem si myslel, že jsou všeobecně sdílené a jasné, takže není nutné si je vyjmenovávat. Ale zjevně se rozcházíme už v těchto premisách, takže se pojďme pobavit nejdříve o nich:
- Aby presenter jasně deklaroval všechny svoje závislosti, ať už přes konstruktor nebo přes settery.
- Aby celý mechanizmus byl postaven tak, abych mohl v konfiguraci DI kontejneru maximálně využívat autowiring, bez nutnosti psát byť i jen jednu zbytečnou deklaraci, která by tam explicitně být nemusela.
- Čili nikde žádná magie, žádné spoléhání se na anotace, metainformace či metaznalosti (jakože třeba injectuju do všech proměnných, co mají nad sebou @inject, nebo do všech funkcí, jejichž jméno začíná na inject*()). Důsledný typehinting a explicitní vyjmenování parametrů a jejich typů přímo do deklarací funkcí (konstruktoru a setterů).
- Aby presenter využíval navenek i uvnitř sebe čistou dependency injection, vůbec nikde žádný ServiceLocator.
- Aby mi jakékoliv budoucí interní změny ve frameworku nerozbily stávající aplikaci ve smyslu funkčnosti mých presenterů.
- Aby mě jakékoliv budoucí interní změny ve frameworku nenutily sahat do mých uživatelských presenterů a cokoliv v nich měnit a přepisovat.
- Aby mě framework nenutil psát žádný zbytečný kód vícekrát, případně abych ho nemusel psát vůbec, pokud to jde udělat zároveň čistě a nemagicky.
- Aby mě framework nenutil explicitně injectovat nebo v DI kontejneru explicitně konfigurovat věci, které vůbec nikdy nevyužiju a ve své konkrétní aplikaci a přístupu ani nepotřebuju.
- Pokud jsou dvě srovnatelná řešení, jak framework postavit, přičemž programátorovi využívajícímu framework stačí u prvního jen všeobecné znalosti, zatímco u druhého by se musel nejdřív naučit nějaké framework-specific věci, mělo by být zvoleno první řešení. Případně to druhé nabídnout jako syntax-sugar alternativu.
- Neboli abych musel znát co nejméně okolností a specifičností Nette pro to, abych mohl začít presentery psát a používat.
- Abych se nemusel například nutně učit o tom, že existuje nějaký setContext(), než začnu psát presentery, ale vše fungovalo i v případě, že o něm vůbec nevím a chci si své uživatelské závislosti injectovat nějak jinak, jak jsem sám zvyklý z jiných knihoven.
- Abych nemusel například vědět, že zrovna u presenterů nesmím sáhnout na konstruktor, když ve všech svých ostatních knihovnách na něj sahám a používám ho pro svou inicializaci třídy.
- Pokud framework nabízí nějakou elegantnější cestu, jak něco injectovat, která je ale framework-specific (jako je setContext), tak aby tato cesta byla pouze volitelný „syntax sugar“ pro ty, kdo ji chtějí používat a zjednodušit si práci.
- Ale aby mi nenutil jediný správný způsob injektáže mých uživatelských závislostí.
- Pokud v celé zbylé aplikaci a ve všech svých knihovnách používám klasický starý dobrý constructor injection, rád bych i pro své uživatelské závislosti v presenterech používal jednotně starý dobrý constructor injection. Pokud všude jinde používám tupý setter injection ve smyslu jednoho setteru pro každou jednu závislost, měl bych mít možnost totéž dělat i v presenterech. Framework by mi ani pro jednu z těchto voleb neměl házet klacky pod nohy a vynucovat si jen jedno své správné řešení, pokud pro to nemá nějaký opravdu dobrý jednoznačný důvod (syntax sugar takovým důvodem není).
- Abych měl konstruktor k dispozici pro to, k čemu konstruktor slouží, pokud ho tak budu chtít použít. Tedy pro inicializaci dané třídy a všech věcí, které si já sám zrovna v téhle konkrétní třídě potřebuju inicializovat.
- Aby byly eliminované věci, na které musím myslet, protože se to jinak rozbije, a které se velice často zapomínají. Například volat parent::__construct() v konstruktoru. Framework by si měl být vědom toho, že tyhle chyby se dělají poměrně často, a systémově jim předcházet, aby se to vůbec dělat nemuselo.
V první fázi, než se vrátíme ke konkrétní implementaci, bychom se tedy měli pobavit o tom, jestli jsou všechny tyto požadavky legitimní a správné. Z mého pohledu ano, prosím případně o protinázor.
A následně se teprve pobavit, která varianta všechny tyto požadavky splňuje. A tady předesílám, že IMHO je beze zbytku, bez jakýchkoliv opomenutí, nesystémovostí či prasáren splňuje jen a pouze varianta „systémové závislosti přes settery, konstruktor nechat volný“.
Editoval jantichy (2. 3. 2012 16:41)
- David Grudl
- Nette Core | 8227
Všechny tyto požadavky jsou legitimní a správné. (když si odmyslím některé formulace, které navádějí k určitému řešení.) A právě proto bych „systémové závislosti přes settery, konstruktor nechat volný“ nebral zcela automaticky.
Zkus prosím udělat jednu věc, je to úžasný trik. Udělej ďáblova advokáta a obhaj si sobě nebo někomu jinému řešení opačné. Nebo alespoň se zkus zamyslet nad jedním:
Pokud framework jde cestou přímého otevřeného řešení v podobě konstruktor injection a vidíš, že presenter má volný konstruktor, nepřijde ti WTF, když posléze zjistíš, že ve skutečnosti závislosti má, jen se tváří, že nikoliv?
Nebo naopak vidíš, že závislosti získává přes konstruktor, a dozvíš se, že existuje i metoda setContext(), která řeší problém, kdy sice přepisovat konstruktory lze, ale je to děsná pruda. Nikdo tě nenutí ji používat, je to jen užitečná věc řešící tvůj problém. Věc navíc.
(neříkám tím, že se kloním k tomuto řešení, jen je mi trošku nepochopitelné samozřejmé preferování dle mého více WTF přístupu)
- David Grudl
- Nette Core | 8227
dundee napsal(a):
Ani toto není čisté řešení. Porušuješ tím pravidlo předávání přímých závislostí a Demeterův zákon.
Ale kdeže… Tohle nemá se Service Locatorem vůbec nic společného, to je korektní řešení. Uvnitř není žádný registr nebo list.
Tohle:
class A
{
function __construct(B $b)
{
$this->b = $b;
}
}
je z pohledu DI ekvivalentem s
class A
{
function __construct(A_Deps $deps)
{
$this->b = $deps->b;
}
}
class A_Deps
{
function __construct(B $b)
{
$this->b = $b;
}
}
- jantichy
- Člen | 24
Udělej ďáblova advokáta a obhaj si sobě nebo někomu jinému řešení opačné.
To od začátku dělám a právě jsem nakonec sám sebe uargumentoval, že řešení s volným konstruktorem je lepší ;-).
Pokud framework jde cestou přímého otevřeného řešení v podobě konstruktor injection a vidíš, že presenter má volný konstruktor, nepřijde ti WTF, když posléze zjistíš, že ve skutečnosti závislosti má, jen se tváří, že nikoliv?
Chtěl bych se ujistit v jedné věci, jestli mi jenom něco neuniká. Rozdíl mezi dvěma základníma řešeníma je v tom, že:
- v jednom se viditelně přes konstruktor injectují „systémové“ závislosti a méně viditelně přes settery injectují „uživatelské“ závislosti.
- v druhém se viditelně přes konstruktor injectují „uživatelské“ závislosti a méně viditelně přes settery injectují „systémové“ závislosti.
Je to tak, nebo něco přehlížím? Je to tedy celé o otázce, zda zvýraznit systémové a upozadit uživatelské závislosti, nebo naopak? V čem je pak tedy druhé řešení více WTF než to první? Já to cítím přesně naopak a mám pro to racionální důvody, viz dále.
Pokud framework jde cestou přímého otevřeného řešení v podobě konstruktor injection a vidíš, že presenter má volný konstruktor, nepřijde ti WTF, když posléze zjistíš, že ve skutečnosti závislosti má, jen se tváří, že nikoliv?
Právě že nepřijde. Setter injection je pro mě zcela legitimní deklarace závislostí.
Ale pokud chci při svém programování používat jakýkoliv typ injektáže, na který jsem zvyklý, sic za tu cenu, že framework pro své účely bude „nucen“ používat setter injection, tak mi to přijde lepší, než abych byl já jako vývojář nucen používat jen jeden konkrétní dost specifický typ injektáže, protože ten druhý můj oblíbený typ se mi framework rozhodl zablokovat.
Obecně ale k těm dvěma variantám výše. Pro mě jako vývojáře jsou „systémové“ závislosti o řád méně „důležité“, protože s nimi nejčastěji vůbec nepracuji ani je tam často vůbec nepotřebuji, framework si je tam strká sám ze svých soukromých důvodů a zajišťuje, že prakticky vždycky fungují samy, takže se o ně nepotřebuji vůbec starat. Takže je nemusím vidět úplně na první pohled (mohou být v setterech). Zatímco „uživatelské“ závislosti jsou ty, se kterými neustále napřímo pracuji a definuji je. Jsou pro mě při běžné každodenní práci o řád „důležitější“. Takže bych je na první pohled (tedy v konstruktoru) vidět měl.
Je tu ještě jeden důvod, proč jsou pro mne „systémové“ závislosti méně „důležité“. „Systémové“ závislosti jsou stejné napříč všemi presentery. Takže když už na to někdy jednou narazím, tak už potom tak nějak do budoucna vím, co se injektuje do každého dalšího presenteru a nemusím se na to dívat zas a znova a mít to stále na očích na úkor „uživatelských“ závislostí. Je to prostě pořád stejné. Pokud jsem prostě už na takovém levelu, že o těchto „systémových“ závislostech chci nebo musím vědět, tak si je zjistím jednou (bez větších obtíží, protože jak jsem říkal, setter injection je naprosto legitimní způsob injektáže) a pak už je v rámci své „znalosti frameworku“ znám napořád, aniž bych je musel mít pořád na očích. Princip ovšem je, že velice dlouho, pravděpodobně většinou i napořád, o nich nic nebudu potřebovat vědět a v ničem mě to nebude jakkoliv omezovat.
Naproti tomu u varianty 1) mám na očích pořád ty samé závislosti, které se furt kolem dokola opakují, a to i když už je tisíckrát znám. Zatímco ty důležité závislosti, které bych potřeboval mít pořád na první pohled na očích, protože se mění presenter od presenteru, jsou schované někde v nějakém setteru. Navíc v praxi pak ten kostruktor bude nejčastěji jen v předkovi, takže při pohledu na tu výslednou třídu presenteru nevidím na první pohled pořádně nic – systémové závislosti jsou schované v předkovi, uživatelské jsou schované v setterech.
Editoval jantichy (1. 3. 2012 20:52)
- Patrik Votoček
- Člen | 2221
David Grudl napsal(a):
Všechny tyto požadavky jsou legitimní a správné. (když si odmyslím některé formulace, které navádějí k určitému řešení.) A právě proto bych „systémové závislosti přes settery, konstruktor nechat volný“ nebral zcela automaticky.
Souhlas. (po přečtení těch románů nemám sílu napsat něco víc :-D)
jantichy napsal(a):
Setter injection je pro mě zcela legitimní deklarace závislostí.
O tom že je setter injection legitimní způsob injektáže se nepřu. Ale pro mě je setter injection signálem nepovinné / volitelné závyslosti. Tj. takové kterou když nepředám nic se nestane (pokud nenastavím cache tak se nic nepokaka).
Proto mě jako jedinná správná cesta™ připadá první varianta (milión parametrů konstruktoru). Ale jelikož jsme se shodli že to je to co nechceme tak je nutné přijít s kompromisem.
Celá diskuse se ve výsledku točí v kruhu a to z důvodu že každý má na prioritu „systémových“ a „uživatelských“ závyslostí jiný názor.
Celkem by mě naštvalo kdyby mě začali failovat testy jenom kvůli tomu že jsem zapoměl někde zavolat nějaký ten setter (což jak už jsem popsal výše je pro mě nepovinná závyslost).
Řešení s tím definovat jako standard vytváření presenterů pouze přes PresenterFactory tak trochu nabourává:
- Neboli abych musel znát co nejméně okolností a specifičností Nette pro to, abych mohl začít presentery psát a používat.
- Abych se nemusel například nutně učit o tom, že existuje nějaký setContext(), než začnu psát presentery, ale vše fungovalo i v případě, že o něm vůbec nevím a chci si své uživatelské závislosti injectovat nějak jinak, jak jsem sám zvyklý z jiných knihoven.
- Abych nemusel například vědět, že zrovna u presenterů nesmím sáhnout na konstruktor, když ve všech svých ostatních knihovnách na něj sahám a používám ho pro svou inicializaci třídy.
- paranoiq
- Člen | 392
definovat, že Presenter může být zkonstruován pouze v PresenterFactory je ještě horší závislost než PresenterDependencies. úkol, který to plní je naprosto stejný, ale narozdíl od PresenterDependencies je tato závislost skrytá
proti sobě stojí řešení:
- vše v konstruktoru (otravuje uživatele;
parent::__construct()
; nabourává funkční aplikace při změně) - systémové v konstruktoru (otravuje uživatele ještě víc;
parent::__construct()
; nabourává funkční aplikace při změně) - systémové v setterech (otravuje uživatele při testování; zboří testy při změně; zboří uživatelskou PresenterFactory při změně)
- systémové v PresenterDependencies v konstruktoru
(
parent::__construct()
; zboří uživatelskou PresenterFactory pouze v případě, že upravuje systémové závilosti)
poslední řešení má podle mého malý WTF faktor, nejmenší BCBreak faktor a je i rozumě testovatelné (a jak víme BCBreak faktor je u frameworku důležitá věc)
- David Grudl
- Nette Core | 8227
Přesto, že způsob 4 je asi nejrozumnější, volil bych možná pragamtičtější způsob 3. Váhám vlastně jen mezi těmito dvěma.
- duke
- Člen | 650
V Nette Framework Roadmap se pro verzi 2.1 vyskytuje mj. bod:
- nastínění cesty, jak nahradit u presenterů dědičnost za kompozici
Možná by nebylo úplně od věci zaměřit se na tento bod a vzít v úvahu, jak bude vypadat předávání závislostí v této nastíněné podobě, aby se pak nemuselo všechno radikálně měnit.
- juzna.cz
- Člen | 248
jantichy napsal(a):
[závislosti] … se dají rozdělit na dvě celkem odlišné skupiny:
- systémové závislosti …
- uživatelské závislosti …
Ja mam jeste 3.typ (dalo by se rici pod-typ systemovych), a to zavislosti spolecne pro mou aplikaci. Kdyz programuju konkretni presentery, tak jsou pro me ekvivalentni tem systemovym. Z pohledu Nette jsou to ale zase uzivatelske zavislosti, protoze nejsou soucasti Nette.
Zde mi pak prekazi final setContext
ktery vnucuje ekvivalenci
mezi systemove a Nette zavislosti. Ackoliv je tam ten
final „spravne“ (rozhodne lepe nez kdyby tam nebyl), tak mi tam
vadi a musel jsem si ho vyhodit, abych si v BasePresenteru mohl
udelat vlastni systemovy setContext
.
- David Grudl
- Nette Core | 8227
Sepsal jsem o Dependency Injection a předávání závislostí článek, který se v závěru dotýká i problému setter vs. konstruktor.
S odstupem dvou měsíců jsem na věc mírně změnil pohled. Honzovo řešení (nyní v dev verzi) se mi nelíbí stále, jen lépe chápu proč :-)
Má totiž jeden zásadní problém. Konstruktor potomka je volán nad
neinicializovaným objektem. Volá se prostě v situaci, kdy ještě nebyly
předány závislosti metodou setContext. Totéž platí o konstruktoru třídy
UI\Presenter
, ten také ještě nemá své závislosti a
hypoteticky ji to omezuje v tom, co může dělat.
Toto vnímám jako objektivní nedostatek, který přebíjí
subjektivnější příjemnost používání metody
__construct
.
V zásadě tedy jsou možnosti:
- použít pomocnou třídu PresenterDependencies, na čemž jsme se dohodli s Vaškem a on připravil pull
- použít pro uživatelské závislosti metodu inject()
- použít obojí, nijak se to totiž nevylučuje
Uvědomil jsem si, že pokud bych šel cestou c), určitě bych vždycky
závislosti předával funkcí inject(), protože psát strašidelné
__construct(Nette\Application\UI\PresenterDependencies $dp, ...)
se
mi nebude chtít, nebudu-li muset.
Zároveň bych všechny maximálně tlačil do používání metody inject(), protože se tak předejde evergreenu „- nefunguje mi to. – a zavolal jsi parent::__construct()? – ne“.
Ve výsledku se tak zpětně ptám sám sebe, jestli má PresenterDependencies smysl.
- Jan Jakeš
- Člen | 177
Mně tedy přijde PresenterDependencies
takové nemastné,
neslané a nepříliš čisté řešení. V podstatě to není DI, je to
jakýsi ServiceLocator a zase je to někajá výjimka z obecného principu.
Pokud chceš jedno ze tří nabízených řešení, metoda inject()
je podle mě o řád lepší, ale zde opravdu
PresenterDependencies
ztrácí smysl, takže variantu c. bych
vyřadil.
Jinak ještě obecněji, jak říká tvůj článek:
Ukazuje se, že dokonalý obecný mechanismus asi ani neexistuje. Možná by nebylo od věci zkusit nějaký, byť za využití PHP magie, vymyslet.
A metoda inject()
už je nějaký mechanismus, možná by bylo
dobré (když už se zavádí nový mechanismus/konvence), zamyslet se nad tím,
jaké mechanismy lze v PHP naimplementovat a zvážit i:
Dovedu si proto představit, že by vzniklo nové čisté řešení využívající nějaké PHP magie uvnitř třídy, která by ušetřila psaní režijního kódu, elegantně exponovala závislosti a předávala je do proměnných. Ty by mohly být označené třeba anotací @inject, nicméně šlo by o anotaci určenu pro tuto vnitřní implementaci, nikoliv o hint pro DI kontejner. Efekt by to mělo ve chvíli, kdyby se z toho stala obecněji uznávaná konvence, jinak to bude jen magie.
Z jakéhokoliv řešení se stane okamžitě konvence, tedy pro uživatele Nette :)
EDIT: A ještě na závěr – snad se shodneme, že Presenter opravdu nutně potřebuje zrefraktorovat, takže by se rovnou mělo trochu myslet i na tohle.
Editoval Juan (8. 5. 2012 14:59)
- David Grudl
- Nette Core | 8227
PresenterDependencies je nemastné, neslané, ale čisté řešení. Nejde totiž v žádném případě o Service locator. Presenter totiž naprosto jasně uvádí své závislosti.
Myslet na to, že Presenter potřebuje zrefaktorovat, je přesně to, na co by se vůbec myslet nemělo, protože při šikovném předávání závislostí nebude mít pozdější refaktoring na nic vliv.
- Jan Jakeš
- Člen | 177
Já nevím, mně pořád přijde, že v PresenterDependencies je jistá míra utajování závislostí. Ano, presenter jasně uvádí své závislosti, ale jednou z jeho závislostí je „třída obsahující závislosti“. Z pohledu DI je to asi OK, ale PresenterDependencies jako jakýsi sklad závislostí ve mě evokuje označení ServiceLocator. I když je to ve skutečnosti asi tedy „čisté“ řešení, tak se mi z těhle důvodů moc nezamlouvá.
- David Grudl
- Nette Core | 8227
Klidně ho můžeš označovat service locator, na „označení“ ještě není nic špatného :-)
- Ascaria
- Člen | 187
Docela se to tu zvrhlo jak tak koukám :) Tak taky přidám trochu do mlýna.
Myslím, že nejlepší by bylo vydat se cestou setterů po vzoru C# pro všechno a v konstruktoru předat jen to co fakt nejde předat jinde, což není nic takže konstruktor bude vždy prázdný, tudíž programátor nebude mít důvod nikdy v ničem použit konstruktor na nic jiného, než na nastavení výchozích hodnot což pro objekty bude asi většinou null, který jak jsem tu někde četl, se moc nelíbí.
V C#ku to vyřešili jakýmisi „setter konstruktory“:
Trida a = new Trida() {
Property1 = 0,
Property2 = 1
};
Myslím že to je docela geniální nápad, protože to je takový „lepší konstruktor“. V PHP sice takový zápis nejde, ale je zcela ekvivalentní:
$a = new Trida();
$a->Property1 = 0;
$a->Property2 = 1;
Potom už stači dodržovat jedno nepsané pravidlo – v konstruktoru (pokud se rozhodneme ho použit a který bude muset být bezargumentový) neřešit žádnou logiku.
Četl jsem tu, že někomu se nelíbí, že pak bude muset ty vlastnosti tříd kontrolovat, jestli je v nich opravdu daný objekt – to je podle mě nutné zlo, a taky to nejmenší zlo. A že bude potřeba to v každé metodě zvlášť kontrolovat a zasere to kód? Není nic jednoduššího, než nechat kontrolu na gettery:
class A
{
private $property;
public function getProperty()
{
if(null === $this->property)
// Lazyload, nebo výjimka
throw new Exception();
return $this->property;
}
public function setProperty(Trida $t)
{
$this->property = $t;
}
public function doSomething()
{
// Jelikož je P v Property velký, použije se getter
// a případně se vyhodí výjimka a sama metoda nemusí
// kontrolovat existenci objektu v property
try {
// nebo prostě $this->getProperty() jako je tu zvykem
$chciHodnotu = $this->Property;
} catch(Exception $e) {
$this->flashMessage('Smůla: '.$e->getMessage(), 'error');
}
}
}
Snad jsem na nic nezapoměl… Zkrátka – konstruktor by měl sloužit jen pro základní nastavení třídy bez závislosti a při přístupu k property kontrolovat jestli je závislost předaná.
V jquery jsem viděl na začátku souboru v komentáři například:
/*
* jQuery UI Sortable 1.8.16
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Sortables
*
* Depends:
* jquery.ui.core.js
* jquery.ui.mouse.js
* jquery.ui.widget.js
*/
Takhle krásně se pak může v dokumentaci třída přiznat k závislostem a programátor nemusí bádat co potřebuje.
Tak snad si to někdo přečte a okomentuje proč uvažuju blbě :)
Editoval Ascaria (17. 5. 2012 10:35)
- Patrik Votoček
- Člen | 2221
Problém setter injection je ten že když závyslost zapomenu nastavit dovím se o tom až když jí bude něco potřebovat (což nemusí třeba nikdy nastat). A pak se problém objeví v nejméně vhodnou chvíli.
- Ascaria
- Člen | 187
@Patrik Votoček: To je bohužel nutné zlo, opakem (zase dobro) může být, že nemusíš předat závislost, když víš, že jí třída na akci nebude potřebovat. V každém případě je to podle mě lepší, než „konstruktorová verze“, kde se při každé změně ve fw budou muset měnit konstruktory i v app a je to čistší než výše uvedená verze „nette přez konstruktor, programátor přez settery“, protože je to jednotný.
Mám z té debaty tady pocit, že hledáte křišťál, který neexistuje.
Setters are the way! :)
Editoval Ascaria (18. 5. 2012 9:46)