5. díl - Špatné způsoby předávání závislostí - Singleton a SL

Návrhové vzory Softwarové architektury a depencency injection Špatné způsoby předávání závislostí - Singleton a SL

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci, Špatné způsoby předávání závislostí - Statika, jsme si představili několik způsobů, kterými se lze ve vícevrstvých aplikacích vypořádat se závislostmi. Již víme, že jakmile je celá aplikace napsaná v jednom souboru, je minimálně nepřehledná, při dnešní složitosti softwaru ne-li již nevytvořitelná. Jakmile aplikaci rozdělíme na vrstvy a vrstvy na objekty, nutně začneme řešit otázku komunikace mezi těmito objekty. Když objekt komunikuje s jiným, říkáme, že má závislosti.

V dnešním tutoriálu o návrhu softwaru se budeme věnovat návrhovému vzoru Singleton, který souvisí se statikou. Tu jsme probrali minule. Dále si ukážeme vzor service locator.

Chyba č. 3 - Závislosti předáváme Singletony

Singleton, česky jedináček, je poměrně kontroverzní návrhový vzor z populární skupiny vzorů GOF. Ačkoli jej uvidíte zde na příkladu, můžete si o něm přečíst zdejší detailní článek pro případ, že byste chtěli další informace.

Singleton je principiálně podobný našemu statickému obalu z minulé lekce. Je důležité zmínit, že Singleton používá statiku, ukládá instanci závislosti do statického atributu a nabízí rovněž statickou metodu k jejímu získání. Platí pro něj tedy úplně všechny negativní vlastnosti, jako jsme si zmínili u statiky. Navíc je pravý Singleton např. v PHP poměrně složité vytvořit. Že je návrhovým vzorem jej nedělá v ničem lepším a minimálně pro předávání závislostí je to anti-pattern. Dle mého názoru je jeho použití méně vhodné, než jen čisté použití statiky. Ale to je spíše věc osobního vkusu. Singleton lze napsat thread-safe a může naleznout své uplatnění při práci s vlákny, kde jeho využití dává smysl. Na závislosti se ovšem nehodí.

Ukažme si, jak by vypadalo předávání připojené instance PDO (databáze) našim modelům v aplikaci pro evidenci automobilů.

Modely/Databaze.php

Pro získání databázové instance 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');
    }

}

Ve třídě vidíme na první pohled opět statický atribut s instancí PDO jako u wrapperu z minulé lekce. Neobalujeme tu však jednotlivé metody PDO, ale pouze poskytujeme jednu metodu pro vrácení celé instance. Povšimněte si lazy-loadingu, tedy, že se instance vytvoří až když si o ni řekneme a poté se vždy vrátí tato jedna instance. Jak atribut s PDO, tak metoda vratInstanci() jsou statické, abychom je mohli používat odkudkoli. Co ale když někdo vytvoří novou databázi zavoláním new Databaze()? Vytvářet instanci Singletonu nemá smysl, jelikož slouží pouze pro získání obsahu statického atributu. Proto tvorbu instancí třídy Databaze zakážeme a to vytvořením privátního konstruktoru. Jelikož v PHP je možné tvořit instance ještě příkazem clone nebo deserializací, musíme zakázat i tyto akce. Vidíte, že Singleton začíná být trochu magický.

Modely/Spravce­Aut.php

V modelu si jednoduše řekneme o instanci pomocí statické metody a uložíme si ji:

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);
    }

}

Již bylo řečeno, že máme stále všechny nevýhody statiky. A kód je oproti příkladu se statickým wrapperem ještě delší, ten vypadal takto:

class SpravceAut
{

    public function vratAuta()
    {
        return Databaze::dotazVsechny("SELECT * FROM auta");
    }

}

Singleton nám tedy opravdu moc nepomáhá.

Kontrolery/Au­taKontroler.php

Kontroler bude identický, správce vytvoříme nového a nemusíme mu nic předávat, jelikož si databázi získá 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 přístupu

Zůstávají všechny problémy spojené se statikou:

  • 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. Singleton by tedy musel být navíc napsaný jako thread-safe, což ten náš není.

