V Nette jsme narazili na zajímavý problém, který úzce
souvisí s článkem Dependency Injection a
předávání závislostí a komplikacemi způsobenými dědičností.
Stěžejním bodem tvorby aplikací v Nette je vytváření vlastních
presenterů, tedy potomků třídy Nette\Application\UI\Presenter
(dále Presenter s velkým P). Ta má své závislosti. Byl bych rád, aby
uživatelé stavěli aplikace v souladu s DI a tedy se i jejich presentery
hlásily ke svým závislostem.
Jenže jak to realizovat? Možná řešení:
Presenter i potomek uvedou závislosti v konstruktoru – to
by bylo peklo
pro každou závislost se použije samostatný setter –
zbytečně moc režijního kódu
- použije se jedna metoda pro závislosti Presenteru a jedna pro potomka
- Presenter využije konstruktor, potomek metodu inject()
- potomek využije konstruktor, Presenter metodu inject()
- nebudeme do toho tahat konstruktor, použijí se jiné metody
- použije se třída PresenterDependencies sdružující závislosti do
jedné proměnné
- injektování přímo do proměnných – nevím, jak pojmout
ad 1) Jak už jsem psal v odkazovaném článku, pokud by Presenter i jeho
potomek pro předání závislostí použili konstruktor, musel by se ten
překrývající postarat o předání všech rodičovských závislostí. Ale
jakmile se rodičovské závislosti změní (refactoring Presenteru), všechny
třídy se rozbijí. Neakceptovatelné.
ad 2) Zda předávat všechny závislosti jednou metodou nebo zda vytvořit
metodu/injektor pro každou závislost zvlášť, je v principu jedno. Protože
nemám rád, když je moc režijního kódu a zbytečně bobtná API, volím
raději jednu metodu.
ad 3) Šikovně vypadají přístupy 3a a 3b, tedy použít konstruktor pro
závislosti jedné strany a metodu inject()
pro závislosti druhé.
O tom, který způsob použít, jsme se přeli s Honzou Tichým. On preferoval
způsob 3b s tím, že když už nemáme žádné ideální řešení, zvolme
z možných to, které bude funkční, pokud uživatel začne věc řešit
intuitivně, aniž by věděl o nějaké metodě inject(). A intuitivně
použije konstruktor.
Namítal jsem, že ho tak utvrdíme v mylném dojmu, že předek, tedy
třída Presenter, vlastně žádné své závislosti nemá, jelikož si
programátor v konstruktoru předává jen ty své. Ale jde víceméně
o banalitu.
Horší je, že konstruktor potomka je volán nad objektem, který ještě
není plně inicializován, tj. nejsou mu předány závislosti rodiče.
Zkrátka se volá ještě před voláním inject(). Pokud by uživatel
v konstruktoru použil funkci, která nějakou takovou závislost vyžaduje
(například getUser()), výsledkem bude zavádějící chybová hláška Fatal
Error: Call to a member function xyz on a non-object nebo nějaká podobná.
Což není dobře.
Kdyby se uživatel zeptal, jak má tedy situaci vyřešit a v konstruktoru
plné inicializace docílit, odpovědí by bylo: nejde to, přesuň kód do
metody startup(). (Pokud nejsem srozumitelný, podívejte se na příklad
níže.) Tímto problémem řešení 3a netrpí.
ad 5) injektování přímo do proměnných s anotací @inject
by bylo vůbec nejšikovnější. Naprosté minimum režijního kódu, konec
problémů s dědičností. Má to ale svá úskalí: obsah public proměnných
nelze kontrolovat, private proměnné nejsou součástí veřejného API (jak je
plnit?) nicméně k tomuto tématu se ještě v nějakém dalším článku vrátím.
Nakonec jsme se s Vaškem Purchartem domluvili na šalamounském řešení
4, kdy se všechny závislosti budou předávat konstruktorem s tím, že
závislosti třídy Presenter sbalíme do jediného objektu
PresenterDependencies, ten už se bude předávat snadno a při změně
závislostí Presenteru se nic nerozbije.
Pokud budu chtít do svého presenteru předat objekt ArticleFacade, bude to
vypadat takto:
class ArticlePresenter extends Presenter
{
function __construct(Nette\Application\UI\PresenterDepedencies $pd, ArticleFacade $af)
{
parent::__construct($pd);
$this->articleFacade = $af;
// nyní je objekt plně inicializován a lze volat třeba:
if (!$this->getUser()->isLoggedIn()) { ... }
}
}
Podstatné je, že všechny závislosti budou předány přímo
v konstruktoru, objekt bude ihned inicializován a nebudou se zavádět
žádné novoty v podobě metod inject. Jak Vašek psal, jde o ústupek
pohodlnosti v zájmu přehlednosti: „Většinou v Nette vyhrávala vždy
pohodlnost … mám za to, že v rámci plánovaných změn a refaktoringů by
Nette mělo tuhle houpačku mírně zhoupnout směrem k přehlednosti za cenu
absolutní pohodlnosti.“
Poznámka: aby bylo evidentní, že třída PresenterDependencies je jen
workaround nedostatku v návrhu jazyka a že vlastně nemá sémantický
význam v objektovém modelu, nevadilo by mi ji implementovat takto
triviálně:
class PresenterDependencies
{
private $services;
function __construct(Application\Application $application, ..., Security\User $user)
{
$this->services = func_get_args();
}
function get()
{
return $this->services;
}
}
abstract class Presenter
{
function __construct(PresenterDependencies $pd)
{
list($this->application, ..., $this->user) = $pd->get();
}
}
Konec dobrý, všechno dobré. Vašek připravil pull request a já si
uvědomil…
Slepá ulička
…že tohle řešení je vlastně nejhorší ze všech. Proč?
Udělal jsem si výčet faktorů, které chci u jednotlivých řešení
sledovat (pořadí nesouvisí s důležitostí):
- pohodlnost
- intuitivnost
- blbuvzdornost
- moment inicializace
- rozšiřitelnost (dědičnost)
ad pohodlnost: Povinné předání objektu PresenterDependencies je
zdánlivá drobnost, ale troufám si tvrdit, že tohle by byl důvod, proč
mnozí DI nepoužijí a raději si ArticleFacade vytáhnou ze service locatoru.
Uvedený type hint je nejdelší název třídy z celého frameworku a psát ho
je za trest.
Představil jsem si sám sebe, jak tohle ukazuju na školení a připadal
bych si jako kokot. Jako bych školil nějaký Zend. Musel bych se ptát sám
sebe, proč to neděláme nějak lépe.
ad intuitivnost: Honza položil otázkou, co se stane, pokud člověk začne
věc řešit intuitivně. Dopadne to špatně. Nenapadne ho, že existuje
nějaké PresenterDepedencies a že je ho potřeba předat. Nepředá ho,
Presenter nezíská závislosti a bude v poloinicializovaném stavu.
V nečekané chvíli vyskočí nějaký divoký nicneříkající
Fatal Error.
S tím velmi úzce souvisí otázka blbuvzdornosti. I když budu vědět,
že musím volat parent::__construct()
(vždycky volejte!), mohu na
to snadno zapomenout a bude problém. V případě presenteru by bylo možné
(a vhodné) implementovat kontrolu, zda byl volán, stejně jako je tomu
u metody startup(), ale problém je, že řešení 4 jde této chybě
naproti.
Programátor také může udělat chybu v type hintu (všimli jste si, že
jsem ji v příkladu udělal?) nebo jej neuvede vůbec. Sice jsem se snažil
poladit chybové hlášky DI kontejneru, ale ty ve své obecnosti nikdy nemohou
být dokonalé a programátor bude stejně zmaten.
Pod momentem inicializace myslím okamžik, kdy je objekt plně
inicializován. PresenterDependencies ho dokázal dostat do konstruktoru, jiné
řešení tohle neumí. Nicméně výhodu v případě presenterů relativizuje
fakt, že uživatelé jsou stejně zvyklí úvodní kód dávat do metody
startup(), kde už objekt plně inicializován je.
Pátý a dosud opomíjený faktor hodnotí, jak se řešení vypořádá
s existencí více úrovní dědičnosti. Což je v případě Nette obvyklé,
tj. vytvořit si nějaký abstraktní BasePresenter a teprve od něj dědit. Co
když bude mít BasePresenter také nějaké závislosti? Řešení
4 předpokládá, že bychom je také uváděli v konstruktoru třídy
BasePresenter a znovu opakovali v jeho potomcích. Otravnost by se tím
zvyšovala.
Ven ze slepé uličky
Udělal jsem si tabulku, která u každého řešení uvádí, jak vyhovuje
jednotlivým faktorům. Jasným vítězem se ukázalo být 3c. To konstruktor
vůbec nepoužívá a každá třída si své závislosti předá metodou
(nejlépe finální) s názvem začínajícím na inject
. (Viz implementace.)
PresenterFactory, která má za úkol vytvářet instance presenterů,
postupuje tak, že po vytvoření objektu zavolá všechny jeho metody inject
v pořadí od třídy Presenter k potomkům. Tedy programátor si v každé
třídě vytvoří metodu nazvanou inject()
nebo podobně, a v ní
uvede jednotlivé závislosti. Nemusí předávat nic navíc, minimalizuje se
možnost chyby. Z hlediska pohodlnosti, rozšiřitelnosti a blbuvzdornosti je
tak učiněno za dost.
Typický příklad předávání závislostí do presenterů bude v Nette
vypadat takto:
class BasePresenter extend Presenter
{
function injectBase(User $user, ...)
{
$this->user = $user;
...
}
}
class ArticlePresenter extends BasePresenter
{
function inject(ArticleFacade $af)
{
$this->articleFacade = $af;
}
}
A co z hlediska intuitivnosti? Co když uživatel použije pro předání
závislostí konstruktor, protože o žádných metodách inject() neví?
Nevadí, fungovat to bude. Pochopitelně v konstruktoru nebude stále možné
volat např. metodu getUser(), ale tuto daň rád zaplatím.
Celá věc by šla zjednodušit injektováním přímo do proměnných,
třeba časem k tomu najedeme klíč. Do té doby se mi 3c jeví
nejschůdnější.
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.
Vyplatí se používat CSS preprocesory? A pokud ano, který
zvolit? Pokračuji v rozboru tří nejznámějších konkurentů.
CSS preprocesor je nástroj, který vám ze zdrojového kódu zapsaného ve
vlastní syntaxi vygeneruje CSS pro prohlížeč. Mezi nejznámější patří
SASS, LESS
a Stylus. Mají řešit
praktické nedostatky samotného CSS.
V předchozí části
jsme si ukázali, jak preprocesory nainstalovat. Dnes se jim podívám na
zoubek.
Syntaxe
Ač jednotlivé nástroje používají odlišnou syntax, všechny rozumí
klasickému CSS. To je nesmírně důležité! Můžete kopírovat
existující CSS fragmenty a budou fungovat. Ačkoliv…
SASS používá dvě různé syntaxe, jedna se nazývá SASS (stejně jako
preprocesor), nepoužívá středníky ani složené závorky {}
a
místo toho odsazuje mezerami či tabulátory:
// SASS
#main
color: blue
font-size: 0.3em
Druhou syntaxí je SCSS, která vypadá jako klasické CSS a měla být
i plně kompatibilní (budu ji pro SASS používat v dalších příkladech).
Stejnou syntax má i LESS:
// SCSS, LESS a CSS
#main {
color: blue;
font-size: 0.3em;
}
Stylus také rozumí CSS syntaxi, nicméně upozorňuje, že nejde o 100%
kompatibilitu, především proto, že hodně bazíruje na odsazování. V jeho
syntaxi lze vynechat složené závorky, středníky a dokonce
i dvojtečky:
// Stylus
#main
color blue
font-size 0.3em
Ale pozor! Důležité je, že tyto znaky můžeme vynechat. Pokud
vám zápis připadá příliš hutný, klidně si dvojtečky doplňte. Nechcete
odsazovat? Vražte tam složené závorky. Velmi podobně funguje NEON. V tomto se zásadně liší od úsporné
SASS syntaxe, kde vynechání závorek a středníků je povinnost. Což
ji činí z praktického hlediska nepoužitelnou a chápu, proč tvůrci
o kompatibilitu s CSS usilovali, ale udělali to nešťastně zavedením
další, nekompatibilní, ukecané syntaxe SCSS.
Když už se totiž rozhodnu preprocesor používat, ocením, když mi
zjednoduší i syntaxi (alespoň nepovinné středníky), což bohužel umí
jen Stylus. Jeho syntax je nejpružnější a nejúspornější.
V kontrastu přísnosti LESS a SASS je zajímavé, že všechny tři
preprocesory podporují // řádkové komentáře
.
Když udělám chybu
Pokud uděláte v dokumentu chibu, preprocesory vás na ni upozorní
chybovou hláškou. A v tomto směru se jednotlivé nástroje zásadně
liší. Pokud třeba zapomenu ve výše uvedeném kódu uzavírací
}
, oznámí SASS:
Syntax error: Invalid CSS after "font-size: 0.3em;": expected "}", was ""
on line 4 of test.scss
Stylus:
if (err) throw err;
^
ParseError: stdin:4
1| #main {
2| color: blue;
3| font-size: 0.3em;
> 4|
unexpected "eos"
a LESS:
ParseError: missing closing `}` in test.less:9:17
8
9 unexpected "eos"undefined
Nutno dodat, že LESS měl šťastnější chvilku. Pokud bych zapomněl
středník, skončí to u obvyklejšího
ParseError: Syntax Error on line 2 in test.less:2:1
1 #main {
2 color: blue
3 font-size: 0.3em
Výmluvné chybové hlášky jsou pro mě důležité a v tomto směru
s přehledem vede SASS. Ačkoliv se to z ukázky tak nejeví, hodně se
snaží i Stylus. Běžně však špatně spočítá číslo řádku a hlásí
chybu o pár řádků výš, než skutečně je. LESS se nejčastěji zmůže
na nicneříkající Syntax Error on line XY
.
Občas jsem narážel na chyby v samotném preprocesoru. Pokud se mi je
podařilo izolovat, snažil jsem se je nahlásit (LESS, Stylus), ale ne vždy
to šlo. Zde se nejvíc projevila vyspělost jednotlivých nástrojů. Zatímco
nejstarší SASS se mi z rovnováhy nepodařilo vyvést nikdy, druhé dva
nástroje obsahují chyb bohužel požehnaně.
Co všechno umí?
Drtivou většinou užitečných vychytávek najdeme u všech tří
nástrojů, byť třeba nemusí být uvedeny v dokumentaci. V množství asi
vede SASS, ale takto se to hodnotit nedá. Třeba nested
properties nepotřebuji, naopak property
lookup a generátor data URI
ve Stylusu vypadá užitečně. Tedy do chvíle, než zjistíte, že převádí
buď všechny obrázky, nebo žádný.
Nechci sepisovat výčet jednotlivých vlastností ani srovnávat jejich
zápis, to najdete v dokumentaci nebo ve srovnávacím
článku na NetTuts. Zaměřím se místo toho na fundamentální rozdíly a
největší z nich se týkají tzv. mixinů.
Mixiny
Mixiny jsou z mého pohledu nejdůležitější vychytávkou preprocesorů,
protože mění způsob, jak píšeme HTML kód. Vysvětlím. Asi
nejznámější „mixin“ je tzv. clearfix, která se stará o to, aby
z bloku nevytékaly plovoucí prvky. Obvykle vypadá nějak takto:
.clearfix:after {
clear: both;
content: ".";
display: block;
height: 0;
visibility: hidden;
font-size: 0;
}
(Poznámka: existují jednodušší i složitější varianty clearfixu.
Kupodivu mně vždycky fungovalo nastavit bloku overflow: auto
a
clearfix jsem nepotřeboval. Ale v tuto chvíli jde jen
o příklad.).
Clearfix pak aktivujeme v HTML:
<div id="content" class="clearfix">
<div style="float: left;">....</div>
...
</div>
Ale to neděláš dobře, Jaromíre. Clearfix by se měl aplikovat na straně
stylů, přímo pro #content
, nikoliv v HTML. Jenže komu by se
chtělo kazit si krásný stylopis kopírováním ošklivého hacku? Mnoho
kodérů raději sáhne do HTML.
S preprocesory však netřeba nic kopírovat a vystačíme si s jedinou
instrukcí. Nejprve si clearfix uložíme jako tzv. mixin a poté ho v definici
#content
zavoláme:
SASS:
// definice mixinu (zkráceno)
@mixin clearfix {
&:after { clear: both; ... }
}
#content {
@include clearfix; // vložení mixinu
}
LESS:
.clearfix() {
&:after { clear: both; ... }
}
#content {
.clearfix;
}
Stylus:
clearfix()
&:after { clear: both; ... }
#content
clearfix()
Zmizí tak nutkání zasahovat do zdrojového kódu. A to je velmi
důležité.
Jak vidno, syntaxe SASS je opět nejukecanější. Za zmínku stojí, že
LESS umožňuje volat jako mixin i jakoukoliv třídu nebo ID.
Parametrické mixiny
Ještě zajímavější je to ve chvíli, kdy mixinům předáme
parametry:
SASS:
@mixin border-radius($arg) {
-webkit-border-radius: $arg;
-moz-border-radius: $arg;
border-radius: $arg;
}
#content {
@include border-radius(2px);
}
LESS:
.border-radius(@arg) {
-webkit-border-radius: @arg;
-moz-border-radius: @arg;
border-radius: @arg;
}
#content {
.border-radius(2px);
}
Stylus:
border-radius(arg)
-webkit-border-radius: arg
-moz-border-radius: arg
border-radius: arg
#content
border-radius(2px)
Ve Stylusu můžeme použít i zapis s dvojtečkou:
#content
border-radius: 2px
Což je velmi zajímavé! Když totiž později vendor prefix odstraníme,
nebo když naopak zjistíme, že určité vlastnosti potřebujeme vendorovou
variantu dodat, nemusíme změnit ani čárku v samotném stylu. Wow.
Jenže… co když mixin zavoláme s více parametry? Například uvedeme
border-radius: 2px 3px
. Překvapivě Stylus bude 3px
v tichosti ignorovat. Proč tomu tak je?
V CSS existují dva způsoby, jak uvést více argumentů, lišící se
v oddělovači:
- oddělené mezerama:
border: 1px solid black
- oddělené čárkama:
rgb(10, 20, 30)
Totéž se rozlišuje i v preprocesorech. Ve Stylusu platí, že volání
mixinu border-radius: 2px 3px
je ekvivalentní k
border-radius(2px, 3px)
(a dává mi to smysl), obojí představuje
předávní dvou argumentů. Naopak border-radius(2px 3px)
bez
čárky předává argument jeden. Náš mixin očekává pouze jeden parametr a
druhý tiše bez jakéhokoliv varování zahodil. Oprava spočívá v použití
užitečného klíčového slova arguments
namísto předaného
argumentu arg
.
SASS a LESS naopak předání více argumentů, než je mixinem očekáváno,
vnímají jako fatální chybu. V případě SASS to v důsledku vede
k podivným blokům $shadow-1, …, $shadow-10 patrným
v galerii Compass, zatímco LESS se s problémem snaží vypořádat
konstrukcí when()
. Nic z toho mi nepřipadá ideální.
Pokračování zase
příště.
Už pár let jsem si pořádně nezakódoval a začalo mi to
chybět. Zachtělo se mi udělat stránky podle nejnovějších trendů.
Responsivní design okořeněný CSS preprocesory. Ale váhal jsem: jsou
preprocesory víc, než jen chvilková móda?
CSS preprocesor je nástroj, který vám ze zdrojového kódu zapsaného ve
vlastní syntaxi vygeneruje CSS pro prohlížeč. Mezi nejznámější patří
SASS, LESS
a Stylus.
Faktem je, že jakmile začne stylopis kynout do větších rozměrů, řada
věcí se řeší dosti nepohodlně. Je třeba vynaložit úsilí, aby zůstal
čitelný a srozumitelný. Aby se z něj nestal write-only soubor plný
magický konstant a hacků. Spoustu těchto nešvarů preprocesory řeší,
nejvíc se těším na vnořené
definice, matematické
výrazy, mixiny
a proměnné.
Vlastně je smutné, že preprocesory musely vzniknout. Ukazuje to na
zanedbanost vývoje CSS. Na druhou stranu, může z nich i těžit.
Preprocesory jsou mladé projekty procházející bouřlivým vývojem, reagují
na potřeby uživatelů a lze u nich, na rozdíl od standardu, tolerovat
i případné nekompatibilní změny. Ve finále tak mohou ukázat směr,
kterým se má vydat příští CSS.
Pokud se kódováním webů bavíte řadu let, máte vybudované
konvence
pomáhající nedostatky obcházet. Preprocesory pak nemusí být úplně
samozřejmou volbou. Nicméně dnes je běžné používat různé CSS generátory a preprocesor nabízí
čistější cestu, než copy&pastovat vygenerovaný kód.
Rád zkouším nové věci, proto jsem dal preprocesorům šanci. Který ale
zvolit? Nejlepší je si všechny osahat.
Instalace
Začneme tedy rovnou instalací. Na webu SASS i LESS rychle najdete
vyčerpávající postup, jak knihovny získat. SASS je napsaný v Ruby, LESS v Node.js, takže prvním krokem bude instalace
překladače, což by neměl být v žádném operačním systému problém.
Preprocesor pak nainstalujete příkazem:
gem install sass
resp.
npm install less
Velmi snadné, PHP může závidět.
Naopak web Stylusu selhává, snaží se mást odkazy na Github, zatímco
informace, jak ho instalovat, je důmyslně skrytá kdesi vespod úvodní
stránky. Vězte tedy, že Stylus je také napsán v Node.js a nainstaluje se
obdobně příkazem npm install stylus
.
Pozor na jednu věc, npm instaluje
balíček do aktuálního adresáře, takže abyste mohli preprocesory pohodlně
spouštět z příkazové řádky, musíte si cestu k lessc.cmd
a
stylus.cmd
přidat do proměnné PATH. Ve Windows se to dělá sparťansky, takže
spíš oceníte možnost nainstalovat balíčky do globálního adresáře
(pomocí přepínače -g
, tj. npm install -g stylus
),
ke kterému už cestu zaregistroval při instalaci překladač.
Příkazová řádka pro SASS a Stylus nabízí spoustu voleb, LESS umí jen
konvertovat vstupní soubor do výstupního CSS. Zmátlo mě, že SASS i Stylus
zavolané bez parametrů se nijak neohlásí a očekávají vstup
z klávesnice. Zavolejte je tedy s parametrem -h
a vypíše se
nápověda všech voleb.
Vývoj a deployment
Preprocesory vyžadují, aby se mezi úpravou stylopisu a zobrazením
v prohlížeči udělat jeden krok navíc: kompilace do CSS.
Tento krůček může mnohé odradit. Pokud jste zvyklí rovnou editovat CSS
soubory na FTP serveru, nad preprocesorem vůbec neuvažujte. Existují sice
možnosti, jak třeba z LESS generovat CSS za běhu přímo v prohlížeči,
ale rozhodně nejsou určeny pro produkční nasazení.
Pokud máte deployment automatizovaný, stačí do procesu zařadit kompilaci
voláním příkazové řádky a je vystaráno.
Jak řešit onen krok navíc během vývoje? Kodéra rozhodně blbnutí
s příkazovou řádkou nezajímá a chce rovnou psát stylopis.
Jak jsem zmínil, LESS umí překládat stylopisy v prohlížeči, stačí
tedy zalinkovat soubor less.js a můžete rovnou připojovat soubory ve formátu
LESS (povšimněte hodnoty atributu rel
):
<link rel="stylesheet/less" type="text/css" href="styles.less">
<script src="less.js" type="text/javascript"></script>
SASS a Stylus zase nabízejí sledovací režim, ve kterém monitorují
adresář se styly ve svém formátu a při každé změně souboru je ihned
překládají do CSS.
// překlad souborů z adresáře /css.sass do /css (včetně podadresářů)
sass --watch css.sass:css
// překlad souborů z adresáře /css.stylus do /css
stylus --watch css.stylus --out css
Do vygenerovaného CSS lze pomocí přepínače --line-numbers
přidat pro lepší orientaci komentáře s číslem řádku zdrojového
souboru. Pokud vyvíjíte ve Firefoxu, ještě užitečnější je nainstalovat
FireStylus
a kompilovat s přepínačem --firebug
. V záložce HTML by se
pak měly objevovat odkazy přímo na zdrojový soubor. Píšu měly, protože
mi to nefunguje.
Všechny tři preprocesory jsou seřazeny na startovní čáře. Který
z nich běží nejlépe? Pokračování příště.
(Doporučuji aktuálnější průvodce CSS preprocesory od Martina
Michálka, první a druhý
díl.)
I found a strange bug in Internet Explorer 9, which appears in the last
version 9.0.8112.16421. If the same cookie is sent twice, once with the
expiration at the end of the session and once with any other expiration, IE9
will remove cookie. Webpage must have a strict doctype.
Example:
<?php
// expire at the end of the session
setcookie('test', 'value', 0);
// expire in 1 hour
setcookie('test', 'value', 3600 + time());
// switch browser and document mode to IE9
?>
<!doctype html>
Is cookie set? <?php echo isset($_COOKIE['test']) ? 'yes' : 'no' ?>
Try to open this example in IE9 and
refresh: cookie will not be set.
Solution is to remove older cookies after sending new one:
$headers = array();
foreach (headers_list() as $header) {
if (preg_match('#^Set-Cookie: .+?=#', $header, $m)) {
$headers[$m[0]] = $header;
header_remove('Set-Cookie'); // requires PHP 5.3 or newer
}
}
foreach ($headers as $header) {
header($header, false);
}
Bug is fixed in Nette Framework.
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í:
$article = new Article;
$article->title = ...
$article->content = ...
$article->save($connection);
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.