Komentáře k článku
Dependency injection a metody globálního prostoru v PHP

Poslední dobou se celkem intenzivně zabývám dependency injection a s ním spojenými problémy. Při zkoumání DI jsem narazil na problém, který vám zde budu prezentovat.

Poslední dobou se celkem intenzivně zabývám dependency injection a s ním spojenými problémy. Při zkoumání DI jsem narazil na problém, který vám zde budu prezentovat.
Ne vše je závislosti
Rozhodně zajímavý článek.
Příklad s funkcí
strlennení ú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
strlenvrací 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í
strlena ž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
strlens lomítkem. Ale to už skutečně nemá s DI co dělat.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.
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.
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.
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.
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_intersect_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
Re: O koze a o voze
ad) array_intersect_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
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.
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.
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.
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.
Re: Dependency injection a metody globálního prostoru v PHP
S tou výjimkou jsem to nějak nepobral.
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.
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.
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í.
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í.
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 RandomNumberGenerator, která se tím namísto třídy RandomPasswordGenerator 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.
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.
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.
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($string), 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…
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).