Přidat článek mezi oblíbenéZasílat nové komentáře e-mailem IoC a dependency injection v PHP

Veľkou časťou programátorov je PHP považované za patlací jazyk, kde sa všetko nejak polepí a hlbšia koncepcia tu vôbec neexistuje. Pravda je taká, že PHP patlačov je dosť, ale samotný jazyk za to nemôže. V tomto článku by som chcel priblížiť princíp IoC a dependency injection, ktorý môže pomôcť lepšie si rozvrhnúť kód.

Teória

Nechcem tu vypisovať rozsiahle poučky z internetu, ktoré si môžete nájsť aj sami. Len uvediem, že hlavným účelom použitia IoC (inversion of control) je:

- oddelenie väzieb medzi vykonávaním a implementáciou,
- zameranie sa na modul a jeho úlohu,
- oslobodenie modulov od toho, čo robia iné časti systému,
- zabránenie vedľajších účinkov pri výmene modulov.

Dependency injection je jeden zo spôsobov, ako IoC implementovať. A najlepšie je ukázať si to na príklade.

Príklad

Povedzme si, že chceme naprogramovať modul, ktorý bude ukladať dáta o zákazníkovi do databázy. Vytvoríme si teda triedu Customer a triedu Db. Pričom Customer bude používať Db na pripojenie do databázy. Riešenie, ktoré tu popíšem, bude trochu abstraktné, ale ide hlavne o princíp. Taktiež je všetok kód tu uvedený písaný do jedného PHP súboru. Na stiahnutie je aj zip, kde si môžete pozrieť celé riešenie, kde sú jednotlivé triedy rozdelené do separátnych súborov.

class Db
{
    public function OpenConnection()
    {
        echo "Pripojenie do DB bolo otvorené.";
    }
}

class Customer
{
    public function Insert()
    {
        $db = new Db();
        $db->OpenConnection();
        echo "Otvorili sme spojenie a vložili zákazníka.";
    }
}

Problémy

V jednoduchej aplikácií nám takéto riešenie nevadí, ale ak bude riešenie rozsiahlejšie, tak nám vznikajú nasledovné problémy.
Trieda Customer je úzko spätá s triedou Db. Čo ak budeme chcieť v budúcnosti použiť inú databázu? Ak máme len jeden objekt, tak ho prepíšeme, ale ak tých objektov bude 10 a viac, tak budeme musieť v každom z nich upraviť, aby používal povedzme triedu DbSqLite. A neskôr príde Oracle. A každý zákazník bude chcieť používať inú.

Čo potrebujeme?

Potrebujeme, aby tieto triedy neboli na sebe pevne závislé. Takže v podstate triede Customer hodíme do konštruktora parameter Db.

class Customer
{
    private $Db;

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

    public function Insert()
    {
        $this->Db->OpenConnection();
        echo "Otvorili sme spojenie a vložili zákazníka.";
    }
}

Týmto sme presunuli zodpovednosť za dodanie správnej databázy niekomu, kto bude triedu Customer používať. Trieda tak nie je pevne zviazaná s konkrétnou implementáciou. Ale je toto finálne riešenie? Nie je. Problém, ktorý nám teraz nastal je, že sme pridali robotu niekomu, kto chce používať našu triedu. A strom takýchto závislostí môže byť väčší. Trieda Db bude v konštruktore vyžadovať nejakú logovaciu triedu a tá zas bude vyžadovať cestu do log súboru, atď.

IoC framework

Na to, aby sme vyriešili tento problém, potrebujeme použiť niektorý z dostupných IoC frameworkov. Mne sa osvedčilo použitie Dice. Je jednoduchý a ľahko sa konfiguruje.

Načo nám je vlastne takýto framework? Framework nám bude slúžiť na inštancovanie tried. Vďaka konfigurácií, ktorú mu dodáme vie, že ak chceme inštanciu triedy Customer, tak má vytvoriť najprv inštanciu triedy Db a posunúť ju do konštruktora.

IoC container

Samotný framework obsahuje container, alebo kôš, do ktorého nahádžeme, ako sa má ktorý objekt prekladať. Výhodou je, že v samotnej implementácií sa spoliehame len na interface a konfiguráciou IoC containera uvedieme, aká konkrétna implementácia predstavuje daný interface. Druhou výhodou je, že konfigurácia je na jednom mieste a v prípade, že chceme globálne zmeniť databázu z MySql na Oracle, tak ju len zmeníme.

Interface, interface, interface

Na to, aby sme mohli správne využiť IoC, potrebujeme interface a type hinting. To znamená, že v konštruktoroch aj uvedieme, akú implementáciu očakávame.

Upravíme si teda kód nasledovne:

interface IDb
{
    public function OpenConnection();
}

class DbMySql implements IDb
{
    public function OpenConnection()
    {
        echo "Pripojenie do MySQL DB bolo otvorené.";
    }
}

