DI versus Lazy loading
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 ...;
}));
Všechny části:
- Co je Dependency Injection?
- Dependency Injection versus Service Locator
- Dependency Injection a předávání závislostí
- Dependency Injection a property injection
- Dependency Injection versus Lazy loading (právě čtete)