Dependency Injection: předávání závislostí

V dnešním pokračování seriálu o Dependency Injection si představíme různé varianty (Constructor injection, Setter injection, Interface injection či Property Injection), popíšeme jejich principy, zhodnotíme výhody a nevýhody, popíšeme dopady na kód, a pro srovnání si ukážeme i vzor Service Locator.

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

minulém díle seriálu jsme si na příkladu ukázali, proč bychom měli DI používat. Připomeňme si to sérií diagramů, kde jsou naznačené změny v závislostech našeho systému. Třída Assembler na obrázcích reprezentuje místo, kde se sestavují objekty do té podoby, ve které je chceme ve výsledku používat.

Z diagramů je patrné, že s tím, jak postupně zobecňujeme vztah mezi FooService a Cache, se požadavky na konfiguraci přesouvají na „klienta“ – v tomto případě objekt Assembler. To, jak se v Assembleru budou objekty sestavovat, je dáno zvoleným způsobem předávání závislostí do objektů. K dispozici je nám konstruktor, metody a properties objektů.

Varianty Dependency Injection

Constructor injection

Při použití Constructor Injection definujeme závislosti třídy v konstruktoru, a tím vyžadujeme, aby bylo předáno vše, co objekt potřebuje ke svému životu. Tento typ DI jsme použili také v minulém dílu seriálu.

class FooService {

    /** @var ICache */
    private $cache;

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

    ...

}

class FileCache implements ICache {

    /** @var string */
    private $dir;

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

    ...

}

class Assembler {

    public function createFooService() {
        return new FooService(new FileCache(TEMP_DIR . '/cache'));
    }

}

Setter Injection

Při použití Setter Injection využíváme pro předání závislostí metod, které jsou pojmenované podle konvence, tzv.  settery:

class FooService {

    /** @var ICache */
    private $cache;

    public function setCache(ICache $cache) {
        $this->cache = $cache;
    }

    ...

}

class FileCache implements ICache {

    /** @var string */
    private $dir;

    public function setDir($dir) {
        $this->dir = $dir;
    }

    ...

}

class Assembler {

    public function createFooService() {
        $cache = new FileCache();
        $cache->setDir(TEMP_DIR . '/cache');
        $fooService = new FooService();
        $fooService->setCache($cache);
        return $fooService;
    }

}

Interface Injection

U Interface Injection budeme, podobně jako u Setter Injection, definovat metody pro předání parametrů do objektu, tentokrát ale jejich jméno určíme pomocí speciálních rozhraní:

interface InjectCache {

    public function injectCache(ICache $cache);

}

interface InjectDir {

    public function injectDir($dir);

}

Objekty, do kterých chceme závislosti předávat, tato rozhraní implementují:

class FooService implements InjectCache {

    /** @var ICache */
    private $cache;

    public function injectCache(ICache $cache) {
        $this->cache = $cache;
    }

    ...

}

class FileCache implements ICache, InjectDir {

    /** @var string */
    private $dir;

    public function injectDir($dir) {
        $this->dir = $dir;
    }

    ...

}

class Assembler {

    public function createFooService() {
        $cache = new FileCache();
        $cache->injectDir(TEMP_DIR . '/cache');
        $fooService = new FooService();
        $fooService->injectCache($cache);
        return $fooService;
    }

}

Property Injection

Místo použití metod bychom mohli používat i properties třídy. Při použití standardních OOP prostředků bychom ale všechny property, do kterých chceme zapisovat, museli definovat jako public. Ukázky by vypadaly velmi podobně jako u Setter Injection.

Pokud chceme properties ponechat definované jako privátní, můžeme pro dosažení cíle použít například reflexi. Tentokrát v ukázce nepoužijeme kód třídy Assembler, protože tento způsob DI bychom pravděpodobně při „manuálním“ sestavování objektů nevyužívali. Navíc je implementace silně závislá na zvoleném jazyku. Zápis ostatních tříd může vypadat např. takto:

class FooService {

    /**
     * @Inject
     * @var ICache
     */
    private $cache;

    ...

}

class FileCache {

    /**
     * @Inject
     * @var string
     */
    private $dir;

    ...

}

Dopady DI na kód