Máme zde další problémy:

  • Oproti statickému wrapperu musíme instanci někam ukládat, kód je zbytečně delší
  • Musíme zajistit, aby mohla existovat vždy jen jedna instance

A jednu výhodu:

  • Nemusíme wrappovat všechny metody z PDO

Singleton se na předávání závislostí jasně nehodí.

Chyba č. 4 - Závislosti sdružujeme do service lokátoru

Možná by vás napadlo vytvořit si jeden kontejner se všemi závislostmi celé aplikace a předávat všude tento jeden kontejner. Objekty si poté z kontejneru vytahají co potřebují. Tento způsob se používá např. v herním frameworku MonoGame pro C# .NET nebo v jazyce Ruby. Vytvoříme si tedy třídu reprezentující kontejner, tam si uložíme instanci naší databáze a poté budeme všude manuálně předávat tento kontejner. Pokud bude závislostí mnoho, stále předáváme pouze jednu na kontejner. To by mělo být lepší, ne? No... Pojďme to zkusit:

Modely/Service­Locator.php

Připravme si kontejner se sdílený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

Někde na začátku aplikace si kontejner instanciujeme a následně jej musíme všude manuálně předávat:

// 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 lekce o dvouvrstvé architektuře. Tam jsme takto předávali databázi, my zde předáváme lokátor, ve kterém může být závislostí více.

Kontrolery/Au­taKontroler.php

V kontroleru si kontejner opět uložíme a budeme jej předávat všem modelům, v našem případě 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/Spravce­Aut.php

V modelu uděláme opět to samé, předaný lokátor si přebereme a poukládáme si z něj služby, které potřebujeme:

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 přístupu

Jak jsme na tom po úpravě aplikace na service locator?

  • Všechny modely mají přístup ke všem službám v lokátoru. To je sice o něco bezpečnější než u statiky a Singletonu, ale stále to není ideální stav.
  • Musíme stále předávat instanci lokátoru a ukládat jeho služby

Lokátor vyjde asi na stejno jako statika, získali jsme sice výhody, ale také další nevýhody.

Již vás nebudeme dále trápit. Příští lekce, Předávání závislostí pomocí Depencency Injection, obsahuje bod zlomu. Zjistíme společný problém všech dosud představených přístupů a jeho řešení. Představíme si Inversion of Control a již dlouho slibovanou Dependency Injection.


 

 

Článek pro vás napsal David Čápka
Avatar
Jak se ti líbí článek?
4 hlasů
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity (3)

 

 

Komentáře

Avatar
Matúš Petrofčík
Šéfredaktor
Avatar
Matúš Petrofčík:27. srpna 14:58

Som zvedavý na IoC a DI (ďalší článok). Pravdu povediac, pracujem v Zend Framework-u 3 a tam je práve service locator (alebo skôr container), ale nikdy som ho nepredával celý controller-u tak ako je to v ukážkach. Vždy sa vytvorí pre daný controller factory (továrnička) a len v nej je prístup k service locator-u, z ktorého sa vyberú potrebné závislosti, vytvorí controller (závislosti sa predajú cez __construct) a vráti sa inštancia controller-u. Tiež je to nesprávny spôsob?

Editováno 27. srpna 14:59
Odpovědět 27. srpna 14:58
obsah kocky = r^2 ... a preto vlak drnká
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Matúš Petrofčík
David Čápka:27. srpna 15:37

A jak potom z kontroleru předáváš závislosti těm modelům?

Odpovědět 27. srpna 15:37
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Matúš Petrofčík
Šéfredaktor
Avatar
Odpovídá na David Čápka
Matúš Petrofčík:27. srpna 17:50

Uvediem príklad ako to funguje v ZF3:

Mám UserController, a ten má nejaké závislosti, ktoré sú mu predané pri jeho vytváraní v UserController­Factory.

Jednou z tých závislostí je UserFacade, ktorá má tiež nejaké závislosti predané pri vytváraní pomocou UserFacadeFactory.

