Na navigaci | Klávesové zkratky

Translate to English… Ins Deutsche übersetzen…

DI a předávání závislostí

Víte, že Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída otevřeně hlásí ke svým závislostem, místo toho, aby je někde pokoutně získávala. Otázka zní, jak se k nim hlásit a jak je předávat.

K předávání závislostí můžeme využít konstruktor:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
    {
        $this->httpRequest = $httpRequest;
        $this->router = $router;
        $this->session = $session;
    }

}

$foobar = new Foobar($hr, $router, $session);

Nebo metody:

class Foobar
{
    private $httpRequest, $router, $session;

    function setHttpRequest(HttpRequest $httpRequest)
    {
        $this->httpRequest = $httpRequest;
    }

    function setRouter(Router $router)
    {
        $this->router = $router;
    }

    function setSession(Session $session)
    {
        $this->session = $session;
    }
}

$foobar = new Foobar;
$foobar->setSession($session);
$foobar->setHttpRequest($hr);
$foobar->setRouter($router);

Nebo přímo naplnit jednotlivé proměnné:

class Foobar
{
    /** @var HttpRequest */
    public $httpRequest;

    /** @var Router */
    public $router;

    /** @var Session */
    public $session;
}

$foobar = new Foobar;
$foobar->session = $session;
$foobar->httpRequest = $hr;
$foobar->router = $router;

Které řešení je nejlepší? Aby článek nebyl neúměrně dlouhý, odkážu se na Předávání závislostí od Vaška Purcharta, tak si jej přečtěte, protože budu navazovat tam, kde končí.

Takže znovu: které řešení je nejlepší? Kdyby byly ekvivalentní, bylo by nejspíš to poslední, protože kód třídy je nejkratší a kratší kód minimalizuje možnost vzniku chyby a šetří čas při psaní i čtení. Nicméně řešení ekvivalentní nejsou. Jsou spíše diametrálně odlišené.

Immutability tedy neměnnost

Immutable object je objekt, který nemění svůj stav od chvíle, co byl vytvořen. Nevěřili byste, kolik problémů objektového návrhu se dá vyřešit jen tím, že se objekty stanou neměnné. Ale to je téma na jiný článek.

Prakticky vždy budeme chtít, aby závislosti objektu byly neměnné. A v tomto směru se jednotlivé varianty předávání liší. Veřejné (public) proměnné můžeme změnit kdykoliv a změnu nelze detekovat, což je zcela diskvalifikuje ze hry a dále už s touto variantou nebudu vůbec počítat. A to ani nemluvím o chybějící typové kontrole. (Viz také úvaha nad tím, jak by se dalo property injection řešit.)

Mohlo by vás napadnout nahradit public za private a vložit do nich závislosti některým z nízkoúrovňových triků (třeba pomocí reflexe), ale takové obcházení vlastností jazyka do obecných úvah o DI nepatří. Privátní proměnné nejsou součástí veřejného API třídy a nelze se jimi hlásit k závislostem. A také nehackujme jazyk, dokud to není nutné.

Neměnnost bychom si u metod mohli zajistit sami:

function setRouter(Router $router)
{
    if ($this->router) {
        throw new InvalidStateException('Router has already been set.');
    }
    $this->router = $router;
}

A protože metoda není klasický obecný setter, tj. lze ji volat jen jednou, nelze očekávat existenci getteru a můžeme její volání považovat za povinné, mohla by používat jiné názvosloví. Například prefix inject, v tomto případě injectRouter().

Vytvořili bychom tedy pro větší srozumitelnost konvenci, že závislosti předáváme metodami inject.

(Musím zdůraznit, že se bavíme o konvenci užitečné pro programátora, o žádných DI kontejnerech v článku nepadlo ani slovo. Pochopitelně by se jí dalo s úspěchem využít i v kontejnerech, nicméně je naprosto zásadní uvědomit si, co je příčinou a co důsledkem.)

Používání metod pro injektáž má svá úskalí:

  • musíme sami zajistit neměnnost
  • špatně se odhaluje okamžik, kdy jsou nastaveny všechny závislosti, abychom provedli inicializaci objektu
  • měli bychom také ověřovat, že se některé závislosti neopomněly nastavit
  • režijní kód bude poměrně ukecaný a dlouhý

Všechny tyto nedostatky řeší už z principu injektáž přes konstruktor, proto vychází jako nejvhodnější.

(…Tedy, ehm, neřeší… Ale k tomu se hnedle dostaneme.)

Constructor hell

Nenápadný problém předávání závislostí přes konstruktor tkví v tom, že nemáme žádné vodítko, v jakém pořadí jsou parametry uvedeny. Napadá mě snad leda řadit je abecedně (divné, co?). Pokud by dvě závislosti byly stejného typu, potom v pořadí source, destination apod.

Byť nám s tímto problémem může pomoci napovídání v IDE nebo automaticky generované kontejnery, nic to nemění na tom, že metoda s nejasnými parametry snižuje srozumitelnost kódu.