Dependency Injection by měl být pouze způsob, jak sestavovat objekty dohromady, měl by mít proto minimální požadavky na úpravu kódu (za předpokladu, že kód píšeme čistě). Největším porušením návrhu objektů je pravděpodobně využití Property Injection.

U verze s private properties z veřejného rozhraní třídy vůbec není zřejmé, jaké má závislosti (zdánlivě žádné). Při psaní aplikace to nemusíme pocítit – o závislosti se postará nějaké automatizované řešení, ale ve chvíli, kdy budeme chtít napsat např. unit test a předat namockované objekty, se dostaneme do úzkých.

Verze s public properties porušuje zapouzdření objektu. Kromě toho z rozhraní není jasné, které properties musíme inicializovat, aby objekt mohl vůbec fungovat. Stejný problém má i Setter Injection a Interface Injection. Ukažme si tento problém právě na Setter Injection:

class FooServiceTest extends TestCase {

    public function test() {
        $fooService = new FooService();   // konstruktor objektu nepožaduje žádné parametry
        $fooService->doSomethingUseful(); // způsobí PHP Fatal error: Call to a member function on a non-object
    }

}

Objekt tedy lže o svých závislostech – zdánlivě nic nepožaduje, ale při použití se pak dozvíme pravý opak a musíme se vrátit ke studiu rozhraní, případně kódu třídy, abychom zjistili, jaké všechny proměnné musíme před použitím inicializovat. Souvisejícím problémem je, že zveřejněné metody/properties může kdokoli volat opětovně, nemáme tedy nikde jistotu, že v půlce práce s objektem někdo nezmění hodnotu, která byla uvedena při inicializování.

Oba tyto problémy pak vedou ke konstrukcím uvnitř třídy, které kontrolují, zda byla hodnota už nastavena (např. kvůli čitelnějším výjimkám pro programátora než je Fatal Error), resp. jestli se ji nepokouší někdo změnit, když už jednou nastavena byla. U Interface Injection musíme ještě počítat s tím, že pro každou závislost, kterou plánujeme někam injectovat, musíme vyrobit samostatný interface, a ten pak v těchto třídách implementovat.

Constructor Injection netrpí ze své podstaty žádným z výše uvedených problémů – není vlastně možné třídu instancovat v nekonzistentní podobě, ani ji do ní později dostat. Constructor Injection má ale také své problémy. Pokud máme více způsobů, jak daný objekt sestavit, nemáme v PHP mnoho možností (např. v Javě lze používat overloading a vyrobit více různých konstruktorů) – pravděpodobně bychom se museli uchýlit k nepovinným parametrům konstruktoru, což může vést opět k situaci, kde není jasné, jaké kombinace parametrů musíme předat. Pro zpřehlednění je pak vhodné vyrobit např. factory třídu, nebo použít factory metody, které různé způsoby vzniku objektu pokryjí.

Navzdory výše zmíněnému riziku Constructor Injection preferuji, protože má nejmenší vliv na přirozené psaní tříd a poskytuje ochranu před nekonzistentním stavem. – pozn.aut.

Není také žádná povinnost si vybrat pouze jeden způsob, dost často to ani není možné. Především při používání kódu třetích stran (knihovny, frameworky) nemáme možnost ovlivnit jejich podobu. Tedy částečně máme, můžeme využít např. návrhového vzoru Adapter.

Service Locator

Pro srovnání s jednotlivými typy DI si ještě krátce představíme alternativní implementaci IoC – vzor ServiceLocator (SL). Stejně jako DI slouží ke snížení závislostí mezi požadovanou funkčností a konkrétní implementací. Pracuje tak, že máme nějakou třídu, která v sobě nese informaci o tom, jaké rozhraní je implementováno jakou třídou (tzv. kontejner). Pokaždé, když v nějaké třídě potřebujeme přistoupit k jiné, požádáme SL, aby nám vydal příslušný objekt. V našem příkladě by tedy u třídy FooService zmizela závislost na konkrétní implementaci cache, zůstala by závislost na rozhraní ICache, ale přibyla by závislost na rozhraní Service Locatoru (srovnejte s diagramy v úvodu článku).

Následuje ukázka, jak by mohl vypadat FooService v případě, že je Service Locator implementován jako třída se statickými metodami pro výdej objektů (opět silně zjednodušený příklad).

