Dependency Injection: kontejner

V ukázkách jednotlivých typů DI v minulém dílu seriálu byly objekty vždy sestavovány manuálně. Kód potřebný pro sestavování je ale obdobný pro všechny případy, a tak tuto práci můžeme u Dependency Injection přenechat tzv. kontejneru, kterému pouze poskytneme konfiguraci. Ukažme si, jak s ním pracovat.

Seriál: Jak na Dependency Injection (3 díly)

  1. Dependency Injection: motivace 13.6.2011
  2. Dependency Injection: předávání závislostí 20.6.2011
  3. Dependency Injection: kontejner 12.7.2011

Kontejner funguje vlastně tak, že sestaví a instancuje celý strom závislostí ještě předtím, než se samotná aplikace spustí. Respektive skládání začne v momentu, kdy kontejner požádáme o první třídu. Můžeme si to představit na následujícím příkladu aplikace: mějme Controller, ve kterém budeme potřebovat třídu FooService. Konstruktory jednotlivých tříd tedy budou vypadat nějak takto:

class Controller {

    public function __construct(FooService $fooService) {...}

    ...

}

class FooService {

    public function __construct(ICache $cache) {...}

    ...

}

class FileCache implements ICache {

    public function __construct($dir) {...}

    ...

}

Abychom mohli spustit (tzn. nejdříve instancovat) Controller, musíme předat všechny potřebné parametry – budeme vlastně postupovat opačně, zjišťovat závislosti a postupně sestavovat strom. Nejdříve musíme instancovat FileCache, které předáme do parametru string se zvolenou cestou. FileCache pak můžeme vložit do FooService a tu teprve do Controlleru. Toto je samozřejmě velmi jednoduchý případ, v reálné aplikaci bude vzniklý strom daleko rozvětvenější. K tomu, aby za nás byl schopný tuto práci udělat kontejner, mu musíme předat nějakou konfiguraci.

Konfigurace

Konfigurace kontejneru je místo, kde můžeme pro každou třídu určit, jak se z ní budou vytvářet objekty – např. jaké konkrétní parametry dostane (volba konkrétní implementace určitého rozhraní). Konfigurace může mít prakticky dvě podoby: První možností je tuto konfiguraci provést přímo pomocí programového kódu, nebo můžeme konfiguraci načítat z konfiguračního souboru a pak jej zpracovávat.

Výhodou konfiguračních souborů by mělo být především vyšší přehlednost a čitelnost. Teoreticky by měl být schopný upravovat konfigurační soubor i někdo, kdo není programátor, což se ale asi moc často stávat nebude. U kompilovaných systémů může být zásadnější výhodou, že nemusíme znovu kompilovat aplikaci, pokud je potřeba pouze upravit konfiguraci (např. vyměnit použití jedné implementace určitého rozhraní za jinou). Pokud je konfigurace kratší, může být výhodnější zapsat ji pomocí kódu. Nebo naopak pokud je konfigurace velmi komplexní a obsahuje nějakou podmiňovací logiku, pak není příliš vhodné takto „programovat“ v konfiguračním souboru.

Ve výsledku tato volba připadá na konkrétní situaci a preference programátorů. Ostatně pokud kontejner podporuje nastavení pomocí programového kódu, není výrazný problém doimplementovat vlastní podporu pro jakýkoli typ konfiguračního souboru – například XML nebo YAML.

Následuje čistě ilustrativní příklad, jak by mohla vypadat konfigurace ve formátu YAML pro sestavení výše zmíněných objektů:

Controller:
    - arguments: [@FooService]
FooService:
    - arguments: [@FileCache]
FileCache:
    - arguments: [%tempDir%/cache]

Autowiring

Autowiring je název mechanismu, který by měl ulehčit konfiguraci DI kontejnerů. V různých implementacích nabízí různé možnosti, ale několik jich má společných – především ušetřit programátorovi psaní.

Obecně autowiring funguje tak, že se sám kontejner snaží uhodnout určité parametry. Většinou je využita reflexe pro zjištění informací z kódu – kontejner si může přečíst např. jaké třídy/rozhraní jsou očekávány v parametrech konstruktoru a dalších metod. Pokud je možné jednoznačně určit, co se má předat do daného parametru, není potřeba tyto informace duplikovat v konfiguraci.

Může dojít ale i k situacím, kdy to jasné být nemusí. Tato situace může nastat, pokud parametr vyžaduje objekt typu definovaného rozhraním, ale toto rozhraní má v systému více implementací – kontejner by pak nevěděl, kterou implementaci dosadit. Dalším problémem pro autowiring budou např. parametry obecných typů – string, integer apod. Bude se jednat pravděpodobně o různé konfigurační direktivy, kontejner by se mohl pokusit uhádnout, o jakou direktivu jde, pomocí jména proměnné, ale to už je značně nespolehlivé.

Autowiring tedy nemá žádnou standardně definovanou podobu, ale vždy záleží na konkrétním řešení. Vrátíme-li se k ukázce z předchozí sekce, mohli bychom ho za použití autowiringu, který je schopný dosazovat implementace požadovaných tříd, přepsat například takto:

Controller:
    - autowiring: true
FooService:
    - autowiring: true
FileCache:
    - arguments: [%tempDir%/cache]

