21 komentářů k článku Dependency injection a metody globálního prostoru v PHP:

  1. David Grudl

    Ne vše je závislosti

    Rozhodně zajímavý článek.

    Příklad s funkcí strlen není úplně šťastně zvolený, protože pokud nějaká knihovna používá funkci pro zjištění délky řetězce v bajtech v místě, kde má být správně funkce pro zjištění délky řetězce ve znacích, je to jednoduše bug této knihovny a měl by být opraven v jejím kódu.

    Nechci zpochybněním příkladu jít proti myšlence článku – naopak: položme si otázku, co to ona závislost, z pohledu DI, je. Můžeme mít závislost na datech (jméno souboru, parametry připojení k DB atd.) a můžeme mít závislost na algoritmu. V situaci, kdy lze určitou věc řešit právě jedním jediným způsobem, nedává smysl algoritmus exponovat do podoby závislosti. Jako programátor se tedy spoléhám, že mi strlen vrací to, co má, a vnímal bych jako díru do logiky, kdyby se to dalo měnit.

    Vlastně je pro mě děsivé, že nastavením některých globálních parametrů PHP lze změnit chování strlen a že se nelze spolehnout ani na to, co vrátí strtolower('ABC').

    Tedy jakákoliv systémová funkce není závislostí. Tj. nejde o algoritmus, který by dávalo smysl uživatelsky měnit. Pokud bych na takový případ narazil, a spíš odhaduji, že jich bude málo, tak bych ho řešil podobně, jako jsi to udělal s funkcí rand().

    Tedy jednotlivě. Uvádět jako závislost celou obalovou třídu, tj. předávat si Service locator, není nejčistší řešení.

    Stejně tak volat statické metody místo globálních je z pohledu DI totéž. A jakožto finta pro změnu implementace je to nečisté, to už raději nasadit Runkit. Často dokonce stačí, že třída je umístěna ve jmenném prostoru, a lze ji podstrčit jinou funkci jen tím, že ji v tomto prostoru re-definujeme. Málokdo totiž píše strlen s lomítkem. Ale to už skutečně nemá s DI co dělat.

    1. Dundee5

      Re: Ne vše je závislosti

      Hezké zamyšlení, díky!

      Souhlasím s Davidem, ne všechno je potřeba mít konfigurovatelné a nahraditelné. My jsme si zatím v práci vystačili se třídami DateTimeFactory a Oraculum, tedy s obálkami nad funkcemi, které pracují s nějakým globálním stavem.

  2. Mastodont

    Zrušte božství DI

    Dobrý den,

    úplně by stačilo přestat považovat DI za nové, nedotknutelné božstvo, kterému se díky Heverymu a dalším výtečníkům nyní všichni klaní :-)

    Interní závislosti nejsou vůbec ničím tragickým a dá se s nim vyžít, pokud si člověk pořádně rozmyslí architekturu. Viz Pragmatic Programmer (In fact, by reversing the Law of Demeter and tightly coupling several modules, you may realize an important performance gain. As long as it is well known and acceptable for those modules to be coupled, your design is fine.)

    … procedurální volání funkcí pro práci s databází snad nepoužívá nikdo, kdo netrpí sebemrskačskými sklony …

    Nepoužívá, ale IMHO spíš proto, že tyhle funkce se vždy volají jako součást delšího kódu, který lze převést na funkci/metodu.

  3. msx

    Podobne som kedysi postupoval ja a nevedel som o tom, že sa to tak skutočne robí

    Kedysi som sa chcel trochu zdokonaliť v OOP a chcel som prepísať jednu jednoduchú hru do C#. Problém bol v tom, že hra potrebovala grafiku a objekty hry museli s ňou byť nejako prepojené. Nechcel som robiť odkaz na „globálne“ objekty, lebo by som stratil zapuzdrenosť objektov.

    Jednalo sa o klasickú hru s červíkom. Ten musel požierať kapustičky, aby rástol a musel byť zobrazený na nejakej grafickej ploche. Spravil som dva objekty:
    1. záhradu
    2. červíka

    Tieto objekty sa ale museli niekde zobrazovať. Takže to potrebovalo nejaké prepojenie s grafickým objektom. Nakoniec som to spravil takto:

    Konštruktor objektu Záhrada dostal odkaz na grafický objekt, kam sa má zobrazovať záhrada a konštruktor objektu Červík dostal odkaz na objekt Záhrada, aby sa mal kde zobraziť.

    Celé som to preberal na jednej poradni s inými diskutujúcimi a nakoniec diskusia skončila asi tak, že sa vymykám OOP a takto by sa to určite robiť nemalo. Ako pozerám, nie som jediný, kto uvažuje podobne a som rád, že moje konečné rozhodnutie bolo správne. Nevedel som totiž, ako by sa to dalo inak spraviť lepšie.

    Samozrejme nešiel som až do takých detailov ako vytiahnutie samotného generátora náhodných čísel von. Podľa mňa až tak do detailov netreba zachádzať pokiaľ to nie je nutné a v tejto hre to naozaj nutné nebolo.

    Takže mi to nedalo a musím podporiť autora článku a nemôžem inak, len súhlasiť s jeho názorom.

    1. Aleš Roubíček

      Re: Podobne som kedysi postupoval ja a nevedel som o tom, že sa to tak skutočne robí

      Jenže předávat business objektu odkaz na jeho vykreslovací komponentu je pěkné porušení Dependency Inversion Principle.

  4. michal.kocarek

    O koze a o voze

    Podle mne článek míchá dohromady několik spolu nesouvisejících věcí. Pokusím se to trochu zasadit do kontextu.

    Nevhodné srovnání rand() a strlen().

    Autor zcela opomíjí základní rozdíl mezi těmito funkcemi, a to že rand() je nedeterministická funkce, zatímco strlen() obsahuje deterministický, přesně se chovající algoritmus. Logicky, programujeme-li stylem DI, a chceme testovat metodu využívající (aka. závislou) rand(), musíme ji nějakým způsobem mocknout, a proto je vhodné ji extrahovat.
    Extrakce je zde z důvodu umožnění testování, což je omluvitelné.

    Na druhou stranu hovoří-li autor o extrakci funkce strlen() a její pozdější náhradě za mb_strlen(), hovoříme zde o změně implementace, což nemá s testováním nic společného. Jestliže autor nějakého frameworku nemyslí v UTF-8, ale já ano, je to jiný problém, než potřeba testovat data z funkce, která má nepredikovatelný vástup.

    Balení PHP funkcí do wrapperů

    Trochu mi to připomíná Wrapper Wrapper… Ale vážně: jak jsem říkal, PHPí funkce mají (by měly mít) předepsané chování, a tím pádem není problém je v kódu využívat. Chci tím říct, že vždy budou mezi programovacím jazykem, jeho funkcemi a zdrojovým kódem existovat závislosti. Argumentace o zabalení DateTime, strlen()… možná… ale kde je ta tenká hranice, která určuje, co je ještě syntaxe jazyka, a co už je „jen“ knihovní funkce, kterou na kterou je potřeba si brát gumové rukavice. Tedy: co třeba isset(), class_exists(), new ReflectionClass()… mohli bychom dojít k tomu, že si zabalíme také volání operátoru +.

    …maximálně 30 funkcí…

    …tohle mne asi nejvíce vyděsilo. Dobrý programátor v PHP by měl kromě jiného v hlavě nosit mirror manuálu k funkcím pro práci s poli a řetězci – aby dokázal ve vhodné chvíli použit jak strlen() i array_intersec­t_ukey() a spoustu dalších. Ty funkce jsou tady proto, aby zrychlily kód, aby si člověk nemusel psát jejich implementaci znova. A jelikož jsou deterministické (chovají se vždy stejně), opravdu není nutné je balit do wrapperů.

    TL;DR

    • chceme-li testovat něco závislé na rand(), musíme to extrahovat, protože funkce se nechová deterministicky
    • extrahovat volání strlen() do wrapperu, abychom to později mohli zaměnit za mb_strlen(), je hloupost. Pokud používám knihovnu, která je na strlen(), a já ji chci mít nad mb_strlen(), toto není řešení.
    • Knihovní PHP funkce jsou zde od toho, aby se používaly. Jsou napsány přímo v C, tudíž jsou rychlé, a jsou přesně popsané, tedy vždy stejně se chovající. Je na inteligenci programátora, aby pochopil, co je potřeba extrahovat, protože jinak to testovat nelze (rand()).
    1. ...

      Re: O koze a o voze

      ad) array_intersec­t_ukey

      jelikož dnes člověk většinu dat tahá z databáze(a např. všechny DB intersect umí) tak je podle mě důležitější umět správně pracovat s DB a vytahovat z ní data než znát komplet knihovnu funkcí PHP

    2. v6ak

      Re: O koze a o voze

      Přesnější než „deterministická“ by bylo „referenčně transparentní“. V Haskellu by toto použití rand znamenalo typovou chybu :)

      Jinak celkem souhlas.

    3. Jan Machala

      Re: O koze a o voze

      Naprosty souhlas s komentarem. rand(), time() a podobne extrahovat, aby se dali mokovat, zbytek knihovnich funkci zpravidla ne, protoze jsou deterministicke.

  5. reddish

    Re: Dependency injection a metody globálního prostoru v PHP

    Netýká se to úplně článku, ale rád bych třeba věděl, co byste kdo dělal v tomto scénáři:

    Mám třídu, která pracuje s bajty a má metodu toSHA1(), která vezme všechny bajty a udělá z nich SHA-1 hash. Proč bych měl této třídě (nebo metodě) předávat objekt, který z bajtů umí hash vytvořit, když jediné, co po té metodě chci je dostat string s hashem těch bytů. Nechci měnit implementaci, nechci to nijak mockovat, chci jen dostat hash. Je pak správné toto:

    public function toSHA1(SHA1MessageDigest $digest) {
        return $digest->...
    }

    a nebo toto

    public function toSHA1() {
        $digest = new SHA1MessageDigest();
        return $digest->...
    }

    ? A proč?

    disclaimer: i když jsem toho o DI četl spoustu, nijak o tom moc nepřemýšlím, když programuju, takže se stále mám co učit.

    1. David Grudl

      Re: Dependency injection a metody globálního prostoru v PHP

      Správně jsou obě, druhé přesně odpovídá zadání, první je tzv. over-engineered.

      Obdobně se použije `throw new AnyException` bez nutnosti uživatelsky předávat typ výjimky.

        1. Vojtěch Dobeš

          Re: Dependency injection a metody globálního prostoru v PHP

          Výjimka je také objekt. A nikdy si ho nepředáváme přes Dependency Injection, abychom ho mohli vyhodit.

    2. v6ak

      Re: Dependency injection a metody globálního prostoru v PHP

      Jsou chvíle, kdy je legitimní mít možnost předat hashovací funkci. Třeba u přihlašování uživatelů (určité komplikace teď nezmiňuju). Jsou ale chvíle, kdy je to blbost. Třeba tady:

      Mám API, které z nějakých dat na serveru má vrátit hash. Ano, čistě teoreticky bychom mohli brát nějaký Hasher, stáhnout data a zahashovat. Jenže v tom případě by tato metoda nebyla potřeba. Obvykle v takovém případě chceme přenést hash mezi klientem a serverem a nepřenášet celá data.

      Serveru těžko můžeme poslat implementaci Sha1Hasher nebo Md5Hasher. Máme tu sice serializaci, ale:
      * Server může běžet na úplně jiné platformě.
      * Obecně to asi nemůžeme napsat, aniž by měl klient nepatřičný přístup k serveru či naopak.

      Prakticky tak je v tomto případě asi nejlepší přímo v protokolu definovat množinu (klidně jednoprvkovou) hashovacích funkcí. Samozřejmě naprázdnou a konečnou množinu :) API pak bude umět přesně toto.

  6. ondra.novacisko.cz

    Trošku mimo, ale asi k tématu

    Já tedy ve svém programování používám superglobální „services“. To je globální objekt, který registruje různé služby, které jsou definované svým rozhraním. Je to právě výhodné pro věci, které mají být jedenkrát, nebo které fungují jako „default“.

    Třídy pak mají dvojí možnost, jak services využít. Buď klasicky, jak je zde nazýváno „Dependency Injection“ očekávají předávání odkazu v konstruktoru, pak services může využít ten, který třídu vytváří… a nebo se ta třída sama zeptá na na aktuální implementaci služby…

    Aby byla představa o tom, o co jde. Tak mám tam službu pro posílání RPC dotazů, službu pro zápis do logu, službu pro otevření URL v prohlížeč, službu pro otevírání souborů, službu pro vytvoření HTTP spojení. Samozřejmě, že bych to mohl dělat tak, že bych každému objektu předal seznam objektů zajišťující služby. Nebo mu mohu předat objekt obsahující služby, no nebo mohu zařídit konfigurovatelný globální objekt poskytující služby.

    Ne vždy existuje ideální řešení.

  7. K.

    FP-DI

    Jedna technická: když můj kód závisí na triviálních funkcích, tak je přece nemusím balit do tříd, abych si je mohl předat jako závislosti. Stačilo by místo funkce ‚a‘ předat funkci ‚b‘ a aby odpovídala rozhraní si vypomoct curryingem a částečnou aplikací.

  8. ZiziTheFirst

    Není to málo, Antone Pavloviči?

    Nemůžu se ubránit dojmu, že je to překombinované. Možná je to jen nevhodně zvolený příklad. Takto přemýšlející programátor by musel abstrahovat od všech základních konstruktů používaného jazyka (co kdyby foreach začal procházet pole v nedefinovaném pořadí? Nebo if začal jinak vyhodnocovat podmínky?), což by ho při důsledné tohoto principu dovedlo k tomu, že si musí napsat jazyk vlastní.

    Co se testovatelnosti a závislostí týče, tak autor problém stejně nevyřešil, jenom odsunul jinam, a to do třídy RandomNumberGe­nerator, která se tím namísto třídy RandomPasswor­dGenerator stává netestovatelnou a nemající dostatečně popsané závislosti, protože někde uvnitř ní je zřejmě stejně funkce rand použitá.

    Souhlasím s Davidem Grudlem v tom, že použití deterministicky fungujících funkcí jazyka není zatahováním závislostí. Bral bych je za něco jako axiomy. A mít možnost měnit jejich funkci je sice myšlenka lákavá, ale podle mého si autor říká o více problémů než přínosů. Jazyky, které umožňují (i za běhu) měnit fungování svých funkcí, např. Ruby, bývají právě kvůli takto nedeterministickému chování kritizovány. Funkcím, které jsou ze své podstaty nedeterministické (jako v tomto případě – zřejmě nevhodně – zvolený rand) přibývá argument týkající se testování, ale jinak je to podobný případ.

  9. Vena

    Re: Dependency injection a metody globálního prostoru v PHP

    Podle me je nejlepsim pravidlem vzdy naslouchat testum. Pokud potrebuju zavislost injektovat, abych mohl lepe nejakou komponentu otestovat, tak pouziju DI.
    V ostatnich pripadech bych DI spise nepouzival, neni treba.

    1. Vena

      Re: Dependency injection a metody globálního prostoru v PHP

      Tim nechci rict, ze DI je vhodne jenom, kdyz testuju. Tim chci rict, ze testovani mi pomaha identifikovat zavislosti, ktere bych mohl chtit v budoucnosti nahradit za jinou implementaci.
      To jen aby bylo jasno.

  10. Nocturnius

    Pěkný článek, pěkná inspirace

    ale obecně u článků tohoto typu vždycky vnitřně pěním. Kolikrát zjišťuju, že něco co dělám zcela běžně má nějaký honosný název. To je ovšem vedlejší, hlavně mi vadí, že pak někteří berou uvedené jako jedinou správnou možnou cestu a pak je ti druzí, kteří jsou zase úplně proti. Programování obecně je hledání adekvátních řešení pro danou aplikaci, nikoliv vytváření ultimátních pravd. Ono přepspřílišné zapouzdření může mít dokonce kumulativní efekt – kopete všechny v týmu aby používali zapouzdřenou variantu Strings::instance()->nasStrLen($strin­g), která momentálně obsahuje jen return strlen($value) a po dvou letech se příjde na to, že by ji bylo třeba nahradit něčím jiným, ano sice pokud je pěkně pouzdřeno náhrada potrvá pár vteřin, na druhou stranu kolik úsilí bylo věnováno tomu že není použit standardní strlen? Což by zase nemuselo platit o md5, která bude dozajista saltována. A už se určitě objevil někdo kdo půjde do opozice…

  11. Jakub Vrána

    Nešťastné příklady

    Řešit chybu knihovny tím, že jí místo špatné funkce podstrčíme tu správnou, je absurdní. Proč knihovna rovnou nepoužívá tu správnou? To už rozebíral David Grudl.

    Příklad s rand() dává smysl trochu lepší. Už jen proto, že máme dva různé problémy: generovat náhodná čísla, což lze dělat mnoha různými způsoby ( rand(), mt_rand(), /dev/random, …), a využívat tato náhodná čísla pro generování náhodného hesla. Takže oddělit tyto dva problémy do dvou různých tříd dává smysl.

    Co mě vyděsilo, je navržený způsob testování. Opravdu chcete vytvořená náhodná hesla kontrolovat tak, že z nich prvek náhody úplně odeberete? Takže když někdo změní implementaci tak, že náhodu používat úplně přestane a místo toho bude vracet konstantní řetězce, tak nás na to test nijak neupozorní. To mi trochu připomnělo problém opravený v PHP 5.3.8.

    Myslím, že lepší testování by prvek náhody ponechalo a raději testovalo vlastnosti, které od hesla požadujeme (počet písmen a jiných znaků, entropii a tak dále).

Napsat komentář

Tato diskuse je již příliš stará, pravděpodobně již vám nikdo neodpoví. Pokud se chcete na něco zeptat, použijte diskusní server Devel.cz

Zdroj: https://www.zdrojak.cz/?p=3655