class FooService {

    /** @var ICache */
    private $cache;

    public function __construct() {
        $this->cache = ServiceLocator::getCache();
    }

    ...

}

class FileCache implements ICache {

    /** @var string */
    private $dir;

    public function __construct() {
        $this->dir = ServiceLocator::getCacheDir();
    }

    ...

}

class ServiceLocator {

    public static function getCache() {
        return new FileCache();
    }

    public static function getCacheDir() {
        return TEMP_DIR . '/cache';
    }

    ...

}

class Assembler {

    public function createFooService() {
        return new FooService();    // ale předpokládá, že je nějak nakonfigurovaný SL
    }

}

Rozdílem mezi oběma představenými implementacemi IoC je to, že v případě DI třída pouze deklaruje, která rozhraní potřebuje, a to, jakým způsobem a kdy je dostane, nijak neovlivňuje. Naopak u tříd využívajících SL vždy voláme kontejner na konkrétním místě. Velkou nevýhodou také je, že každá třída, která SL využívá, je závislá na jeho rozhraní (případně můžeme definovat částečná rozhraní pro jednotlivé služby, které SL vydává, ale závislost stejně zůstává). Pokud máme vytvořit nějaké hodně obecné třídy, tato závislost v nich zůstane, a třídy budou svázané s tímto kontejnerem.

Z hlediska dopadů na kód, které byly diskutovány v předchozích částech, je SL jednoznačně invazivnější než DI. S tím souvisí i čitelnost kódu – u Dependency Injection, zvláště pokud použijeme Constructor Injection metodu, vidíme zcela přesně všechny závislosti, které třída má, na první pohled. U Service Locatoru bychom pro dosažení stejného cíle museli prohlédnout celý kód třídy, protože volání Service Locatoru se může vyskytnout kdekoli v kódu (v ukázce je volán pouze v konstruktorech, to ale nemůžeme nikde zaručit).

V praxi se kromě „statického“ SL kontejneru můžeme setkat i s variantou, kdy je kontejner předáván do každého objektu, který má nějaké závislosti (bývá označen názvy jako Context nebo Container).

Závěr

Účelem dnešního článku bylo především zmapovat možné způsoby předávání závislostí do objektů a jejich srovnání z hlediska dopadu na kód. Dosud jsme mluvili o DI v teoretičtější rovině a všechny objekty jsme „manuálně“ skládali. To je v kontextu nasazení DI napříč celou aplikací neúnosné, a tak si v dalším díle ukážeme způsob, jak si tuto práci ulehčit.

Komentáře: 34

Přehled komentářů

smilelover SL neni IoC ani DI!
Jan Tichý Re: SL neni IoC ani DI!
alefo Re: SL neni IoC ani DI!
Aleš Roubíček Re: SL neni IoC ani DI!
manik.cze Re: SL neni IoC ani DI!
Jan Tichý Re: SL neni IoC ani DI!
Aleš Roubíček Re: SL neni IoC ani DI!
manik.cze Re: SL neni IoC ani DI!
Aleš Roubíček Re: SL neni IoC ani DI!
manik.cze Re: SL neni IoC ani DI!
Aleš Roubíček Re: SL neni IoC ani DI!
manik.cze Re: SL neni IoC ani DI!
Aleš Roubíček Re: SL neni IoC ani DI!
manik.cze Re: SL neni IoC ani DI!
Aleš Roubíček Re: SL neni IoC ani DI!
Nox Re: SL neni IoC ani DI!
Nox Re: SL neni IoC ani DI!
Michal Augustýn Re: SL neni IoC ani DI!
martin Re: SL neni IoC ani DI!
Aleš Roubíček Re: SL neni IoC ani DI!
Michal Kára Spring
Jan Tichý Re: Spring
alefo Re: Spring
Michal Illich Re: Spring
uživatel Springu Re: Spring
alefo Re: Spring
X Re: Spring
Michal Kára Re: Spring
seberm Pěkný článek
Gaudentius a co type hinting?
Ondřej Mirtes Re: a co type hinting?
Gaudentius Re: a co type hinting?
Aleš Roubíček Re: a co type hinting?
Ondřej Mirtes Re: a co type hinting?
Zdroj: https://www.zdrojak.cz/?p=3508