Jakožto líny člověk neoblibuji ani ty strojově se opakující přiřazování v těle konstruktoru. Jako zkratku lze použít:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
    {
        list($this->httpRequest, $this->router, $this->session) = func_get_args();
    }

}

Ale pokud by byla poslední závislost nepovinná, mohlo by to skončit u Notice: Undefined offset.

Uvažuji nad sepsáním RFC pro PHP, aby bylo možné používat zápis:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $this->httpRequest, Router $this->router, Session $this->session)
    {
    }

}

Nicméně tohle jsou jen syntaktické libůstky oproti kruciálnímu problému s dědičností.

Co se stane, když vytvoříme potomka:

class Barbar extends Foobar
{
    private $logger;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session, Logger $logger)
    {
        parent::__construct($httpRequest, $router, $session);
        $this->logger = $logger;
    }

}

Jak vidno, konstruktor potomka musí:

  • vyjmenovat závislosti rodiče
  • zavolat rodičovský konstruktor

To je v pořádku, závislosti rodiče jsou i jeho dědictvím. Jenže neexistuje mechanismus, kterým by se dalo volání rodičovského konstruktoru vynutit. Jednou z nejprotivnějších chyb se tak stane opomenutí volání parent::__construct. Takže předpoklad, že konstruktor už z principu vynucuje předání závislostí, je vlastně chybný. Konstruktor se dá snadno obejít.

Bez zajímavosti není, že zdáním je i neměnnost, protože nic nebrání zavolat na hotovém objektu $barbar->__construct(...) a protlačit mu jiné závislosti. Měl by tedy konstruktor testovat, zda není volán podruhé? Kašlete na to, konstruktor se prostě znovu volat nesmí. Otázka konvence.

Největší průšvih nastane ale ve chvíli, kdy provedu refactoring třídy Foobar, jehož důsledkem bude změna závislostí. Bude nutné přepsat konstruktory všech potomků. Jistě, je to logické, ale v praxi může jít o fatální zádrhel. Pokud například rodičem bude třída z frameworku (např. Presenter), jejíž potomky píše každý uživatel frameworku, fakticky se tak znemožní její vývoj, protože zásah do závislostí by byl kolosálním BC breakem.

Řada z výhod konstruktorové injektáže se rozplynula jak pára nad hrncem. Pokud se zdálo, že volání konstruktoru je vynuceno jazykem (silné a bezpečné), zatímco volání metod inject jen konvencí (opomenutelné), tak najednou se ukazuje, že to není zcela pravda.

Další možnosti

Možností, která částečně obchází problém konstruktoru a dědičnosti, je použití třídy FooDependencies zmíněné v článku Dependency Injection versus Service Locator:

class FoobarDependencies
{
    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
}

class Foobar
{
    function __construct(FoobarDependencies $deps)
}

class Barbar extends Foobar
{
    function __construct(FoobarDependencies $deps, Logger $logger)
    {
        parent::__construct($deps);
        $this->logger = $logger;
    }
}

Když se změní závislosti rodičovské třídy Foobar, nemusí to nutně rozbít všechny potomky, protože se předávají v jedné proměnné. Běda ale, pokud ji předat zapomenou… Navíc tento způsob vyžaduje největší množství režijního kódu (dokonce celou režijní třídu).

Nebo lze závislosti rodičovské třídy Foobar předávat metodami a konstruktor uvolnit pro potomky. Rodič by se pak fakticky inicializoval až po volání těchto metod, takže konstruktor potomka by se volal nad neinicializovaným objektem. To není dobré.

A co obráceně, závislosti rodičovské třídy Foobar předávat konstruktorem a potomka metodami? To eliminuje všechny problémy, krom toho, že se těžko odhalí okamžik, kdy jsou nastaveny všechny závislosti (kvůli inicializaci objektu) a zda jsou vůbec nastaveny.

A co kdyby se všechny závislosti potomka nastavily jedinou metodou inject()? To by nejspíš vyřešilo všechny komplikace.

Nicméně stále jde jen o dvojstupňový případ rodič – potomek. Pro každého dalšího potomka by bylo třeba přijít s novou injektovací metodou a byl by problém zajistit, aby byly volány ve správném pořadí.

Dovedu si proto představit, že by vzniklo nové čisté řešení využívající nějaké PHP magie uvnitř třídy, která by ušetřila psaní režijního kódu, elegantně exponovala závislosti a předávala je do proměnných. Ty by mohly být označené třeba anotací @inject, nicméně šlo by o anotaci určenu pro tuto vnitřní implementaci, nikoliv o hint pro DI kontejner. Efekt by to mělo ve chvíli, kdyby se z toho stala obecněji uznávaná konvence, jinak to bude jen magie.

tl;dr

Předávání závislostí různými cestami má svá úskalí. Použití metod vyžaduje velké množství režijního kódu. Není od věci tyto metody pojmenovávat jiným prefixem, než obecné settery, kupříkladu lze použít inject. Poskytne to totiž důležitou informaci pro programátora, sekundárně ji může využít i DI kontejner.

