Dependency Injection: motivace

Dependency Injection se stalo horkým tématem programátorských diskusí i na českých fórech. Někteří jej horlivě obhajují a jeho použitím vysvětlují různé konstrukce a doporučení, jiní jej považují třeba za „overkill“ – většinou z nepochopení. Vašek Purchart seznámí s DI všechny, co zatím tápou.

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

Návrhový vzor Dependency Injection (DI) slouží pro snížení závislostí mezi jednotlivými částmi systému. Jeho provedení vychází z obecnějšího návrhového vzoru Inversion of Control (IoC). Tyto dva pojmy jsou často zaměňovány – IoC je obecný princip, který upřednostňuje kontrolu zvenku objektů nad situací, kdy si objekt sám říká o věci v rámci svého kódu. Výsledkem je větší kontrola nad objekty a tento návrh zároveň umožňuje lepší znovupoužitelnost. DI je pojem, který poprvé představil Martin Fowler ve svém článku Inversion of Control Containers and the Dependency Injection pattern.

O seriálu

V tomto miniseriálu se postupně seznámíme s návrhovým vzorem Dependency Injection. Tento vzor sám o sobě nevypadá příliš složitě, důležitější je, jaké dopady má jeho důsledné dodržování na náš kód. Začneme se vzorovým příkladem, který by měl znázornit, proč je výhodné DI používat.

Příklad

Příkladem budiž prosté cachování dat – většinou je potřeba ho použít na několika místech v aplikaci a když už je jednou napsané, je vhodné ho sdílet i mezi více různými aplikacemi. Pravdou je, že je pravděpodobné, že tento problém za vás již vyřešil framework, se kterým pracujete, to ponechme stranou, zde jde pouze o ukázku principu.

Všechny příklady zde uváděné budou zapsány v PHP, ale návrhový vzor jako takový se samozřejmě na žádný konkrétní jazyk neváže. Uvedený kód berte pouze jako ilustrativní, důležité nejsou konkrétní implementace, pouze to, jak objekty skládáme dohromady a jak spolupracují.

Naivní implementace

Máme třídu FooService, která dělá nějakou činnost, o které si myslíme, že by bylo dobré její výsledky cachovat, protože výpočet je náročnější než prosté vyzvednutí již zpracovaných dat. Protože funkci cachování využijeme na více místech v projektu, vytvoříme si samostatnou třídu  Cache, která se bude o ukládání starat a bude zapouzdřovat operace s file systémem. Pro  cachování si vyhradíme složku cache v adresáři, který je označený konstantou TEMP_DIR.

class FooService {

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

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

    public function doSomethingUseful(Object $foo) {
        $index = ...
        if ($this->cache->exists($index)) {
            return $this->cache->read($index);
        } else {
            $result = $this->process($foo);
            $this->cache->write($index, $result);
            return $result;
        }
    }

    ...

}

class Cache {

    public function read($index) {
        return file_get_contents(unserialize(TEMP_DIR . '/cache/' . $index));
    }

    public function write($index, $data) {
        file_put_contents(serialize(TEMP_DIR . '/cache/' . $index));
    }

    public function exists($index) {
        return file_exists(TEMP_DIR . '/cache/' . $index);
    }

}

Výše uvedené řešení bude mít hned několik problémů, zejména:

  1. FooService je přímo závislá na konkrétním způsobu ukládání cache, nemůžeme jej tedy snadno vyměnit
  2. ve třídě Cache máme napevno napsané místo, do kterého budeme soubory ukládat

Zavedení interface

Chceme, aby FooService dokázala pracovat s libovolným způsobem ukládání dat, třídu Cache tedy proměníme na FileCache, která bude implementovat obecné rozhraní ICache. Druhý problém vyřešíme právě pomocí Dependency Injection – místo toho, aby třída FileCache rozhodovala, kam bude data ukládat, přenechá tuto povinnost tomu, kdo ji chce používat – to dá najevo požadováním předání cesty přes konstruktor.