Takou závislosťou pre UserFacade je EntityManager pre prácu s Doctrine, a tá má niekde tiež vlastnú EntityManager­Factory :D atď...

Chcem použiť ten UserController, ale ešte ho nemám vytvorený (vytvára ho Router a doteraz som inak závislosti než pomocou factory nepredával). Router sa pozrie či má UserController nejaké závislosti (a k tomu určenú továrničku), a ak áno, tak ho vytvorí pomocou UserController­Factory.

V tej UserController­Factory sa zo service locator-u (container po novom) vyťahujú potrebné závislosti, napr. tá UserFacade. Service locator sa pozrie, či už ju má vytvorenú, a ak nie, vytvorí ju. Pri jej vytváraní sa pozerá, či má UserFacade nejaké závislosti a k tomu potrebnú UserFacadeFactory. Ak aj UserFacade má závislosti, napr. ten EntityManager, tak ho zas vytvorí, atď až má všetko pripravené.

V konečnom dôsledku zjednodušene vytvorí EntityManager, ten vloží ako závislosť pri vytvorení UserFacade, a tú vloží ako závislosť pri vytvorení UserController-u. Všetky inštancie tak majú všetky pre seba potrebné závislosti, ale nemajú v skutočnosti prístup k ničomu inému. K service locator-u majú prístup iba továrničky, aby mohli získavať a predávať závislosti (samozrejme pokiaľ sami nepredáme celý service locator, ale to by skúsený programátor neurobil).

Ak sa raz vytvorí UserController alebo UserFacade alebo EntityManager, tak sa už pri jeho potrebe niekde inde nevytvára znovu pokiaľ si to explicitne od service locator-u nevyžiadame.

Tie factory dokonca nie je potrebné definovať, dá sa to aj automatizovať, ale osobne ich vždy vytváram pretože občas nie je jasné čo sa tam má predať (ak sa ako závislosti definujú rozhrania namiesto konkrétnych tried).

PS: EntityManager je tiež tak trochu service locator, pretože sa pomocou neho dajú vytvárať/získavať repozitáre pre entity, ale na to kašlime.

Odpovědět 27. srpna 17:50
obsah kocky = r^2 ... a preto vlak drnká
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Matúš Petrofčík
David Čápka:27. srpna 18:21

Pokud to je opravdu takhle, tak je to variace service lokátoru a manuálního předávání, protože to musíš stejně ručně tahat z lokátoru v té továrničce. IMHO to není moc šťastné řešení, ještě jsem neviděl, že by se musela pro všechny závislosti vytvářet továrna.

Odpovědět 27. srpna 18:21
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Matúš Petrofčík
Šéfredaktor
Avatar
Odpovídá na David Čápka
Matúš Petrofčík:27. srpna 21:00

Je dosť možné že existuje aj iný spôsob :) Dík za odpovede.

Odpovědět 27. srpna 21:00
obsah kocky = r^2 ... a preto vlak drnká
Avatar
Matúš Petrofčík
Šéfredaktor
Avatar
Odpovídá na David Čápka
Matúš Petrofčík:27. srpna 21:15

Tak som si to rýchlo pozrel a predsa má ZF3 balíček zend-di, ktorý ponuka práve IoC Container. Avšak v dokumentácii pre zend-di sa odporúča používať zend-servicemanager, ktorý je ZF3 východzí (a preto sa píšu tie továrničky). Zaujímavé, ani som o tom nevedel.

Unlike zend-di, zend-servicemanager is code-driven, meaning that you tell it what class to instantiate, or provide a factory for the given class. This approach offers several benefits:

  • Easier to debug (error stacks take you into your factories, not the dependency injection container).
  • Easier to setup (write code to instantiate objects, instead of configuration).
  • Faster (zend-di has known performance issues due to the approaches used).

Unless you have specific needs for a dependency injection container versus more general Inversion of Control, we recommend using zend-servicemanager for the above reasons.

Odpovědět 27. srpna 21:15
obsah kocky = r^2 ... a preto vlak drnká
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 6 zpráv z 6.