Dependency injection === boilerplate?

xr
Člen | 94
+
0
-

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:

  1. vykonovy: vytvaranie instancii sluzieb, ktore sa pocas obsluhy requestu nepouziju
  2. 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:

  1. nizka pracnost / nizka ukecanost / ziaden bolierplate (⇒ spokojni vyvojari)
  2. rezolucia zavislosti v momente potreb sluzby
  3. nizka narocnost operacie z pohladu vypocetneho vykonu (⇒ rychlejsia odozva, spokojnejsi zakaznik, nizsie zatazenie planety)

Diky za nazory a diskusiu. 🙏

Marek Bartoš
Nette Blogger | 1146
+
0
-

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

F.Vesely
Člen | 368
+
0
-

Ja s oblibou pouzivam https://github.com/Kdyby/Autowired

xr
Člen | 94
+
0
-

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 | 1146
+
0
-

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
+
0
-

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
+
0
-

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 | 1146
+
+1
-

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
+
0
-

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)