Lekce 4 - Špatné způsoby předávání závislostí - Statika
V minulé lekci, Třívrstvá architektura a další vícevrstvé architektury, jsme si vysvětlili třívrstvou architekturu a jak naše aplikace rozdělit na komponenty 3 typů, abychom dosáhli maximální přehlednosti a znovupoužitelnosti. Naše ukázková aplikace pro evidenci automobilů měla komponenty závislé na databázi. Databázi jsme předávali ručně přes konstruktory. I v takto krátké ukázce bylo vidět, že je to hodně práce navíc.
V dnešní lekci si ukážeme, jak závislosti, v našem případě připojenou databázi, do modelů dostaneme.
Způsoby předávání závislostí
Jakmile začnete programovat objektově, je jen otázka času, než budete někde potřebovat zavolat funkčnost mimo odpovědnost daného objektu. Jinými slovy aktuální objekt danou funkci nebude umět a ani by neměl, proto ji zavoláte na jiném objektu, který je za tuto oblast aplikace odpovědný. To jsme si již říkali v úvodní lekci. Háček je samozřejmě v tom, že onen jiný objekt může mít nějaký stav a další závislosti a proto jej buď nemůžeme přímo vytvořit nebo ani nechceme. Objekt byl totiž již vytvořen a nakonfigurován někde jinde a my jej potřebujeme odtamtud získat. Databáze je jednoduchým příkladem, její instanci nastavíme a připojíme jednou na začátku aplikace a veškerou práci s databází budeme chtít realizovat přes tuto jednu instanci.
Asi vás nepřekvapí, že programátoři za ta léta vymysleli hned několik způsobů, jak závislosti předávat. Dovolil jsem si je rozdělit do několika kategorií:
Špatné způsoby
- Závislosti vůbec nepředávat
- Statika
- Singleton
- Service locator
- Různé variace (lokátor je Singleton a podobně)
Pracné způsoby
- Závislosti předáváme manuálně
Správné způsoby
- IoC (Dependency Injection)
- Automatizace DI
Jednotlivé způsoby si během kurzu vyzkoušíme a zjistíme, co je na nich špatně. Nakonec pochopíme princip obráceného řízení a vzor Dependency Injection.
Špatné způsoby předávání závislostí
Špatná řešení jsou jednoduchá, proto jsou stále používaná, i když se dávno ví, že nejsou ideální.
Chyba č. 1 - Závislosti vůbec nepředávat
Prvním špatným řešením je závislosti vůbec nepředávat. Jakmile nějakou závislost potřebujeme, vytvoříme si její instanci znovu. Pokud na jeden dotaz na aplikaci potřebujeme 10 různých manažerů a každý potřebuje databázi, vytvoříme si instanci databáze v každém znovu, tedy celkem 10 databází a 10x se připojíme. Je snad jasné, že tento způsob není ani reálně použitelný a jedná se o odstrašující příklad. Pro úplnost si uveďme příklad.
Modely/SpravceAut.php
Model potřebuje databázi? Vytvoří si tedy novou instanci PDO
a tu si připojí.
class SpravceAut { private $databaze; public function __construct() { $this->databaze = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'root', ''); } public function vratAuta() { return $this->databaze->query("SELECT * FROM auta")->fetchAll(PDO::FETCH_ASSOC); } }
Kontrolery/AutaKontroler.php
class AutaKontroler { public function vsechna() { $spravceAut = new SpravceAut(); $auta = $spravceAut->vratAuta(); // Proměnná pro šablonu require('Sablony/auta.phtml'); // Načtení šablony } // Případné další akce jako jedno($id), odstran($id), ... }
Všimněte si, že SpravceAut
již nevyžaduje v konstruktoru
databázi, protože si tvoří sám novou.
Přístup bychom mohli "vylepšit" tím, že bychom databázi tvořili v kontrolerech a následně ji předávali ručně modelům, což je takový kočkopes mezi tímto přístupem a přístupem s manuálním předáváním závislostí. Další nevýhodou tohoto přístupu je, že máme připojovací údaje k databázi na několika místech.
Problémy přístupu
Tvoření stále nových instancí je:
- výkonově neefektivní
- paměťově neefektivní
Máme více objektů než potřebujeme. V budoucnu by se nám stávalo, že bychom si nastavili nějaká data do jednoho objektu a následně je očekávali v instanci té samé třídy jinde. Ale tam by data samozřejmě nebyla, protože by šlo o jiný objekt.
Závislosti chceme zkrátka sdílet, potřebujeme od každé 1 jedinou instanci a k té přistupovat z různých míst aplikace.
Chyba č. 2 - Závislosti předáváme staticky
Použít statiku k předávání závislostí je pravděpodobně nejlepším ze špatných řešení. Statika je také základem Singletonu, o kterém si povíme v příští lekci. Pokud jste absolvovali místní kurzy, tak víte, že jsem se v nich k řešení pomocí statiky uchýlil. Napsat si vlastní DI kontejner je totiž práce pro expertní programátory a používat složité cizí frameworky hned ze začátku také není velkou výhrou. S něčím se začít musí, ale zároveň bychom u statiky neměli příliš dlouho zůstávat.
Víme, že statické atributy patří třídě, nikoli instanci. A třída je globálně viditelná. Co si uložíme do statických atributů bude tedy vždy přístupné odkudkoli. Sice to není přesně to, co potřebujeme, ale z jednotlivých modelů se ke staticky uloženým závislostem dostaneme. Existuje mnoho způsobů, jak pomocí statiky závislosti předávat. Nejlepší implementací je pravděpodobně:
- Manažery tvořit stále znovu
- Jakmile manažer potřebuje nějakou závislost, přistoupí k jiné
třídě staticky. Klíčové závislosti, jako je např. databáze,
můžeme napsat celé staticky, to si ukážeme za chvíli.
Menší závislosti, např. instanci aktuálně
přihlášeného uživatele, můžeme uložit do statického
atributu, např. na třídu
SpravceUzivatelu
, aby byla tak přístupná i ostatním třídám.
Modely/Databaze.php
Pro databázovou třídu PDO
z PHP si vytvoříme statický wrapper (obal). Tím ji
bude možné používat kdekoli bez omezení a zároveň se bude sdílet vždy
jen 1 připojení. Zdrojový kód třídy by vypadal takto:
class Databaze { private static $pdo; public static pripoj($db, $jmeno, $heslo) { self::$pdo = new PDO("mysql:host=localhost;dbname=$db;charset=utf8", $jmeno, $heslo); } public static function dotazVsechny($dotaz) // Parametry nebudeme pro jednoduchost řešit { return self::$pdo->query($dotaz)->fetchAll(PDO::FETCH_ASSOC); } }
Všimněte si, že všechny metody i atributy třídy reprezentující klíčovou závislost jsou statické. To abychom instanci vůbec nepotřebovali a nemuseli ji předávat.
index.php
Někde na začátku aplikace, v index.php, bychom databázi měli připojit:
// Autoloader // ... Db::pripoj('testdb', 'root', ''); // Nějaké volání routeru, který spustí příslušný kontroler // ...
Připojená instance PDO
se uložila do statického atributu na
třídě Databaze
.
Modely/SpravceAut.php
Nyní můžeme odkudkoli zavolat metodu
Databaze::dotazVsechny()
, která obaluje metodu
query()
na připojené instanci PDO
:
class SpravceAut { public function vratAuta() { return Databaze::dotazVsechny("SELECT * FROM auta"); } }
Model SpravceAut
má k databázi přístup.
Kontrolery/AutaKontroler.php
A co kontroler? Tam opět nemusíme nic řešit, pouze vytvoříme novou
instanci SpravceAut
.
class AutaKontroler { public function vsechna() { $spravceAut = new SpravceAut(); $auta = $spravceAut->vratAuta(); // Proměnná pro šablonu require('Sablony/auta.phtml'); // Načtení šablony } }
V celé aplikaci se nám sdílí jedna připojená instance
databázové třídy PDO
díky faktu, že jsme ji obalili
statickou třídou Databaze
, která má atribut statický.
Problémy přístupu
Co je tedy špatně?
- Databáze je přístupná odkudkoli, tedy z kontroleru, ale i např. z šablony! Z kteréhokoli PHP souboru, který ji ani nepotřebuje. To není zrovna bezpečné a svádí to ke špatnému návrhu a porušování Low Coupling.
- Databáze se bude velmi špatně mockovat (nahradit testovacími daty), což znepříjemní testování, až se aplikace stane větší.
- Aplikace "lže" o svých závislostech, není na první pohled patrné jaké třídy která třída používá. Abychom to zjistili, musíme projít celý zdrojový kód a všímat si statických přístupů.
- Velkým problémem může být používání statiky ve vícevláknových aplikacích, v PHP se do této situace pravděpodobně nedostanete, ale v jiných jazycích vstoupíte do race condition.
Statika má nápadně blízko k globálním proměnným, které jsou antipattern a moderní jazyky je již ani nepodporují. Aby nedošlo k nedorozumění, zopakuji ještě na závěr, že statické předávání závislostí je občas použito v místních kurzech kvůli své jednoduchosti. V pokročilejších kurzech se poté pracuje s frameworky, které tuto problematiku řeší lépe.
Pro dnešní lekci je to vše.
Příště, v lekci Špatné způsoby předávání závislostí - Singleton a SL, si ukážeme problémy Singletonu a service
locatoru, čímž dokončíme špatné přístupy předávání závislostí a
budeme se moci pustit do DI