Dependency Injection a property injection
Dependency Injection je princip, kdy 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 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 */
private $httpRequest;
/** @var Router */
private $router;
function __construct(HttpRequest $httpRequest, Router $router)
{
$this->httpRequest = $httpRequest;
$this->router = $router;
}
}
versus
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šechny části:
- Dependency Injection není žádná věda
- Dependency Injection versus Lazy loading
- Dependency Injection versus Service Locator
- Dependency Injection a předávání závislostí
- Dependency Injection a property injection (právě čtete)
DI a předávání závislostí presenterům
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 peklopro 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ší.
Dependency Injection a předávání závislostí
Víte, že Dependency Injection je princip, kdy 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. Kupodivu to není tak snadné.
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.)
Konstruktor 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.
Všechny části:
- Dependency Injection není žádná věda
- Dependency Injection versus Lazy loading
- Dependency Injection versus Service Locator
- Dependency Injection a předávání závislostí (právě čtete)
- Dependency Injection a property injection
SASS, LESS, Stylus nebo čisté CSS? (2)
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ě.
SASS, LESS, Stylus nebo čisté CSS? (1)
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ě.

