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.

Rychlý úvod do dependency injection

Dependency injection je princip, který nám zjednodušeně řečeno říká, že cokoli třída ke svému fungování potřebuje, má vyžadovat po svém okolí, místo toho, aby si to získávala sama. V důsledku to znamená, že třída by vůbec neměla přijít do kontaktu s globálním prostorem – pokud dostane to, co potřebuje, udělá svoji práci, ale nikdy si nic sama neshání. Pokud tedy potřebuje nějakou jinou service, pak o ní požádá v konstruktoru (jako povinný parametr) nebo alespoň pomocí setteru nebo public property. Výsledkem je zjednodušení aplikace, lepší čitelnost návrhu a lépe testovatelný kód. Pokud se o dependency injection chcete dozvědět více, doporučuji vám seriál Vaška Purcharta – Jak na dependency injection, který vyšel zde na Zdrojáku.

Globální prostor

V globální prostoru se nachází všechno možné – globální proměnné, údaje o čase, lokalizace, $_GET, $_POST atd. Pokud tedy některá z našich tříd pracuje přímo s proměnnou $_GET, vnášíme tím do systému neočekávatelné závislosti. Třída se bude chovat různě podle toho, co proměnná $_GET zrovna obsahuje. Nicméně se bez studia jejího kódu nedozvíme, proč se třída občas chová jinak. Pokud naopak třída bude vyžadovat předání pole s parametry místo toho, aby si je získávala sama, bude se vždy chovat podle toho, jaké parametry jí zrovna předáme, a tedy zcela podle očekávání. Pokud se pak rozhodneme, že místo proměnné $_GET budeme pracovat s proměnnou $argv, nemusíme implementaci třídy měnit.

Globální prostor ale obsahuje ještě jednu věc, a sice funkce definované přímo v jádru PHP, například strlen() nebo rand() a také jazykové konstrukty jako include nebo array. Měli bychom se jejich používání také vyhnout? To je otázka, které se chci v tomto článku věnovat.

Odkrývání závislostí

Pokud některou z těchto globálních funkcí použijeme uvnitř třídy, pak jsme podle všeho porušili pravidlo, podle kterého by naše třídy neměly své závislosti skrývat. Ukážeme si to na příkladu.

Mějme zcela triviální generátor hesel.

class RandomPasswordGenerator {
    public function generateRandomPassword() {
        return 'abc' . rand(1, 9);
    }
}

Generovaná hesla sice nejsou příliš bezpečná, ale jinak vypadá příklad v pořádku. Na první pohled není porušení DI principu patrné. Ukažme si ale protipříklad. Představme si, že v PHP žádná funkce rand() vůbec neexistuje a nikdy neexistovala. Místo ní nám tvůrci PHP dali k dispozici třídu, která slouží ke generování náhodných čísel. Příklad by pak vypadat asi takhle:

class RandomPasswordGenerator {
    public function generateRandomPassword() {
        $random = new RandomNumberGenerator();
        return 'abc' . $random->getRandomInteger(1, 9);
    }
}

Tady už je porušení DI mnohem lépe vidět. Co kdybychom se totiž rozhodli použít jinou implementaci generátoru náhodných čísel například pro účely testování? Typickým příkladem může být výměna funkce rand() za mt_rand(). Při tomto přístupu to ale není možné, museli bychom třídu přepsat.

To se dá naštěstí jednoduše opravit. Provedeme refaktoring třídy tak, aby svou závislost na generátoru náhodných čísel deklarovala ve svém konstruktoru.

class RandomPasswordGenerator {
    private $random;

    public function __construct(RandomNumberGenerator $random) {
        $this->random = $random;
    }

    public function generateRandomPassword() {
        return 'abc' . $this->random->getRandomInteger(1, 9);
    }
}

Teď už naše třída vypadá úplně jinak než na začátku. První i poslední implementace dělají úplně to samé. Poslední implementace ale dává své závislosti jasně najevo.

U generátoru náhodných čísel je problém celkem dobře vidět. V testech budeme určitě potřebovat, aby třída pracovala s nám známým „náhodným“ číslem, abychom mohli kontrolovat její výstupy. Proto vytvoříme mock třídy RandomNumberGe­nerator, v mocku upravíme metodu getRandomInteger tak, aby vracela stále stejné číslo a můžeme začít testovat. Pokud bychom použili implementaci, kdy si třída RandomPasswor­dGenerator obstarává náhodné číslo sama, byla by třída neotestovatelná.

Je to opravdu tak hrozné?

