Dependency injection === boilerplate?
- xr
- Člen | 94
Ahoj, mozno uz tato otazka bola niekde zodpovedana, kazdopadne by som si rad vypocul vase riesenia a nazory.
David na konferencii vravi, ze sa mame zbavit bazovych prezenterov… ale ako sa mam zbavit nekonecneho injektovania sluzieb? 🤷♂️
Problem:
Mam dynamicky layout, kde nie je staticky jasne, ake komponenty sa budu
vykreslovat a ake sluzby pouzivat. Preto je nutne injektovat mnohe sluzby a
tovarne na komponenty, krore sa vacsinou vobec nepouziju.
Problemy su teda dva:
- vykonovy: vytvaranie instancii sluzieb, ktore sa pocas obsluhy requestu nepouziju
- prakticky: repetitivne injektovanie sluzieb … proste nudny boilerplate
Injektovanie sluzieb robime kvoli IoC, ale zaroven nechceme, aby nam hadzalo prekazky pod nohy. Ake su patterny na riesenie tohoto problemu v Nette?
Kedze vyvojari vacsinou potrebuju v komponentach nejake zavislosti a casto ich nezaujima, aky prezenter tu-ktoru komponentu pouziva, vytvaraju plne samostatne tovarne, ktore potom injektuju do bazoveho prezenteru. Vznika potom nieco taketo (ano, plus definicia atributov prezenteru s anotaciami a use namespace a podobne, na kazdu jednu sluzbu je potrebnych zhruba 7 riadkov zbytocneho kodu):
public function injectServices(
ReactFrontRendererFactory $rfrf,
NewsletterFormControlFactoryInterface $newsletterFormControlInterface,
FrontMenuControlFactoryInterface $mf = null,
CategoryTreeControlFactoryInterface $ct = null,
InfoboxControlFactoryInterface $icf = null,
InfoTextControlFactoryInterface $infoTextControlFactory = null,
FrontBreadcrumbsControlFactoryInterface $frontBreadcrumbsControlFactory = null,
MenuFactoryInterface $menuFactory = null,
AdminPanelControlFactoryInterface $adminPanelControlFactoryinterface = null,
FrontGridControlFactoryInterface $frontGridControlFactory = null,
CustomFormControlFactoryInterface $customFormControlFactory = null,
ThemeSwitcherControlFactoryInterface $themeSwitcherControlFactory = null,
PollControlFactoryInterface $poll = null
// ...
) {
$this->categoryTreeFactory = $ct;
$this->infoboxControlFactory = $icf;
$this->frontBreadcrumbsControlFactory = $frontBreadcrumbsControlFactory;
$this->menuControlFactory = $menuFactory;
$this->adminPanelControlFactory = $adminPanelControlFactoryinterface;
// ...
}
Plus ma kazda komponenta tovaren, a vacsina vyzera takto:
protected function createComponentInfobox(): InfoboxControl
{
return $this->infoboxControlFactory->create();
}
Plus inerface a zaznam v NEON subore… 😩
A to su len komponenty layoutu, kazdy konkretny prezenter ma kopec vlastnych zavislosti.
Ake existuju postupy aplikovatelne pre Nette, ktorymi by slo riesit uvedeny problem pri zachovani IoC /Inversion of Control/ ?
Poziadavkami su:
- nizka pracnost / nizka ukecanost / ziaden bolierplate (⇒ spokojni vyvojari)
- rezolucia zavislosti v momente potreb sluzby
- nizka narocnost operacie z pohladu vypocetneho vykonu (⇒ rychlejsia odozva, spokojnejsi zakaznik, nizsie zatazenie planety)
Diky za nazory a diskusiu. 🙏
- Marek Bartoš
- Nette Blogger | 1274
nizka pracnost / nizka ukecanost / ziaden bolierplate (⇒ spokojni vyvojari)
SearchExtension (pro zkrácení neonu) + public property s inject anotací
rezolucia zavislosti v momente potreb sluzby
nizka narocnost operacie z pohladu vypocetneho vykonu
Od toho jsou factory/accessor třídy – vytvoří se jen objekt, který si pro skutečnou službu sáhne do DI v okamžiku zavolání create/get metody
Napsání interface ze kterého Nette třídu vygeneruje se ale nejspíš nezbavíš
Mam dynamicky layout, kde nie je staticky jasne, ake komponenty sa budu vykreslovat a ake sluzby pouzivat
Možná bys chtěl přetížit v presenteru
createComponent($name)
metodu a komponenty získávat z nějakého
svého manageru. Můžeš tak nahradit createComponentExample
metody, kdy example
ti přijde v $name
- xr
- Člen | 94
Diky za prve reakcie!
Pouzivame „proxy“ pattern ako accessor:
fooProxy: FooProxy(@container::callable(@Foo))
, kde
FooProxy
aj Foo
implementuju nejaky
FooInerface
. Kontajner nam tym callable
zabali volanie
$container->get*
do anonymnej funkcie, ktoru proxy resolvuje pri
prvom pouziti.
- ➕ zapuzdrenie je ok, je to lazy
- ➖ ale je to pracne nadrotovat a napisat.
Taktiez pouzivame servisny lokator na izolovane veci spolu so SearchExtension (mame vlastnu implementaciu, ale robi to to iste, akurat to ma par vychytavok naviac). Mame potom triedu, ktora vie vratit lubovolny repozitar (vracia len repozitare).
- ➕ jednoduche na pisanie, pokial pribudne nova servisa nejakeho typu netreba nic naviac robit
- ➖ ale je to redukcia na service locator 😑, ziadne zapuzdrenie
Redukciou na @inject
anotaciu z inject*
metod
neusetris nic, akurat si zadelas na problem, ked ti programator niekde
v zanorenej komponente v komponente siahne na public atribut prezenteru
$this->getPresenter()->foo->doStuff()
a potom tu
komponentu pouzijes niekde v inom prezenteri, kde nie je… a jejda!
Pouzitie @utowired
property je tiez len obchadzanie okolo
horucej kase, rovnaky efekt a zapuzdrenie docielis obycajnou metodou
$this->get(ServiceInterface::class)
v prezenteroch, akurat ti
odpadne pisanie atributov a navyse si mozes do get
metody
implementovat nieco viac ako len
$this->context->getService( ... )
.
- ➖ stale je to len service locator a ziadne IoC
- ➖ magia zvysuje barieru, ktoru musia novi vyvojari prekonat, ked sa snazia pracovat s tvojim projketom. A nie su to len novi programatori, kto ma problem s touto barierou. To je podla mna hlavna vyhoda servis lokatoru – je az primitivny.
Okrem toho mam zlu skusenost s pouzitim anotacii, takze riesenie na baze anotacii – prosim nie.
@Mabar
O pretazeni createComponent
metody viem, v kombinacii s vyssie
uvedenymi to pouzivame prave na implementaciu dynamickych komponent (t.j. metody
createComponentFoo
su realne implementovane inde nez
v prezenteri.
- ➖ cele to ale znizuje prehladnost aplikacie, pretoze nevies vlastne, kde sa ktora komponenta vytvara atd.
Namiesto toho mi napadla moznost pretazit Presenter::tryCall
a
skusit vyrobit method injection po vzore Laravelu. Ma s tym
niekto skusenosti alebo na to nejaky nazor?
Cielom by bolo, ze sa zavislsoti rezolvuju az v momente volania konkretnej
akcie alebo signalu, tym padom si mozes type-hintovat tovaren komponenty alebo
dokonca zavislosti komponenty a tovaren uplne vynechat. Ale vhladom na to, ze
vytvaranie komponenty sa iniciuje v sablonach {control foo}
, tak
si skor myslim, ze toto by sluzilo len na injectovanie sluzieb, pretoze nevies
(v mojom pripade), ktore tovarne budes pouzivat… a nebude to fungovat pri
velkom mnozstve sluzieb… 🤷♂️
- Marek Bartoš
- Nette Blogger | 1274
fooProxy: FooProxy(@container::callable(@Foo))
To mi přijde zbytečně komplikované. Nette podporuje accessor stejným způsobem jako factory. Jen napíšeš interface s get() metodou (to je jediný rozdíl od factory, která má create()) a zaregistruješ jako službu
Mame potom triedu, ktora vie vratit lubovolny repozitar (vracia len repozitare). ale je to redukcia na service locator 😑
To mi zní jako nějaký lazy manager. Dokud je to konfigurovatelné z vnější, tedy do něj přidáváš páry požadovaná třída ⇒ vracená služba v konfiguraci, tak principy IoC nenarušuješ. Samo o sobě přímé volání DIC nevadí, factories a accessories jsou založené na stejném principu
Redukciou na @inject anotaciu z inject* metod neusetris nic, akurat si zadelas na problem, ked ti programator niekde v zanorenej komponente v komponente siahne na public atribut prezenteru $this->getPresenter()->foo->doStuff() a potom tu komponentu pouzijes niekde v inom prezenteri, kde nie je… a jejda!
Nebo ti property změní na public a máš tentýž problém. Od toho je code review a statická analýza – pokud v komponentě getPresenter() nepřetížíš, aby typehint obsahoval konkrétní třídu presenteru, tak tě například phpstan seřve.
Namiesto toho mi napadla moznost pretazit Presenter::tryCall a skusit vyrobit method injection po vzore Laravelu. Ma s tym niekto skusenosti alebo na to nejaky nazor?
To zní užitečně pro action/render metody, ušetříš tím accessory. Naimplementovat se to dá, otázkou je jak bys pak kombinoval parametry, které ti Nette do metody vkládá z http requestu a jak by se to dalo z vnější konfigurovat.
Ale vhladom na to, ze vytvaranie komponenty sa iniciuje v sablonach {control foo}
Nebo když se požadavek na komponentu vyskytne v http requestu, třeba při zpracování formuláře. To pak Nette komponentu vytvoří hned po zavolání action metody. V podstatě se komponenta může vytvořit v kterýkoli okamžik běhu presenteru
Ja s oblibou pouzivam https://github.com/Kdyby/Autowired
Zdánlivě narušuje zapouzdření, jde o service locator a není lazy. Předpokládám, že o chybějících službách se při kompilaci též nedozvíš a na chybu dojde až při vytvoření presenteru.
Editoval Mabar (9. 5. 2020 12:03)
- xr
- Člen | 94
Method injection cez pretazenie tryCall ma kopec problemov, nie len poradie parametrov… Preto som to chcel nadhodit, keby sa chytil niekto, kto to uz skusal.
Nechcel by som polemizovat o rozdiele medzi service lokatorom a lazy manazerom, kazdopadne povies „chcem sluzbu podla tohoto identifikatoru“ a dostanes ju. Pokial toto pouzijes na mieste, kde dratujes zavislosti, je to samozrejme ok. Pokial to pouzijes na mieste, kde pouzivas vratenu sluzbu, je to lokator, akokolvek prezleceny. Lokator ma nevyhodu aj (zdanlivu) vyhodu, ze ti prebura vrstenie.
Dik za radu ohladom accessoru 👍 – toto jednak nepoznam a jednak som to nenasiel v dokumentacii.
Pozeral som do kontajneru, co mi taky accessor vygeneruje, a v podstate nase
riesenie je ekvivalentne – vraciame anonymnu funkciu, ktora po invokacii
vracia pozadovanu sluzbu z kontajneru. Rozdiel je akurat v tom, ze musime
rucne napisat injekciu plus par riadkov naviac na invokaciu. Aksesor moze byt
pohodlnejsi v niektorych pripadoch, vyskusam. Akurat musis zase pisat ten
interface a zaznam do konfiguraku. Navyse, cez callable si schopny predratovat
aj sluzby, ktore nie su autowired, co ten accessor asi nevie:
@container::callable(repositories.foo)
, ale nie je to samozrejme
autowiring.
Stale sa vsak bavime o prinajlepsom marginalnom znizeni boileplate kodu.
- xr
- Člen | 94
Ok, co trebars toto:
// MyPresenter.php
protected function createComponentMyForm()
{
return $this->wire(
MyServiceInterface::class,
MyOtherServiceInterface::class
)(function (
MyServiceInterface $a,
MyOtherServiceInterface $b
) {
return MyFormFactory::populate(new Form(), $a, $b, $this->localDependency);
});
}
Ak nie je zrejma ta syntax, tak
$this->wire( deps )( callable )
je mozne nahradit za
$this->wire( deps )->call( callable )
, jedna sa o draft.
Nez sa pustime do debaty DI vs Service lokator, uvedme, naco nam vlastne DI je po praktickej stranke. Aby sme mohli casti kodu izolovat a napriklad testovat. Vyssie uvedeny staticky populator je mozne perfektne izolovat, nema ziadne zavislosti okrem tych, co mu dame vo volani.
Dalsia namietka urcite bude voci tomu, ze sa presuva miesto, kde sa
zavislosti drotuju, z konfiguracie do prezenteru. Ale preco nie? Aka je
„cista“ alternativa so zachovanim lazy loadingu? Vyrobim rozhranie na
tovarnicku MyFormFactoryAccessorInterface
a necham, nech mi ho
implementuje DIC. Accessor si potom injektujem do prezenteru a v metode
createComponentMyForm
resolvujem tovaren, ktora mi zostavi alebo
naplni formular.
$this->myFormFactory->get()->populate(new Form());
Realne instancovanie zavislosti nastane pri volani metody accessoru
get
a type-hintovanie v konstruktore tovarne nam zabezpeci, ze
mame spravny typ. Pokial chceme nadrotovat nieco, co nie je autowired, spravime
to rucne v konfiguraku.
🤷♂️
Tento navrh je zobecnenim patternu, ktory pouzivam na vytvaranie formularov (kde sa drotuju tie iste sluzby dookola), tam su vsak sluzby nadratovane v kontajneri „cistou“ formou.
Kod tohoto mechanizmu je tu: https://github.com/…ireGenie.php a zakomponovanie do nette (napr. bazovy prezenter) vyzera napr. takto:
abstract class BasePresenter extends Nette\Application\UI\Presenter
{
/** @var WireGenie */
protected $wireGenie;
public function injectWireGenie(Nette\DI\Container $ndic): void
{
$this->wireGenie = new WireGenie(new Contributte\Psr11\Container($ndic));
}
protected function wire(...$args): callable
{
return $this->wireGenie->provide(...$args);
}
}
Obmedzenie platnosti len na urcite sluzby je mozne, staci do WireGenie poslat
spravne nakonfigurovany kontajner.
Nevyhodou je zrejma absencia autowiringu.
Edit: upraveny odkaz
Editoval xr (11. 5. 2020 14:57)
- Marek Bartoš
- Nette Blogger | 1274
Nevidím jaký je rozdíl vůči
$container->getByType(MyServiceInterface::class);
, kromě toho
že je to možná trošičku kratší.
V prvním příspěvku píšeš, že něco děláš kvůli IoC a pak svůj
container degraduješ na čistokrevný service locator. Není to
konfigurovatelné z vnější a při kompilaci se nedozvíš, že nějaká
služba chybí nebo koliduje s jinou.
- xr
- Člen | 94
Urcite to je service locator, z pohladu prezenteru.
Z pohladu tovarne/populatoru formu mas vsetky zavislosti zvonka,
konfigurujes/dratujes to v createComponentMyForm
metode, o ktorej
tovaren/populator nic nevie.
Samozrejme sa o problemoch s nepritomnostou sluzby dozvies az v momente
dratovania.
Na druhej strane ti odpadnu mraky balastu a samotnu tovaren/populator mozes velmi jednoducho testovat.
Oproti $container->getByType
je rozdiel v tom, ze ak chces,
mozes si obmedzit rozsah dostupnych sluzieb medziclankom, nemusis pouzivat cely
kontajner, ale pouzijes to, co nazyvas vyssie „lazy manazer“. Zalezi, ako si
implementujes wire()
metodu.
IMHO
- ❔ zachovanie IoC otazne (zalezi na uhle pohladu)
- ➕ vyhodou je mala pracnost riesenia
- ➕ a velmi jednoduche a prehladne na pochopenie
- ➕ moznost konfiguracie existuje
- ➕ testovatelnost je jednoduchsia ako v priapde tovarni vygenerovanych DI
- ➕ prezenter neriesi, odkial zavislosti tecu, ale _deklaruje_, ake sluzby sa maju pouzit
- ➕ lazy loading
- ➖ ziaden autowiring
- ➖ „maskovany“ service lokator / lazy manazer (❔)
- ➖ kontajner pri kompilacii nezisti problemy s chybajucimi alebo konfliktnymi sluzbami
Nechcem to propagovat ako univerzalne riesenie, ponukam to ako jednu z moznosti, kompromis, so svojimi vyhodami a nevyhodami. A som zvedavy na nazory a diskusiu, tak sa mozno dopracujeme k niecomu lepsiemu.
Editoval xr (10. 5. 2020 12:38)