class FooService {

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

    public function __construct() {
        $this->cache = new FileCache(TEMP_DIR . '/cache');
    }

    public function doSomethingUseful(Object $foo) {
        $index = ...
        if ($this->cache->exists($index)) {
            return $this->cache->load($index);
        } else {
            $result = $this->process($foo);
            $this->cache->save($index, $result);
            return $result;
        }
    }

    ...

}

interface ICache {

    public function load($index);

    public function save($index, $data);

    public function exists($index);

}

class FileCache implements ICache {

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

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

    public function load($index) {
        return file_get_contents(unserialize($this->dir . '/' . $index));
    }

    public function save($index, $data) {
        file_put_contents(serialize($this->dir . '/' . $index));
    }

    public function exists($index) {
        return file_exists($this->dir . '/' . $index);
    }

}

V této podobě můžeme v konstruktoru třídy FooService do proměnné cache vložit jakoukoli implementaci. Jak je ale vidět z ukázky, stále natvrdo vytváříme instanci jedné konkrétní implementace. Momentálně tedy není příliš znovupoužitelná třída FooService – nemůžeme ji používat s různými úložišti. To se týká i určení cesty, kam se budou data ukládat v případě použití FileCache.

Ještě o krok dál

Zopakujeme tedy stejný krok ještě jednou – na parametry, které nechceme sami pevně určit, se zeptáme v konstruktoru. V tomto kroku změníme už jen konstruktor třídy FooService (to je dobré znamení).

class FooService {

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

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

    // zbytek stejný jako dřív

}

Z příkladu nám úplně zmizelo určení nějakých konkrétních hodnot, což je stav, ke kterému jsme se chtěli probojovat. Ani jedna ze tříd nemá žádné skryté závislosti (tedy kdybychom chtěli jít do důsledků, tak je závislostí i konkrétní file systém). Pokud bychom chtěli námi vytvořené třídy použít, mohlo by to vypadat nějak takhle:

$fooService = new FooService(new FileCache(TEMP_DIR . '/cache'));

Je důležité udělat ten správný krok – požadovat v konstruktoru implementaci ICache. Pokud bychom se v tomto kroku místo toho rozhodli, že chceme mít možnost zvenku FooService pouze konfigurovat cestu k úložišti (ať už přes konstruktor, nebo setter), tak bychom zaváděli do FooService kód který s ní přímo nesouvisí. FooService by pak vyžadovala parametr, jehož jediným smyslem by bylo jeho přeposlání do další třídy (FileCache). Kromě toho parametr s cestou využívá jen FileCache, je to tedy krok směrem pryč od používání interface ICache.

Poznámka k použitému příkladu: Bylo by vhodné oddělit logiku spojenou s cachováním (např. invalidace) a samotným ukládáním dat. Ve výsledku by tedy cache vyžadovala v konstruktoru předání libovolného úložiště.

Závěr

V tomto, úvodním dílu jsme si názorně ukázali na konkrétním příkladu, co nás k používání Dependency Injection vede. Postupné úpravy vedly k zobecnění tříd a snížení závislostí v systému. Pokud se budeme principu DI držet, dojdeme do fáze, kdy budeme mít kompletně oddělené instancování konkrétních implementací a jejich využívání v aplikačním kódu. V následujících dílech se budeme DI zabývat dál do hloubky a také probereme záležitosti, které s jeho používáním souvisí v širším měřítku.

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

Komentáře: 94

Přehled komentářů