class DbOracle implements IDb
{
    public function OpenConnection()
    {
        echo "Pripojenie do Oracle DB bolo otvorené.";
    }
}

class Customer
{
    private $connection;

    public function __construct(IDb $connection)
    {
        $this->connection = $connection;
    }

    public function Insert()
    {
        $this->connection->OpenConnection();
        echo "Zákazník bol vložený.";
    }
}

Čo sme urobili?

Vytvorili sme si základný interface IDb, ktorý má jednu metódu OpenConnection. Implementovali sme ho dvomi triedami, jednu pre MySql a jednu pre Oracle.

Trieda Customer očakáva, že v konštruktore jej príde trieda, ktorá implementuje interface IDb. Nezaujíma ju už konkrétna implementácia. Tú si zabezpečíme my triedami DbMySql a DbOracle. IoC container a jeho konfigurácia nám zase zabezpečí, že pri vytváraní objektu Customer sa použije správna implementácia.

Použitie IoC containera

Teraz nám už stačí len nakonfigurovať Dice a použiť ho. Ja si na to použijem jednoduchú factory triedu (o factory patterne si môžete prečítať napríklad tu).

require_once ("dice/Dice.php");

class DiceFactory
{
    public static function Instance ()
    {
        $dice = new \Dice\Dice();

        $dice->addRule('Customer', ['substitutions' => ['IDb' => ['instance' => 'DbMySql']]]);

        return $dice;
    }
}

Vysvetlenie

DiceFactory je trieda obsahujúca jednu statickú metódu Instance, ktorá vytvorí inštanciu triedy Dice a vloží do nej konfiguráciu. V tomto prípade sme uviedli, že pre triedu Customer treba nahradiť interface IDb inštanciou triedy DbMySql.

Použitie

Nakoniec už len vytvárame objekty pomocou IoC containera. Zavoláme našu factory triedu a vypýtame si inštanciu triedy Customer. Potom zavoláme jeho metódu Insert.

$instance = DiceFactory::Instance()->create("Customer");
$instance->Insert();

Záver

Naučili sme sa základy použitia dependency injection pomocou IoC frameworku. Nie je samozrejme povedané, že takýto dizajn je všade vhodný. Treba zvážiť aj dopady na výkon. Nie všetky IoC frameworky sú výkonnostne odladené.
Veľkou výhodou použitia tohto princípu je modulárnosť riešenia. Ľahšie sa testuje, jednoduchšie sa vytvárajú mockupy za moduly, ktoré ešte nie sú implementované, a taktiež sa dobre konfiguruje. Náhrada jednej implementácie za inú sa robí na jednom spoločnom mieste.

V prílohách nájdete aj zip s úplným riešením zahrňujúcim využitie autoload a oddelenie tried do separátnych súborov.

Předmět Autor Datum
$instance = DiceFactory::Instance()->create("Customer"); Customer je teda singleton?
MaSo 23.09.2015 09:27
MaSo
Nie je. Factory vytvori vzdy novu instanciu objektu customer.
wam_Spider007 23.09.2015 12:28
wam_Spider007
Aha. Nebude po case problem problem s pameti? Vubec teda nevim, jak funguje garbage collection v PHP…
MaSo 23.09.2015 17:42
MaSo
ano, DAO sa vacsinou robia ako static alebo singleton. Toto bol viac menej priklad toho, ako IoC fun…
wam_Spider007 23.09.2015 18:07
wam_Spider007
Když už jsi tam zmínil testování. Jak se PHP dělají unit testy a tvoří mocky?
Wikan 24.09.2015 19:29
Wikan
Kolega, vasnivy PHPista, mi tu septa neco o PHPUnit...:-)
MaSo 25.09.2015 08:53
MaSo
Toto je zaujimava tema. Ako uz spominal maso, existuje PHP framework PHPUnit. Nemam s tymto skusenos… poslední
wam_Spider007 25.09.2015 08:57
wam_Spider007

Aha. Nebude po case problem problem s pameti? Vubec teda nevim, jak funguje garbage collection v PHP, ale dokazu si predstavit, ze na toho customera muze byt milion dotazu denne a tim padem bude v aplikaci existovat i milion instanci tridy...:-)

Cekal bych, ze to bude podobne jako treba ve Springu v Jave, a sice ze DAO objekty budou singletony a ne prototypy.

ano, DAO sa vacsinou robia ako static alebo singleton.
Toto bol viac menej priklad toho, ako IoC funguje.
Co sa pamate tyka, tak dotazov mozno bude milion denne, ale prakticky po kazdom pouziti instancie sa objekt zlikviduje, nakolko stranka bude uz nacitana cela alebo prejdes na inu stranku, kde uz objekt neexistuje.

Toto je zaujimava tema. Ako uz spominal maso, existuje PHP framework PHPUnit.
Nemam s tymto skusenosti. Unit testy robim len v .NET.
Mozem sa ale na to pozriet a pripadne napisat dalsi clanok.

Zpět na články Přidat komentář k článku Nahoru