Jak je to ale s jinými metodami? Těch se problém podle mě týká úplně stejně. Mohlo by se zdát, že u nich nebudeme nikdy potřebovat používat jinou implementaci. Vždyť jsou přímo v jádru PHP, proč bychom je tedy měnili? Podívejte se ale na metodu strlen(). Ta sice je součástí jádra PHP, ale alternativní implementaci používáme každou chvíli a důvody proč jsou zcela jasné. Za pravdu mi dá asi každý, kdo někdy pracoval s externí knihovnou, jejíž třídy uvnitř používají metodu strlen(). Jakmile začnete používat multibytové kódování, přestane spousta věcí fungovat. Máte v takovém případě dvě možnosti, buď všechna místa, kde se strlen() používá, ručně přepsat a smířit se s tím, že to budete dělat pokaždé, když vyjde pro tuto knihovnu nový update, a nebo přestat knihovnu používat. Častým protiargumentem je, že to přece není tak hrozné, a že dnešní IDE dokážou nahrazení udělat za vás. Schválně jsem ale vybral příklad s metodou strlen(). Automaticky ji vyměnit za mb_strlen() je i s dnešními IDE komplikované a téměř vždy se při tom něco rozbije.

Kdyby ale třídy této knihovny nějakým způsobem jasně definovaly svoji závislost na funkci strlen(), pak jim stačí předávat jinou implementaci. V takovém případě by se jednalo jen o přepsání pár řádků v konfiguračním souboru pro DI kontejner.

Návrh řešení

Jenže co teď s tím? Jak už jsem řekl, sám si nejsem úplně jistý, jak tento problém elegantně vyřešit. Jednou z možností je „zabalit“ všechny metody, které používáme, do objektového rozhraní. Příkladem může být už zmiňovaná třída Rand. Tento přístup se mi celkem líbí, ale má jistá úskalí.

Naše třídy budou moci veřejně deklarovat své závislosti na „obalových třídách“. V případě potřeby změny si můžeme vybrat, jestli změníme už hotovou implementaci, nebo si napíšeme novou třídu, implementující stejné rozhraní a začneme místo původní instance předávat instanci nové třídy. Náš kód bude mnohem lépe testovatelný. Budeme moci kdykoli změnit implementaci a to dokonce za běhu, bez potřeby cokoli přepisovat.

Nevýhoda samozřejmě spočívá v tom, že ty třídy budeme muset napsat, což může být obrovská spousta práce. Funkcí v jádru PHP je úctyhodné množství. Na první pohled to tedy není příliš dobrý nápad. Na druhý pohled to ale není zas tak špatné. Schválně si zkuste uvědomit, kolik funkcí běžně používáte. Já jsem si takový odhad udělal a víc než 30 jich určitě nebude. Samozřejmě jich používám mnohem víc, ale většinu z nich jen zcela výjimečně. Těch, které používám, opravdu často není moc. Prvotní náklad by sice byl celkem značný, ale není to nic, co by se nedalo zvládnout.

Celý nápad se sice může zdát přitažený za vlasy, ale uvědomte si, že je to něco, co se v PHP dělá už dávno. Podívejte se na všechna ta PDO, dibi, DateTime a další. Nikdy se nejedná o nic jiného, než objektové obálky nad globálními funkcemi. A myslím, že procedurální volání funkcí pro práci s databází snad nepoužívá nikdo, kdo netrpí sebemrskačskými sklony.

Druhou možností je vložit funkce do nějaké knihovny jako statické metody. Pak bychom ke všem funkcím přistupovali pomocí statického volání. Tím sice zamlčíme závislosti našich tříd na těchto metodách, je ale otázkou, jestli nás to musí trápit. To samé se děje i v případě, že používáme globální funkce. Výhoda tohoto přístupu je v tom, že máme alespoň možnost změnit implementaci v případě, že zjistíme, že současná implementace nám nevyhovuje. Pokud bychom chtěli tento přístup použít, musíme si uvědomit, že volání statické metody nebo globální funkce je prakticky to samé. V jednom případě jsme si ale funkci definovali sami.

Jakub Tesárek je programátor který se rozhodl, že už bylo dost špagetového kódu a že to tak nenechá. Přednáší, školí, natáčí videa a jak vidíte, tak i píše o čistém kódu a co dělat, aby čistý byl.

Komentáře: 21

Přehled komentářů

David Grudl Ne vše je závislosti
Dundee5 Re: Ne vše je závislosti
Mastodont Zrušte božství DI
msx Podobne som kedysi postupoval ja a nevedel som o tom, že sa to tak skutočne robí
Aleš Roubíček Re: Podobne som kedysi postupoval ja a nevedel som o tom, že sa to tak skutočne robí
michal.kocarek O koze a o voze
... Re: O koze a o voze
v6ak Re: O koze a o voze
Jan Machala Re: O koze a o voze
reddish Re: Dependency injection a metody globálního prostoru v PHP
David Grudl Re: Dependency injection a metody globálního prostoru v PHP
reddish Re: Dependency injection a metody globálního prostoru v PHP
Vojtěch Dobeš Re: Dependency injection a metody globálního prostoru v PHP
v6ak Re: Dependency injection a metody globálního prostoru v PHP
ondra.novacisko.cz Trošku mimo, ale asi k tématu
K. FP-DI
ZiziTheFirst Není to málo, Antone Pavloviči?
Vena Re: Dependency injection a metody globálního prostoru v PHP
Vena Re: Dependency injection a metody globálního prostoru v PHP
Nocturnius Pěkný článek, pěkná inspirace
Jakub Vrána Nešťastné příklady
Zdroj: https://www.zdrojak.cz/?p=3655