Radek Miček Proč nepoužít staticky typovaný jazyk
Landy Re: Proč nepoužít staticky typovaný jazyk
Radek Miček Re: Proč nepoužít staticky typovaný jazyk
manik.cze Re: Proč nepoužít staticky typovaný jazyk
Radek Miček Re: Proč nepoužít staticky typovaný jazyk
Jan Tichý Re: Proč nepoužít staticky typovaný jazyk
Radek Miček Re: Proč nepoužít staticky typovaný jazyk
Landy Re: Proč nepoužít staticky typovaný jazyk
manik.cze Re: Proč nepoužít staticky typovaný jazyk
kruppi Re: Proč nepoužít staticky typovaný jazyk
Aleš Roubíček Re: Proč nepoužít staticky typovaný jazyk
manik.cze Re: Proč nepoužít staticky typovaný jazyk
Opravdový odborník :-) Re: Proč nepoužít staticky typovaný jazyk
Radek Miček Alternativa k DI
Opravdový odborník :-) chro chro
fos4 Re: chro chro
manik.cze Re: chro chro
HosipLan Re: chro chro
R! Re: chro chro
mark8468464 Re: chro chro
Tharos Re: chro chro
kruppi Re: chro chro
Opravdový odborník :-) Konvence Impl vs. I
Opravdový odborník :-) Re: Konvence Impl vs. I
HosipLan Re: Konvence Impl vs. I
Opravdový odborník :-) Re: Konvence Impl vs. I
Michal Illich Re: Konvence Impl vs. I
Jan Kodera Re: Konvence Impl vs. I
mark8468464 Re: Konvence Impl vs. I
Radek Miček Re: Konvence Impl vs. I
Martin Soušek Re: Konvence Impl vs. I
Opravdový odborník :-) Re: Konvence Impl vs. I
Tharos Re: Konvence Impl vs. I
Opravdový odborník :-) Re: Konvence Impl vs. I
anonym Re: Konvence Impl vs. I
Opravdový odborník :-) Re: Konvence Impl vs. I
www.google.com Re: Konvence Impl vs. I
Ondřej Mirtes Re: Konvence Impl vs. I
Zaboj Campula Re: Konvence Impl vs. I
dusanmsk Re: Konvence Impl vs. I
arnold Re: Konvence Impl vs. I
msk Re: chro chro
mark8468464 Re: chro chro
harvejs dik za super clanok
Radek Miček Re: dik za super clanok
Ondřej Mirtes Re: dik za super clanok
zomp Re: dik za super clanok
Andrew Re: dik za super clanok
v6ak Re: dik za super clanok
František Kučera Výjimky vs. návratové hodnoty
uživatel Pravdou je, že je pravděpodobné,
kruppi Re: Pravdou je, že je pravděpodobné,
Fanoušek A. Denta Hrůza
František Kučera Re: Hrůza
Lopata Mimochodem ne tak docela
. Re: Dependency Injection: motivace
Aleš Roubíček Nevhodný příklad
manik.cze Re: Nevhodný příklad
Jan Tichý Re: Nevhodný příklad
Fanoušek A. Denta Re: Dependency Injection: motivace
František Kučera Šachy
Fanoušek A. Denta Re: Šachy
Leoš statické typování
Tharos Re: statické typování
Jirka V. Re: statické typování
asdasd Re: statické typování
stydim se Re: statické typování
stydim se Re: statické typování
asdasd Re: statické typování
David Grudl Re: statické typování
Radek Miček Re: statické typování
Radek Miček Re: statické typování
kruppi Re: statické typování
Radek Miček Re: statické typování
David Grudl Re: statické typování
manik.cze Re: statické typování
Radek Miček Re: statické typování
Opravdový odborník :-) Spring?
Radek Miček Re: Spring?
Sid Re: Spring?
Radek Miček Re: statické typování a testování
Radek Miček Re: statické typování a testování
Jan Tichý Re: statické typování a testování
Radek Miček Re: statické typování a testování
kruppi Re: statické typování a testování
Radek Miček Re: statické typování a testování
asdasd Re: statické typování a testování
martin Re: statické typování a testování
v6ak Re: statické typování
v6ak Re: statické typování
Pavel Šimerda (pavlix) zvláštní...
v6ak Re: zvláštní...
Aleš Roubíček Re: zvláštní...
defk Re: Dependency Injection: motivace
Zdroj: https://www.zdrojak.cz/?p=3504