Pokud nepoužíváte dědičnost, je zpravidla nejšikovnější závislosti předat skrze konstruktor a PHP by mohlo v příštích verzích syntaxi ještě o něco zjednodušit. Pokud ale do hry vstoupí dědičnost, je najednou všechno jinak. Ukazuje se, že dokonalý obecný mechanismus asi ani neexistuje. Možná by nebylo od věci zkusit nějaký, byť za využití PHP magie, vymyslet.


Všechny části:

Komentáře

  1. Samuel #1

    Možno nebude problém v odovzdávaní závislostí konštruktorom, ale v prílišnom (zne)užívaní dedičnosti v Nette a nedôslednom využití DI. Množstvo objektov v Nette si svoje závislosti vytvára, miesto toho, aby si ich pýtali (napr. Form).

    To má aj vedľajšie následky, ako nemožné unit testy presenterov, náročné upravovanie funkcionality k obrazu svojmu.

    Aby bolo jasné, Nette framework používam rád, mnoho vecí je dotiahnutých do detailov a user friendly. Len objektový návrh mi z tohto hľadiska nepríde najšťastnejší.

    před 5 lety | reagoval [3] David Grudl
  2. Jakub Tesárek http://www.tjsd.cz/ #2

    avatar

    Jak je zvykem, každý si z toho článku odnese co bude chtít. Já si odnesu „Nepoužívejte dědičnost, injektujte přes konstruktor“ a budu se potutelně usmívat, že jsem měl zas jednou pravdu.

    Jinak super článek. Škoda žes ho nenapsal už včera. Sháněl jsem do svého článku nějaký zdroj přesně o tomhle, na který bych mohl odkazovat pro další studiom o předávání závislostí.

    PS: S FooDependencies stále ještě nesouhlasím

    před 5 lety | reagoval [3] David Grudl
  3. David Grudl http://davidgrudl.com #3

    avatar

    #1 Samueli, Problém s DI a dědičností existuje, ať už se dědičnost zneužívá nebo využívá, každopádně článek není o Nette, tak to prosím nemíchejme.

    #2 Jakube Tesárku, Píšu ho měsíc :-)
    Každopádně s příchodem rozhraní a nejnověji i traitů se dědičnost stala zbytná.

    před 5 lety | reagoval [4] Samuel
  4. Samuel #4

    #3 Davide Grudle, Nette bolo v článku uvedené ako príklad zlyhania konštruktorového DI, polemizoval som len s tým, či by sa problém nedal odstrániť lepším návrhom.

    před 5 lety | reagoval [6] David Grudl
  5. Michal Gebauer http://mishak.net #5

    avatar

    Cokoli, co by dovolilo tento zápis, by bylo super.

    <?php
    class A {
        private $cache/* bind ICache required */;
    }
    ?>
    před 5 lety | reagoval [7] Filip Procházka (@HosipLan)
  6. David Grudl http://davidgrudl.com #6

    avatar

    #4 Samueli, Nette bylo uvedeno jako příklad kódu používajícího dědičnost. Problém by se dal vyřešit odstraněním dědičnosti z Nette.

    před 5 lety
  7. Filip Procházka (@HosipLan) #7

    avatar

    #5 Michale Gebauere, Porušovat DI? Tady nejsme v Javě.

    před 5 lety | reagoval [8] Michal Gebauer
  8. Michal Gebauer http://mishak.net #8

    avatar

    #7 Filipe Procházko (@HosipLan), To byl jen ideální kód. Technicky by se musel zaregistrovat dependency injector ala __autoload (nebo jeho SPI verze) a nebo rozšířit syntaxe pro vytvoření objektu:

    $a = new A/* injector $b*/;

    Právě kontrola závislostí a samotné sestavení by se nechalo na PHP popřípadě na __ready() a __bind($object) funkcích objektu.

    Bind u členské proměnné by automaticky vynucoval chráněnou nebo privátní viditelnost. (Spíš jako vynucení uzavřenosti objektu)

    před 5 lety
  9. v6ak http://twitter.v6ak.com #9

    avatar

    Opomenutí zavolání konstruktoru je logicky podobná chyba jako jeho vícenásobné volání, akorát se v PHP stane snadno.

    Pojmenované parametry jsou věcí jazyka (pak odpadne problém s pořadím), ale v PHP souhlas.

    Přidání povinného parametru byl BC break vždy a z principu to nejde jinak – máme zpětně nekompatibilní logiku. V případě setterů se akorát tato skutečnost skryje před případnou statickou typovou kontrolou, ale kód fungovat nemůže.

    Naopak přidání nepovinného parametru BC break není ani v případě konstruktoru a dědičnosti. Jen nebude možné nastavit nové parametry. Je ale otázka, jestli je to z hlediska potomka žádoucí. Obecně to vědět nemůžeme, takže je bezpečnější to neumožnit. Tady je fakt, že PHP toto nectí ve chvíli, kdy neuvedeme konstruktor. To ale neznamená, že je to dobře. Pro potomka mohou být některé varianty konstrukce nesmyslné.

    Pokud zrcadlení konstruktorů zabere příliš práce, možná se zneužívá dědičnosti.

    před 5 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.