5. diel - Zlé spôsoby odovzdávania závislostí - Singleton a SL
V minulej lekcii, Zlé spôsoby odovzdávania závislostí - Statika , sme si predstavili niekoľko spôsobov, ktorými sa dá vo viacvrstvových aplikáciách vysporiadať so závislosťami. Už vieme, že akonáhle je celá aplikácia napísaná v jednom súbore, je minimálne neprehľadná, pri dnešnej zložitosti softvéru ak nie už nevytvořitelná. Akonáhle aplikáciu rozdelíme na vrstvy a vrstvy na objekty, nutne začneme riešiť otázku komunikácie medzi týmito objektmi. Keď objekt komunikuje s iným, hovoríme, že má závislosti.
V dnešnom tutoriále o návrhu softvéru sa budeme venovať so svojimi vzoru Singleton, ktorý súvisí so statikou. Tú sme prebrali minule. Ďalej si ukážeme vzor service locator.
Chyba č. 3 - Závislosti odovzdávame Singleton
Singleton, česky jedináčik, je pomerne kontroverzné návrhový vzor z populárnej skupiny vzorov GOF. Hoci ho uvidíte tu na príklade, môžete si o ňom prečítať tunajšie detailné článok pre prípad, že by ste chceli ďalšie informácie.
Singleton je principiálne podobný nášmu statickému obale z minulej lekcie. Je dôležité spomenúť, že Singleton používa statiku, ukladá inštanciu závislosti do statického atribútu a ponúka taktiež statickú metódu na jej získanie. Platí pre neho teda úplne všetky negatívne vlastnosti, ako sme si spomenuli u statiky. Naviac je pravý Singleton napr. V PHP pomerne zložité vytvoriť. Že je návrhovým vzorom ho nerobí v ničom lepšom a minimálne pre odovzdávanie závislostí je to anti-pattern. Podľa môjho názoru je jeho použitie menej vhodné, než len čisté použitie statiky. Ale to je skôr vec osobného vkusu. Singleton možno napísať thread-safe a môže nájsť svoje uplatnenie pri práci s vláknami, kde jeho využitie dáva zmysel. Na závislosti sa však nehodí.
Ukážme si, ako by vyzeralo odovzdávanie pripojenej inštancie PDO (databázy) našim modelom v aplikácii pre evidenciu automobilov.
Modely / Databaze.php
Pre získanie databázové inštancie si implementujeme Singleton:
class Databaze { private static $pdo; private function __construct() { /* Prázdné */ } public static function vratInstanci() { if (!self::$pdo) self::$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'root', ''); return self::$pdo; } final public function __clone() { throw new Exception('Klonování není povoleno'); } final public function __wakeup() { throw new Exception('Deserializace není povolena'); } }
V triede vidíme na prvý pohľad opäť statický atribút s
inštanciou PDO
ako u Wrapper z minulej lekcie.
Neobalujeme tu však jednotlivé metódy PDO
, ale iba poskytujeme
jednu metódu pre vrátenie celej inštancie. Všimnite si lazy-loading, teda,
že sa inštancia vytvorí až keď si o ňu povieme a potom sa vždy
vráti táto jedna inštancia. Ako atribút s PDO
, tak
metóda vratInstanci()
sú statické, aby sme ich mohli
používať odkiaľkoľvek. Čo ale keď niekto vytvorí novú
databázu zavolaním new Databaze()
? Vytvárať inštanciu
Singleton nemá zmysel, pretože slúži len na získanie obsahu
statického atribútu. Preto tvorbu inštancií triedy
Databaze
zakážeme a to vytvorením
privátneho konstruktoru. Keďže v PHP je možné tvoriť inštancia
ešte príkazom clone
alebo deserializací, musíme zakázať aj
tieto akcie. Vidíte, že Singleton začína byť trochu magický.
Modely / SpravceAut.php
V modeli si jednoducho povieme o inštanciu pomocou statickej metódy a uložíme si ju:
class SpravceAut { private $databaze; public function __construct() { $this->database = Databaze::vratInstanci(); } public function vratAuta() { return $this->databaze->query("SELECT * FROM auta")->fetchAll(PDO::FETCH_ASSOC); } }
Už bolo povedané, že máme stále všetky nevýhody statiky. A kód je oproti príkladu sa statickým Wrapper ešte dlhší, ten vyzeral takto:
class SpravceAut { public function vratAuta() { return Databaze::dotazVsechny("SELECT * FROM auta"); } }
Singleton nám teda naozaj moc nepomáha.
Kontrolery / AutaKontroler.php
Kontrolér bude identický, správca vytvoríme nové a nemusíme mu nič odovzdávať, pretože si databázu získa sám:
class AutaKontroler { public function vsechna() { $spravceAut = new SpravceAut(); $auta = $spravceAut->vratAuta(); // Proměnná pro šablonu require('Sablony/auta.phtml'); // Načtení šablony } }
Problémy prístupu
Zostávajú všetky problémy spojené so statikou:
- Databáza je prístupná odkiaľkoľvek, teda z kontroleru, ale i napr. Z šablóny! Z ktoréhokoľvek PHP súboru, ktorý ju ani nepotrebuje. To nie je práve bezpečné a zvádza to k zlému návrhu a porušovania Low Coupling.
- Databáza sa bude veľmi zle Mocková (nahradiť testovacími dátami), čo znepríjemní testovanie, až sa aplikácia stane väčšou
- Aplikácia "klame" o svojich závislostiach, nie je na prvý pohľad zrejmé aké triedy ktorá trieda používa. Aby sme to zistili, musíme prejsť celý zdrojový kód a všímať si statických prístupov.
- Veľkým problémom môže byť používanie statiky vo viacvláknových aplikáciách, v PHP sa do tejto situácie pravdepodobne nedostanete, ale v iných jazykoch vstúpite do race condition. Singleton by teda musel byť navyše napísaný ako thread-safe, čo ten náš nie je.
Máme tu ďalšie problémy:
- Oproti statickému Wrapper musíme inštanciu niekam ukladať, kód je zbytočne dlhší
- Musíme zabezpečiť, aby mohla existovať vždy len jedna inštancia
A jednu výhodu:
- Nemusíme wrappovat všetky metódy z PDO
Singleton sa na odovzdávaní závislosťou jasne nehodí.
Chyba č. 4 - Závislosti združujeme do service lokátora
Možno by vás napadlo vytvoriť si jeden kontajner so všetkými závislosťami celej aplikácie a odovzdávať všade tento jeden kontajner. Objekty si potom z kontajnera vyťaháme čo potrebujú. Tento spôsob sa používa napr. V hernom frameworku MonoGame pre C# .NET alebo v jazyku Ruby.
Vytvoríme si teda triedu reprezentujúci kontajner, tam si uložíme inštanciu našej databázy a potom budeme všade manuálne odovzdávať tento kontajner. Pokiaľ bude závislostí veľa, stále odovzdávame iba jednu na kontajner. To by malo byť lepšie, nie? No ... Poďme to skúsiť:
Modely / ServiceLocator.php
Pripravme si kontajner so zdieľanými službami:
class ServiceLocator { private $databaze; private __construct() { // Může být i líně $this->databaze = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'root', ''); } public function vratDb() { return $this->$databaze; } }
Index.php
Niekde na začiatku aplikácie si kontajner instanciujeme a následne ho musíme všade manuálne odovzdávať:
// Autoloader // ... $locator = new ServiceLocator(); // Vytvoří lokátor a závislosti v něm // Kód pro vytvoření kontroleru... $kontroler = new $nazevKontroleru($locator); // Předání lokátoru kontroleru // ...
Kód je podobný tomu z index.php z lekcie o dvojvrstvovej architektúre. Tam sme takto odovzdávali databázu, my tu odovzdávame lokátor, v ktorom môže byť závislosťou viac.
Kontrolery / AutaKontroler.php
V kontroleru si kontajner opäť uložíme a budeme ho odovzdávať všetkým
modelom, v našom prípade do SpravceAut
:
class AutaKontroler { private $locator; public function __construct(ServiceLocator $locator) { $this->locator = $locator; } public function vsechna() { $spravceAut = new SpravceAut($locator); $auta = $spravceAut->vratAuta(); // Proměnná pro šablonu require('Sablony/auta.phtml'); // Načtení šablony } }
Modely / SpravceAut.php
V modeli urobíme opäť to isté, odovzdaný lokátor si preberieme a poukládáme si z neho služby, ktoré potrebujeme:
class SpravceAut { private $databaze; public function __construct(ServiceLocator $locator) { $this->databaze = $locator->vratDb(); // Získání databáze z lokátoru } public function vratAuta() { return $this->databaze->query("SELECT * FROM auta")->fetchAll(PDO::FETCH_ASSOC); } }
Problémy prístupu
Ako sme na tom po úprave aplikácie na service locator?
- Všetky modely majú prístup ku všetkým službám v lokátora. To je síce o niečo bezpečnejšie ako u statiky a Singleton, ale stále to nie je ideálny stav.
- Musíme stále odovzdávať inštanciu lokátora a ukladať jeho služby.
Lokátor vyjde asi na rovnaký ako statika, získali sme síce výhody, ale aj ďalšie nevýhody.
Už vás nebudeme ďalej trápiť. Budúci lekcie, Odovzdávanie závislostí pomocou Dependency Injection , obsahuje bod zlomu. Zistíme spoločný problém všetkých doteraz predstavených prístupov a jeho riešenie. Predstavíme si Inversion of Control a už dlho sľubovanú Dependency Injection.