Případně bychom mohli definice tříd, které autowiring používají, úplně vynechat. Každopádně v případě, že bychom chtěli do konstruktoru třídy Controller nebo FooService přidat nějaký další parametr (nebo upravit stávající), úpravy provedeme právě na jednom místě – pouze v kódu dané třídy, o zbytek se opět postará kontejner.

Konfigurace kontejneru může obsahovat celou řadu dalších možností, kromě autowiringu se často vyskytuje možnost zvolit, jestli se bude třída pro každé použití (předání jako závislosti do dalšího objektu) vytvářet znovu, nebo zda se použije pokaždé ta samá instance. Tím se dá snadno dosáhnout podobného výsledku, jakým by bylo použití Singletonu: všude v aplikaci pracujeme s tou samou instancí, ale bez nepříjemných jevů, které Singleton často provázejí – jde o globální stav, který je tedy velmi špatně testovatelný.

Services vs. Value Objects

Pokud se podíváme na objekty, které tvoří naše aplikace, můžeme zde nalézt dva základní druhy – objekty, které jsou zajímavé tím, že především vyjadřují svojí hodnotu (skalární typy jako string, integer, ale také doménové objekty) – ty budu označovat jako Value Objects. Druhým typem jsou objekty, jejichž vnitřní stav není důležitý a jejich práce spočívá v manipulaci s Value Objects.

V zásadě tyto dva typy objektů není dobré „míchat“ dohromady na úrovni závislostí. Value Objects by měly být základní kameny, na kterých jsou postavené další vrstvy, kde se nacházejí Services. Pokud předáváme jako závislost objekt stejného typu, neměli bychom narazit na žádný problém. K případům, kdy bychom potřebovali Service vložit do Value Object, by pravděpodobně docházet nemělo, protože Services jsou z vyšší vrstvy. K opačnému případu – vkládání Value Object do Service by mohlo docházet častěji.

Ostatně v zde uvedeném příkladu parametr $dir třídy FileCache je typu string a tudíž Value Object, třídu FileCache bych označil za Service. V našich příkladech jsme na žádný problém nenarazili, protože jsme hodnotu adresy měli uloženou v konfiguraci a tak byla známá již v době sestavování stromu závislostí. Hodnotu Value Objects budeme dost často znát až za běhu aplikace, typicky na základě vstupu od uživatele. Představme si, že máme objekt File, reprezentující jeden soubor na disku, v konstruktoru požaduje pouze adresu daného souboru:

class File {

    public function __construct($dir) {...}

    ...

}

Pokud by na instanci File závisel některý objekt, ale hodnota by nebyla dostupná v době spuštění aplikace, tak můžeme místo toho předat Factory třídu a samotná instance se vyrobí až v momentě, když je skutečná hodnota potřebná pro inicializaci Value Objectu známa.

class FileFactory {

    public function create($dir) {
        return new File($dir);
    }

}

Alternativou k použití Factory by bylo v místě volání metody create použít prosté new File($dir). Výhodou použití Factory je opět oddělení místa, kde se pracuje s konkrétními instancemi od jejich vytváření – stejně jako jsme se o to snažili v minulých dílech. Praktickým důsledkem je pak, že se vnitřní implementace FileFactory může kdykoli změnit bez zásahu do všech tříd, které ji používají (FileFactory si např. může vyžádat v konstruktoru nějaké další pomocné parametry, Services apod.) nebo můžeme později FileFactory převést na interface a používat více různých implementací této factory, kdy každá bude objekty vytvářet odlišným způsobem (nebo vytvářet např. specializované potomky původního Value Object). Factory třídy samozřejmě nemá cenu používat všude, je velmi nepravděpodobné, že bychom například potřebovali vyměnit implementaci wrapper objektů jako String nebo Integer (na to v seriálu s PHP příklady asi příliš nenarazíme).

Závěr

V tomto díle jsme si ukázali některé možnosti, které nám nabídne DI kontejner, především kde nám dokáže usnadnit práci. Jak jsem již výše zdůrazňoval, jednotlivé implementace se liší, ale v zásadě nabízejí podobné schopnosti, záleží tedy na programátorovi, co si vybere, případně jestli dané řešení umí nějak vhodně spolupracovat s jeho oblíbeným frameworkem. Ve svých příkladech jsem používal pouze Constructor Injection, DI kontejnery ale většinou nabízejí možnosti konfigurace, které dokáží pracovat i s ostatními typy DI.

Na závěr bych rád podotkl, že DI je ve své podstatě velmi primitivní návrhový vzor – „Prostě triviální pospojování objektů.“ pomocí předávání parametrů, jak bylo řečeno v diskuzi u prvního článku. Ano, nezbývá než souhlasit. Podstatné je, že pokud tento vzor začnete používat skutečně důsledně napříč celou aplikací, tak změnu jistě poznáte. Doufejme, že k lepšímu. Vaše aplikace by měly být minimálně lépe dekomponované, znovupoužitelné a testovatelné.

V tomto článku jsme se o kontejnerech bavili obecně, v jazycích jako Java jsou již na denním pořádku a v poslední době si je můžete vyzkoušet i v PHP. Například v Symfony, Morphine či v Nette.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 7

Přehled komentářů

Martin Re: Dependency Injection: kontejner
f4s konfigurace
Vsehomir Re: konfigurace
f4s Re: konfigurace
Cechjos Re: konfigurace
anonym Re: Dependency Injection: kontejner
anonym Re: Dependency Injection: kontejner
Zdroj: https://www.zdrojak.cz/?p=3516