PHP přistupuje ke třídám způsobem známým ze staticky
typovaných jazyků a neumožňuje monkey patching, tedy
měnit za běhu metody tříd, kopírovat je mezi instancemi a podobně.
Abyste porozuměli, co mám na mysli, vytvořme třídu Greeting
s metodou say():
class Greeting
{
function __construct($name)
{
$this->name = $name;
}
function say($message)
{
echo "$message $this->name.";
}
}
$g = new Greeting('John');
$g->say('Hello'); // Hello John.
V PHP neočekáváme, že by bylo možné metodu třeba uložit do
proměnné či jiného atributu a poté zase zavolat:
Na jedné straně je mi líto, že tohle PHP neumí, na straně druhé vidím
ve statickém pojetí tříd podstatné výhody. A na straně třetí: v PHP
lze tohle chování snadno emulovat.
Emulace dynamiky
Vytvořit funkci jako je výše uvedená shout() a vložit ji do
proměnné objektu PHP už umí od verze 5.3. Ale abychom ji mohli zavolat
běžným zápisem, musíme si vypomoci magickou metodou
__call():
class Greeting
{
function __call($name, $args)
{
if (!isset($this->$name) || !$this->$name instanceof Closure) {
throw new Exception("Method $name not found.");
}
return call_user_func_array($this->$name->bindTo($this, $this), $args);
}
...
}
A nyní už bude příklad s metodou shout() fungovat.
Abychom mohli stejně nakládat i se statickými metodami, jako byla třeba
výše uvedená say(), doplníme ještě __get():
class Greeting
{
function __get($name)
{
if (!method_exists($this, $name)) {
throw new Exception("Property $name not found.");
}
return function() use ($name) {
return call_user_func_array(array($this, $name), func_get_args());
};
}
...
}
A nyní bude fungovat i první příklad s přiřazením
$method = $g->say a následným voláním.
Pro verzi 5.3
Uvedené příklady vyžadují PHP 5.4. Ve verzi 5.3 jsou closures
ořezané a nesmí se v nich používat $this. Řešení by
vypadalo trošičku jinak:
// for PHP 5.3
class Greeting
{
function __call($name, $args)
{
if (!isset($this->$name) || !$this->$name instanceof Closure) {
throw new Exception("Method $name not found.");
}
array_unshift($args, $this);
return call_user_func_array($this->$name, $args);
}
function __get($name)
{
if (!method_exists($this, $name)) {
throw new Exception("Property $name not found.");
}
return function() use ($name) {
$args = func_get_args();
return call_user_func_array(array(array_shift($args), $name), $args);
};
}
...
}
A namísto $this bychom uvnitř closure použili první
argument, pojmenovaný třeba $self.
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. Co kdyby se závislosti předávaly přímo do
proměnných? Proberu úskalí a výhody property injection.
Property injection má jednu podstatnou výhodu: stručnost. Srovnejte:
class Foobar
{
/** @var HttpRequest @inject */
public $httpRequest;
/** @var Router @inject */
public $router;
}
Proměnné musíme definovat tak jako tak. Zpravidla u nich uvádíme
i anotaci @var a příslušný datový typ. Je lákavé si
ušetřit práci a místo psaní rutinního kódu konstruktoru nebo metody
inject() doplnit prosté @inject. Property injection
kromě minimálního režijního kódu navíc parádně řeší problém s předáváním
závislostí a dědičností.
Použití anotace představuje jinou konvenci pro předání
závislostí. Zdůrazňuji slovo jinou, protože ať už vyjmenujeme závislosti
jakožto argumenty metody nebo anotováním, jde o ekvivalentní činnost.
Čímž oponuji
názoru, že použití anotace představuje závislost na kontejneru. To
v žádném případě není pravda, jde jen o konvenci, koneckonců dosud
o kontejnerech nepadla řeč a ukázky dávají smysl.
Stejně tak se nedívejte na anotaci @inject jako nějakou odpornou
magii, kterou musíte nastudovat, abyste ji mohli používat. Žádná magie
tu není. Jde o obyčejné veřejné proměnné a anotace je jen doplňující
informace pro programátora, říkající, že objekt vyžaduje tyto proměnné
naplnit. (Nutno dodat, že Jakub Vrána reagoval na použití anotací
u private proměnných, což magie je.)
V článku o předávání
závislostí jsem se používání proměnných širokým obloukem vyhnul,
protože mají vážné nedostatky:
public proměnné nezajistí typovou kontrolu
public proměnné nezajistí neměnnost
private proměnné nelze naplnit žádnou jazykovou konstrukcí
private proměnné nejsou součástí veřejného API – nejde tedy
o deklaraci závislosti!
pro protected proměnné platí nevýhody obou
Ještě bych přidal, že anotace nejsou nativní součástí jazyka PHP a
jde tedy o nestandardní konvenci, oproti třeba injektáži přes
konstruktor.
Poznámka: vstřikování závislostí do privátních proměnných
posvětila třeba Java EE 6 a je to skutečně ee. Třída své závislosti
tají (private = neveřejný) a nelze ji instancovat jinak, než kontejnerem
(závislost na kontejneru). Jde zcela proti smyslu Dependency Injection, jak je
popsán v perexu tohoto článku, a také proti základnímu principu OOP,
zapouzdření. Označil bych to jako „Inversion of Dependency
Injection.“
Pro properly property injection bychom potřebovali once-write-only
veřejnou proměnnou s typovou kontrolou. Kdyby tohle PHP umělo, nic by
nebránilo je používat. Jenže PHP to neumí.
Emulace inject property
PHP to neumí, ale lze to emulovat!
Emulaci zajistíme pomocí magických metod __set a
__get. Jak ale dosáhnout toho, aby se k public proměnné
přistupovalo skrze tyto metody? Použijeme trik: v konstruktoru ji
unsetneme. Proměnná zmizí a při přístupu k ní se již použijí
magické metody.
Příklad implementace ve formě základní třídy Object by mohl vypadat
třeba takto:
class Object
{
private $injects = array();
function __construct()
{
// následující analýza proměnných by se mohla kešovat
$rc = new ReflectionClass($this);
foreach ($rc->getProperties() as $prop) {
if ($prop->isPublic() && strpos($prop->getDocComment(), '@inject')
&& preg_match('#@var\s+(\S+)#', $prop->getDocComment(), $m)
) {
// unset property to pass control to __set() and __get()
unset($this->{$prop->getName()});
$this->injects[$prop->getName()] = array('value' => null, 'type' => $m[1]);
}
}
}
function __set($name, $value)
{
if (!isset($this->injects[$name])) {
throw new Exception("Cannot write to an undeclared property $$name.");
} elseif ($this->injects[$name]['value']) {
throw new Exception("Property $$name has already been set.");
} elseif (!$value instanceof $this->injects[$name]['type']) {
throw new Exception("Property $$name must be an instance of {$this->injects[$name]['type']}.");
} else {
$this->injects[$name]['value'] = $value;
}
}
function __get($name)
{
if (!isset($this->injects[$name])) {
throw new Exception("Cannot read an undeclared property $$name.");
}
return $this->injects[$name]['value'];
}
}
pak stačí deklarovat výše uvedenou třídu Foobar jako
potomka Object a vše bude fungovat standardně podle
očekávání:
class Foobar extends Object
{
/** @var HttpRequest @inject */
public $httpRequest;
/** @var Router @inject */
public $router;
}
$fb = new Foobar;
$fb->router = new Router;
Navíc však máme zajištěnou neměnnost a typovou kontrolu:
$fb->router = new Router;
// Exception: Property $router has already been set.
$fb->httpRequest = new Router;
// Exception: Property $httpRequest must be an instance of HttpRequest.");
Čistá cesta nebo prasárna?
Zkusme se zamyslet nad tím, co vlastně anotace @inject
představuje: hint pro programátora, že proměnnou má při vytváření
objektu nastavit a že ji později nesmí měnit. Anotace @var pak
nařizuje typ.
Je na programátorovi, aby dodržel kontrakt. Stejně jako v případě
anotace @private v PHP 4 nebo JavaScriptu,
či anotace @return v současném PHP. Jde o pravidla, u nichž
se předpokládá, že je programátor dodrží, aniž to lze na úrovni
interpreteru ověřit.
Třída Object rozšiřuje PHP o schopnost kontroly za běhu, usnadní tedy
identifikaci chyb. Je to vychytávka navíc. Z mého pohledu tedy
akceptovatelná cesta k použití property injection v PHP. Možná by se dalo
uvažovat nad zařazením do Nette\Object a legitimizace této injektáže
v Nette.
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.
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:
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.
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.
Když se mluví o Dependency Injection, bývá zmiňován
i service locator, jako jakési zlé dvojče. O co se vlastně jedná?
Dependency Injection jsem v prvním
článku popsal jako zřejmé předávání závislostí, tedy že se
každá třída hlásí ke svým závislostem v konstruktoru nebo jiné
metodě, místo toho, aby je někde v těle pokoutně získávala
z globálního přístupového bodu. Dodržování tohoto principu vede ke
srozumitelnějšímu a předvídatelnějšímu kódu.
Nicméně na vás číhá nástraha v podobě service locatoru.
Service locator je velechytrý objekt, který umí vrátit veškeré
závislosti, které třída potřebuje, nebo i nepotřebuje. Pokud by si
všechny třídy předávaly jeden service locator, předaly by si tak
v jediném parametru všechny závislosti.
class Authenticator
{
private $locator;
function __construct(ServiceLocator $locator)
{
$this->locator = $locator;
}
function authenticate($user, $pass)
{
$database = $this->locator->getDatabase();
$database->query(...)
}
}
Bohužel Service Locator není v souladu s DI.
Proč? Není v souladu s tím, že předávání závislostí je zřejmé a
že se třída ke svým závislostem otevřeně hlásí.
Třída Authenticator
potřebuje databázi, ale hlásí se k velmi obecnému service locatoru,
což je v naprostém nepoměru vůči tomu, co skutečně potřebuje
že potřebuje zrovna databázi se nedá zjistit jinak, než
zkoumáním její implementace
Třída se tedy musí hlásit ke všem svým závislostem a právě jen
k nim. Jinak o svých závislostech lže.
(Může nastat situace, kdy požadovat service locator je korektní: pokud ho
třída skutečně jako takový potřebuje. Třeba kvůli jeho
vizualizaci apod.)
Co naopak service locator není
Občas někdo pojmenuje validní konstrukci termínem service locator a na
základě toho ji odsoudí. Podobný styl uvažování je zavádějící.
Znamená, že něco používáme či zavrhujeme a už nevím proč vlastně.
Důležité je si uvědomit, co je tím špatným na service locatoru, tj.
proč jej řadíme mezi antipatterny, a ověřit, jestli naše konstrukce
netrpí stejnými vadami. Tedy diskuse o tom, zda jde o service locator nebo
ne, je pak podružná.
Jako příklad si ukažme refaktoringu třídy Foo se třemi
závislostmi:
class Foo
{
function __construct(A $a, B $b, C $c)
}
$foo = new Foo($a, $b, $c);
Závislosti vytkneme do jedné (immutable) třídy:
class FooDependencies
{
function __construct(A $a, B $b, C $c)
}
class Foo
{
function __construct(FooDependencies $d)
}
$foo = new Foo(new FooDependencies($a, $b, $c));
Z hlediska DI jsou obě alternativy korektní, třídy se hlásí ke všem
svým závislostem a právě jen k nim. Neplatí tu námitky proti service
locatoru. Samozřejmě je otázka, zda uvedený refactoring byl opodstatněný a
správný, ale to už je jiný příběh.
Obdobně v úvodním článku Co je
Dependency Injection, kde jsem skupinu závislostí třídy
Article v presenteru zredukoval na továrničkou
ArticleFactory, nejsou na místě obavy, že továrnička je
service locator. Nevykazuje totiž jeho negativní rysy. Stejně tak
i příklad v článku Dependency
Injection versus Lazy loading, kde Authenticator byl nahrazen
za AuthenticatorAccessor, neboť presenter chtěl službu získat
skrze lazy loading.
Jak vidno, v DI nejde jen o jakékoliv předávání závislostí. Musí
být zřejmé. A pokud si nejste jisti, zda používáte korektní objektový
návrh v souladu s DI, udělejte si test „slepým API“. Skryjte si těla
metod nebo vygenerujte API dokumentaci a
závislosti jednotlivých tříd musí být stále jednoznačně patrné.
Lazy loading je návrhový vzor, který odkládá vytváření
objektů až do okamžiku, kdy je aplikace skutečně potřebuje. Jak to
skloubit s Dependency Injection, které naopak rádo objekty získává už
v konstruktorech?
Jak jsme si řekli, Dependency
Injection je zřejmé předávání závislostí, tedy že se každá
třída ke svým závislostem otevřeně hlásí v inicializačních metodách,
obvykle přímo v konstruktoru.
Mějme SignPresenter, který zobrazuje a obhospodařuje formulář pro
přihlašování se do aplikace. Po odeslání formuláře se zavolá metoda
formSubmitted(), která ověří přihlašovací údaje pomocí
autentikátoru. Zjednodušený kód by vypadal takto:
class SignPresenter
{
function formSubmitted()
{
// $GLOBALS['authenticator']->authenticate(...)
Registry::getAuthenticator->authenticate(...)
}
}
V souladu s principem DI si nebudeme autentikátor čarovat z globálního
prostředí, ale přiznáme se k této závislosti v konstruktoru:
class SignPresenter
{
private $auth;
function __construct(Authenticator $auth)
{
$this->auth = $auth;
}
function formSubmitted()
{
$this->auth->authenticate(...);
}
}
V praxi však narazíme na zásadní problém: na jedno odeslání
formuláře bude připadat třeba 1000 jeho zobrazení. Nicméně autentikátor
se bude inicializovat vždy. A jelikož ověřuje vůči databázi, bude
v souladu s DI vyžadovat v konstruktoru objekt PDO, jehož vytvoření
způsobí připojení se k databázovému serveru.
Tedy každé zobrazení formuláře bude doprovázeno načtením tříd
v 99.9 % případů nepotřebných, vytvářením nepotřebných objektů a
zbytečným připojením k databázi.
To je závažný nedostatek, který nám vyřeší právě lazy loading.
Jednou z možností je vytvořit tzv. proxy, objekt implementující rozhraní
Authenticator a obalující původní autentikátor, který jej
ovšem instancuje až při zavolání metody authenticate(). Druhou
možností, která však vyžaduje změnu presenteru, je si místo
autentikátoru předat továrničku, která nám jej vyrobí později:
class SignPresenter
{
private $authFactory;
private $auth;
function __construct(AuthenticatorFactory $authFactory)
{
$this->authFactory = $authFactory;
}
function getAuthenticator()
{
if (!$this->auth) {
$this->auth = $this->authFactory->create();
}
return $this->auth;
}
function formSubmitted()
{
$this->getAuthenticator()->authenticate(...);
}
}
interface AuthenticatorFactory
{
/** @return Authenticator */
function create();
}
Metoda getAuthenticator() zajišťuje, že budeme ve třídě
SignPresenter pracovat s jedinou instancí autentikátoru.
A tady by mohl článek skončit. Jenže nekončí.
Továrničku nebrat!
Zdálo se vám použití továrničky jako dobrý nápad či jako úplná
samozřejmost? Pak na chvíli zbrzděte.
Zkuste se zamyslet nad rozdílem mezi:
budu potřebovat objekt získat
budu potřebovat objekt vyrobit
My objekt budeme potřebovat získat, což je obecnější formulace
než vyrobit.
Vraťme se k prvnímu DI příkladu, kde získáváme autentikátor přes
konstruktor. Co říká hlavička konstruktoru? Že se mu má předat
autentikátor. Nikoliv, že se má vyrobit a poté předat. Metoda,
která vytváří instanci SignPresenter, může autentikátor získat
jakýmkoliv způsobem (a třeba jej vyrobit), ale samotnému presenteru je po
tom kulový. Ten ho jen vyžaduje a neptá se po původu.
Jenže řešení s továrničkou kromě podpory lazy loadingu předjímá
i způsob získávání objektu: bude se vyrábět. Takže zatímco v prvním
případě dostává SignPresenter jeden autentikátor, ve druhém případě
dostává nástroj, kterým si může vyrobit autentikátorů více. Ale o to
nám nešlo. Nestojíme před potřebou vyrábět autentikátory, řešíme
potřebu lazy-loadingu jediného autentikátoru.
Zdánlivě korektní nasazení továrny je chybné, za chvíli se k tomu
ještě vrátím. Správné řešení je místo továrny předat něco,
co nám autentikázor později vrátí (ne nutně vyrobí). Říkejme tomu
třeba Getter nebo Accessor (v žádném případě nejde o Service locator):
class SignPresenter
{
private $authAccessor;
function __construct(AuthenticatorAccessor $authAccessor)
{
$this->authAccessor = $authAccessor;
}
function formSubmitted()
{
$this->authAccessor->get()->authenticate(...);
}
}
interface AuthenticatorAccessor
{
/** @return Authenticator */
function get();
}
Kód presenteru se nám navíc zjednoduší, protože nepotřebujeme metodu
getAuthenticator(). Samotný accessor totiž zajišťuje, že
pracujeme stále se stejnou instancí.
Jak AuthenticatorFactory tak AuthenticatorAccessor
uvádím jako rozhraní, neboť na implementaci vůbec nezáleží.
Zkusme se podívat, jak může v praxi vypadat kupříkladu testování
presenteru:
// vyrobíme si mockovaný autentikátor
$auth = ...;
// a potřebujeme ho dostat do presenteru
$presenter = new SignPresenter(new TrivialAuthenticatorAccessor($auth));
kde TrivialAuthenticatorAccessor je vskutku triviální:
class TrivialAuthenticatorAccessor implements AuthenticatorAccessor
{
private $instance;
function __construct(Authenticator $instance)
{
$this->instance = $instance;
}
function get()
{
return $this->instance;
}
}
Pokud bychom místo accessoru šli původně navrhovanou cestou továrničky,
měli bychom docela problém, jak $auth do presenteru propašovat.
(Mimochodem ukázkový příklad, jak testování vede k lepšímu
návrhu kódu).
Přičemž jakoukoliv továrničku lze do podoby accessoru snadno
přetransformovat, například pomocí obecného
CallbackAccessor:
abstract class CallbackAccessor
{
private $instance;
private $callback;
function __construct(/*callable*/ $callback)
{
$this->callback = $callback;
}
function get()
{
if (!$this->instance) {
$this->instance = call_user_func($this->callback);
}
return $this->instance;
}
}
Jakýkoliv callback pak můžeme přetavit do podoby
AuthenticatorAccessor:
class CallbackAuthenticatorAccessor extends CallbackAccessor implements AuthenticatorAccessor
{}
$presenter = new SignPresenter(new CallbackAuthenticatorAccessor(function(){
return ...;
}));
Dependency Injection je prostá a skvělá technika, která
vám pomůže psát mnohem srozumitelnější a předvídatelnější kód.
Kód téměř vždy píšeme pro jiné: spolupracovníky, uživatele našich
open source knihoven nebo o pár let starší sebe sama. Abychom předešli
nepříjemným WTF momentům při jeho používání, je dobré dbát na
srozumitelnost. Ať už v pojmenování identifikátorů, výřečnosti
chybových zpráv nebo návrhu rozhraní tříd. A ke srozumitelnosti bych
přidal ještě předvídatelnost. Schválně, očekávali byste, že volání
$b->hello() v této ukázce může nějak změnit stav zcela
nezávislého opodál stojícího objektu $a?
$a = new A;
$b = new B;
$b->hello();
To by bylo divné, že? Jo, kdyby oba objekty byly nějak explicitně
propojeny, třeba kdybychom volali $b->hello($a) (tj.
s argumentem $a) nebo předtím nastavili
$b->setA($a), tak by mezi oběma objekty existovala vazba a dalo
by se očekávat, že $b může něco provádět s $a.
Ale bez toho by to bylo nečekané, nesportovní a matoucí…
Říkáte si, že to je přece jasné? Že jen blázen by takový magický
kód psal? Tak se podívejte na následující příklad, který v různých
obměnách najdete v řadě příruček „blog in 15 minutes with our amazing
framework“:
$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();
Třída Article reprezentuje článek na blogu a metoda
save() nám jej uloží. Kam? Asi do databázové tabulky.
Skutečně? Co když ho uloží do souboru na disk? A pokud do databáze, tak
do jaké tabulky? K jaké databázi se vlastně připojí? Ostré nebo
testovací? K SQLite nebo k Mongu? Pod jakým účtem?
Jde o stejný případ, jako v předchozí ukázce, jen pod $a
si představte (neviditelný) objekt reprezentující databázové spojení a
$b->hello() nahraďte za $article->save(). Co se
však nezměnilo, je nepředvídatelnost a nesrozumitelnost kódu.
Museli bychom se podívat, jak je implementovaná metoda save(),
abychom zjistili, kam se data ukládají. Zjistili bychom, že si šahá do
nějaké globální proměnné udržující databázové spojení. Museli bychom
pátrat dál, kde se v kódu databázové spojení vytváří, a pak bychom
teprve měli obrázek o tom, jak vše funguje.
Nicméně, i kdybychom pochopili, jak je vše provázané, byl by oříšek
do toho zasáhnout. Jak třeba za účelem testování uložit článek jinam?
Asi by to vyžadovalo změnit nějakou statickou proměnnou. Ale nerozbili
bychom tím něco jiného?
Jaj, statické proměnné jsou zlo. Vytvářejí skryté závislosti,
kvůli kterým nemáme kód pod kontrolou. Kód má pod kontrolou nás ☹
Řešení je Dependency
Injection
Dependency Injection (dále jen DI) neboli zřejmé předávání
závislostí říká: odeberte třídám zodpovědnost za získávání
objektů, které potřebují ke své činnosti.
Budete-li psát třídu vyžadující ke své činnosti databázi,
nevymýšlejte uvnitř jejího těla, odkud ji získat (tj. ze žádné
globální proměnné, statické metody, singletonu, registru atd.), ale
požádejte o ni v konstruktoru nebo jiné metodě. Popište závislosti svým
API. Nebudete muset tolik přemýšlet a získáte srozumitelný a
předvídatelný kód.
A to je vše. To je celé slavné DI.
Pojďme si to ukázat v praxi. Začneme u nešťastné implementace třídy
Article:
class Article
{
public $id;
public $title;
public $content;
function save()
{
// uložíme do databáze
// …ale kde databázové spojení seženu?
// GlobalDb::getInstance()->query() ?
}
}
Autor metody save() musel řešit nelehkou otázku, kde vzít
připojení k databázi. Kdyby použil DI, nemusel by nad ničím uvažovat (a
to mají programátoři rádi), neboť DI dává jasnou odpověď: pokud
potřebuješ databázi, ať ti ji někdo dodá. Jinými slovy: nic nesháněj,
ať se postará někdo jiný.
class Article
{
public $id;
public $title;
public $content;
function save(Nette\Database\Connection $connection)
{
$connection->table('articles')->insert(array(
'title' => $this->title,
'content' => $this->content,
));
}
}
Takže aplikace principů DI znamená jen to, že jsme předali
$connection jako parametr metodě? Jako vážně? Ano.
Jako vážně.
Užití třídy Article se pochopitelně nepatrně změní:
Díky této změně je nyní z kódu naprosto zřejmé, že se článek
uloží do databáze, a taky do které databáze.
Řešení pomocí DI tak přestavuje win-win situaci: autor třídy Article
nemusel řešit, kde objekt-databázi sežene, její uživatel nemusel pátrat,
kde ho programátor sehnal. Z kódu je nyní zřejmé, že článek se uloží
do databáze a lze velmi snadno nechat jej uložit do databáze jiné.
Můžete ale přijít s celou řadou námitek. Kde se třeba vezme
v posledním příkladu proměnná $connection? DI opakuje: „ať
se postará někdo jiný“. Databázové spojení zkrátka dodá ten, kdo
zavolá uvedený kód.
Nojo, ale teď to vypadá, že používání DI značně zkomplikuje kód,
protože kvůli vytvoření instance Article musíte uchovávat a
předávat databázové spojení. Navíc časem může ve třídě
Article vzniknout potřeba nějaká data formátovat a v souladu
s DI bude potřeba předávat ještě další objekty. Komplikovalo by nám to
například kontrolery:
class ArticlePresenter
{
function __construct(Connection $connection, TextFormatter $formatter, ...)
{
$this->connection = $connection;
$this->formatter = $formatter;
...
}
function createArticle()
{
return new Article($this->connection, $this->formatter, ...);
}
}
Když bude mít presenter co do činění s dalšími podobnými třídami
jako je Article, bude mít haldu závislostí. Ba co víc, Article
by měla projít refaktoringem, kdy nahradíme databázi obecnějším
úložištěm IArticleStorage, nebo jí zodpovědnosti za
ukládání sebe sama úplně zbavíme a delegujeme to na novou třídu
ArticleRepository. To by znamenalo upravit aplikaci na mnoha
místech; přinejmenším všude, kde se vytváří instance
Article. Co s tím?
Elegantní řešení jsou továrničky. Místo ručního (tj. operátorem
new) vytváření objektů Article si je necháme
vyrábět továrničkou. A místo všech závislostí třídy
Article si budeme předávat jen jeho továrničku:
class ArticleFactory
{
function __construct(Connection $connection, TextFormatter $formatter, ...)
{
$this->connection = $connection;
$this->formatter = $formatter;
...
}
function create()
{
return new Article($this->connection, $this->formatter, ...);
}
}
Původní ArticlePresenter se nám nejen krásně zjednoduší a
zároveň bude jeho API lépe vystihovat podstatu, tedy že ArticlePresenter
nepotřebuje žádnou databázi, chce prostě jen pracovat s články.
Z takového refaktoringu má člověk vyloženě dobrý pocit:
class ArticlePresenter
{
function __construct(ArticleFactory $articleFactory)
{
$this->articleFactory = $articleFactory;
}
function createArticle()
{
return $this->articleFactory->create();
}
}
V praxi se ukazuje, že každá třída mívá jen několik málo
závislostí, které ji předáváme. Důsledné používání DI tak navzdory
obavám kód nijak nekomplikuje a výhody, jako je srozumitelnost, jednoznačně
převažují nad tou troškou psaní navíc v konstruktorech.
Lze také namítnout, že netransparentní chování původní třídy
Article, která článek uložila neznámo kam, vlastně vůbec
nevadí, pokud ho metoda load() zase bude umět načíst. Vtip je
v tom, že nad kódem navrženým podle DI principu vždycky můžeme takto
fungující obálku vytvořit. Ale naopak toho docílit nelze.
Dependency Injection je technika z rodiny Inversion of Control (IoC), do
které patří i Service locator. Ten bývá zmiňován jako jakési zlé
dvojče. Proč se mu vyhnout si řekneme v dalším článku DI versus Service Locator.
Článek volně vychází z dokumentace Dependency Injection na
stránkách Nette Framework a byl aktualizován 24. 9. 2012.
Řada lidí namítala, že zvolený příklad je nevhodný, že by
třída Article neměla být takto provázaná s databází. Zcela
s nimi souhlasím a o to víc mi zvolený příklad vyhovuje. Ukazuje totiž,
jak DI mimoděk upozorňuje na chybný návrh, protože zdůrazňuje vazby,
které byly dříve skryté.
Pod článkem Dependency Injection
versus Lazy loading se vyrojilo velké množství komentářů, dle mého
však nešlo o přínosnou diskusi, ale spíš o nepochopení a špičkování
se bez jakýchkoliv hmatatelných argumentů. Aby neubližovaly samotnému
článku, přenesl jsem je pod tento spot.
Lazy loading je technika, která musí být (stejně jako jakákoliv jiná
technika!) pochopitelně použita jen tam, kde je pro ni vhodné místo. Nebylo
předmětem článku tohle řešit (vydalo by to možná na celý seriál).
Reálným problémem, kterému programátor přecházející od singletonů a
Service Locatorů k Dependency Injection čelí, je, jak vůbec lazy loading
použít, pokud jej skutečně potřebuje. Service Locator ho totiž nabízí by
design. Na což právě zmíněný článek dává odpověď.
Pod článkem Dependency Injection
není žádná věda se vyrojilo několik komentářů, které neměly dle
mého přínos k diskusi, ale spíš byly zdrojem nepochopení a špičkování
se. Aby neubližovaly samotnému článku určenému pro začátečníky,
přenesl jsem je pod tento spot.
Jak vyexportovat události z Facebooku do Google Calendar, aby
vše fungovalo dobře?
Otevřete si Facebook a klikněte na „Události“. Adresa s exportem
událostí ve formátu iCalendar je trošku nečekaně ukrytá pod
ikonkou lupy:
Adresu si zkopírujte do schránky:
Odkaz bychom mohli hned dát to Google kalendáře, ale narazili bychom na
několik nedostatků. Google nedokáže korektně zobrazit neveřejnou událost,
místo ní uvidíte pouze nicneříkající slovo „nedostupný“:
Google nedokáže ani filtrovat události podle toho, zda jsme potvrdili
účast. Takže kalendář se nám plní nejen událostmi, na které jsme
účast potvrdili, ale i těmi, na které nás někdo zatím jen pozval. Navíc
u událostí v letním čase uvádí o hodinu více.
Vše lze vyřešit jednoduchým filtrem facebookfilter.php,
který si umístěte na server:
// load events from Facebook
$s = file_get_contents("http://www.facebook.com/ical/u.php?$_SERVER[QUERY_STRING]");
// PREG requires \n line breaks
$s = str_replace("\r\n", "\n", $s);
// fix private events bug in Google Calendar
$s = str_replace('CLASS:PRIVATE', 'CLASS:PUBLIC', $s);
// remove unconfirmed events
function filterEvents($m) {
return strpos($m[0], 'PARTSTAT:NEEDS-ACTION') ? '' : $m[0];
}
$s = preg_replace_callback('#^BEGIN:VEVENT\n(.*?)^END:VEVENT\n#sm', 'filterEvents', $s);
// fix daylight savings bug in Google Calendar
function fixTime($m) {
$zone = new DateTimeZone('Europe/Prague');
$time = new DateTime($m[2], new DateTimeZone('UTC'));
$fix = $zone->getOffset($time) - $zone->getOffset(new DateTime('20000101', $zone));
$time->modify("-$fix seconds");
return $m[1] . $time->format('Ymd\THis\Z');
}
$s = preg_replace_callback('#^(DTSTART:|DTEND:)(.+)#m', 'fixTime', $s);
// revert line breaks back
$s = str_replace("\n", "\r\n", $s);
header('Content-Type: text/calendar; charset=utf-8');
echo $s;
Do Google kalendáře přidáme adresu našeho filtru se stejnými parametry
v URL, jako měl původní skript od Facebooku: