Dependency Injection a Lazy Loading

Pokud vám název článku něco připomíná, nemýlíte se – nechal jsem se inspirovat článkem Davida Grudla, na nějž bylo mnoho zajímavých reakcí. Ano, i v PHP komunitě se řeší věci jako dependency injection, lazy loading, SOLID nebo čistý kód 🙂
V tomto článku přináším můj pohled na daný problém a popíšu elegantní způsob řešení v .NETu. Nechci polemizovat s Davidem ani porovnávat jeho řešení s mým – jeho článek berte prosím jen jako inspiraci, které mě vedla k sepsání tohoto článku.

David ve svém článku řešil situaci (přeloženo do C# a ASP.NET MVC), kdy máme Controller, který zajišťuje zobrazení formuláře a zároveň jeho zpracování:

public class HomeController : Controller
{
  public HomeController(ILoginService loginService)
  {
    LoginService = loginService;
  }

  public ILoginService LoginService { get; private set; }

  public ActionResult ShowLogin()
  {
     return PartialView();
  }
  
  public ActionResult ProcessLogin(Credentials credentials)
  {
     LoginService.Login(credentials);
     // ...not important
  }
}

Pokud se bude přihlašovací dialog zobrazovat na každé stránce (jak bývá zvykem), bude se vždy do tohoto HomeControlleru injectovat nějaká ILoginService – a pokud má konkrétní použitá implementace závislost třeba na IDbConnection (připojení do databáze), tak to už může být problém, protože si třeba zbytečně vytáhneme z connection poolu připojení do databáze, které nám může na zatíženém webu chybět.

Jak z toho ven? V .NETu máme k dispozici třídu Lazy<>, která nám umožní to samé, co popsal David ve svém článku – ke zpřístupnění/vytvoření živé instance ILoginService dojde až tehdy, když si sáhneme na property Value. Implementace bude pak vypadat takto:

public class HomeController : Controller
{
  public HomeController(Lazy<ILoginService> loginService)
  {
    LoginService = loginService;
  }

  public Lazy<ILoginService> LoginService { get; private set; }

  public ActionResult ShowLogin()
  {
     return PartialView();
  }
  
  public ActionResult ProcessLogin(Credentials credentials)
  {
     LoginService.Value.Login(credentials);
     // ...not important
  }
}

Problem solved, fuck yeah! Nebo ne? Pozorný čtenář začne větřit problém smradlavý kód. V jednom příkazu (první řádek metody ProcessLogin) se nám totiž objevily dvě tečky, což je dobrá indikace toho, že porušujeme Law Of Demeter. Protože je ale Value property BCL objektu, tak bych se z toho hned dogmaticky nehroutil. Ale za bližší prozkoumání tato situace určitě stojí.

Elegantní řešení, kterým se vyhneme použití Lazy a zároveň se zbavíme zbytečného injectování ILoginService, je rozdělit HomeController na dva menší Controllery – to je nakonec správně i podle SRP. Problem solved. Nebo ne? Nevím jak vy, ale já nechci mít v projektu trilión tříd s jednou metodou, takže přemýšlejme dál. A nebo ne – začněme úplně od začátku!

Znovu a lépe

Proč jsme začali vůbec komplikovat ten původní jednoduchý a přímočarý kód? Protože jedna z akčních metod se volá méně častěji a protože vytvoření ILoginService může být náročné. Proč ale do implementace tahat takovéhle předpoklady? Co je HomeControlleru do toho? Tomu musí být jedno, jak je ILoginService implementovaná a jestli je její vytvoření náročné nebo ne – programujeme proti rozhraním!

Jediný, kdo může/musí vědět o konkrétních použitých třídách, jejich vztazích a způsobech použití, je kompoziční root, neboli kontejner. To je místo, kde skládáme z malých kousků výslednou aplikaci.

Takže implementaci HomeControlleru necháme pěknou a čistou, tak jak je v první ukázce kódu. Problém se zbytečným vytvářením těžké implementace ILoginService (nazvěme ji HeavyLoginService) vyřešíme chytrou konfigurací kontejneru:

Vytvoření HomeControlleru nakonfigurujeme tak, že se jako parametr konstruktoru předá jen proxy, která implementuje ILoginService. Při prvním přístupu k této proxy dojde k vytvoření HeavyLoginService a veškerá volání bude proxy delegovat na tuto živou instanci. V kompozičním rootu bychom tedy mohli mít toto:

public class LoginServiceProxy : ILoginService
{
  public LoginServiceProxy(Func<ILoginService> accessor)
  {
     Accessor = accessor;
  }

  private Func<ILoginService> Accessor { get; set; }
  private ILoginService Live { get; set; }

  public LoginResult Login(Creadentials credentials)
  {
    if (Live == null)
    {
       Live = Accessor(); // thread safety?
    }
    return Live.Login(credentials);
  }
}

Předávaná lambda accessor se postará o dodání nějaké živé implementace ILoginService, v našem případě tedy HeavyLoginService.
Jistě uznáte, že psát takovýto kód stále dokolečka dokola pro každý interface (který může mít samozřejmě více metod) je nuda a docela bychom se opakovali. Takže proxy nebudeme psát ručně, ale nějak si je nagenerujeme.

Mohli bychom to dělat třeba pomocí T4 ještě před kompilací, ale já stejně jako můj velký vzor použiji knihovnu Castle DynamicProxy 🙂

Nejprve si vytvoříme interceptor, což je de facto implementace všech metod naší proxy, tedy všech metod daného interface:

public class LazyLoadInterceptor<T> : IInterceptor
{
  public LazyLoadInterceptor(Func<T> accessor)
  {
    Accessor = accessor;
  }

  private Func<T> Accessor { get; set; }
  private T Live { get; set; }

  public void Intercept(IInvocation invocation)
  {
    // will be called for all methods
    if (Live == null)
    {
        Live = Accessor();
    }
    invocation.ReturnValue = invocation.Method.Invoke(Live, invocation.Arguments);
  }
}

Je to tedy prakticky to samé, co jsme viděli u ručně psané proxy. Jediný rozdíl je ve způsobu volání živé metody. Toto funguje bezvadně, jen metoda Invoke nepatří k nejrychlejším, tudíž se vyplatí zde použít třeba nacachované generování IL kódu.

Proxy objekt pak vytvoříme takto (v praxi bychom si to asi zabalili to nějaké pěkné Utils metody):

var pg = new ProxyGenerator(); // cache them!
ILoginService proxy = pg.CreateInterfaceProxyWithoutTarget<ILoginService>(
  new LazyLoadInterceptor<ILoginService>(() => new HeavyLoginService()));

Takto získanou proxy uspokojíme závislost HomeControlleru.

Závěr

HomeController zůstal krásně čistý a lazy loading těžké implementace závislosti je krásně skryt za proxy. U mě dobrý.

http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js
http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCSharp.js

if (SyntaxHighlighter) {
SyntaxHighlighter.all();
}

24 thoughts on “Dependency Injection a Lazy Loading

  1. Věta: „Ano, i v PHP komunitě se řeší věci jako dependency injection, lazy loading, SOLID nebo čistý kód“ mě totálně rozsekala 😀 Třeba nás po tomhle tvrzení začnou považovat za programátory i .NET a JAVA komunity 😉

    To se mi líbí

  2. Augi, díky za inspirující článek.
    Mohu se jen zeptat? Tipuji, že hlavní problém je s (nevyslovenými) předpoklady, jak se má chovat třída LoginService.

    Pokud by šlo o bezstavovou službu, a tak mi podle popisu v článku přijde, která má nastaven životní cyklus „lifestyle“ Singleton, tak nemusíme řešit vytváření jejích per request instancí a k vyzvednutí objektu Connection (resp. nějakého db objektu pro přístup k databázi) může dojít až po za volání každé metody lLogin).
    Jinými slovy:
    Třída LoginService – životní cyklus Singleton – do konstruktoru dostává v kompozičním rootu odkaz na DbComponent factory (také LifeStyle singleton).
    Při volání metody login zavoláme dbComoponent.GetComponent (aka GetDbConnection) a tato metoda nám vrátí per request objekt, který použijeme k vyřízení požadavku.

    To se mi líbí

  3. dkl: Ani já nechápu, proč by měla ILoginService při svém vytvoření vytahovat DbConnection z poolu. V článku jsem psal, že konkrétní implementace ILoginService může mít závislost na IDbConnection. To samotné vytažení z connection poolu je už jen jeden z možných scénářů, jak kontejner tuhle závislost uspokojí.

    arron: Já jsem změnil názor už dávno 🙂 Především díky Davidovi a klukům z Medio.cz

    René: Nejsem si jist, jestli jsem tě teď úplně pochopil. Předpoklad, jak se má chovat LoginService, byl jen ten, který je daný jejím rozhraním, tj. že metoda ProcessLogin má pro credentials vrátit nějaký výsledek (úspěšnost přihlášení, nějaké info o uživateli apod.).

    Článkem jsem se snažil říct, že dle mého skromného názoru by jakékoliv další vykonstruované předpoklady neměly ovlivnit to, jak se HomeController k nainjectované ILoginService chová. Tzn. přesunout aplikačně-specifické věci do kódu konkrétní aplikace, tj. do kompozičního rootu.

    To se mi líbí

  4. @Augi Pokusím se to vysvětlit.

    „Článkem jsem se snažil říct, že dle mého skromného názoru by jakékoliv další vykonstruované předpoklady neměly ovlivnit to, jak se HomeController k nainjectované ILoginService chová. Tzn. přesunout aplikačně-specifické věci do kódu konkrétní aplikace, tj. do kompozičního rootu.“

    Zcela tebou souhlasím – jen dodávám. Když nemá být HomeController zatěžován speciálním zacházením s ILoginService, je opravdu vhodné zatěžovat (proxy) ILoginService „lazy“ inicializací, když na začátku bylo řečeno, že motivací pro zavedení lazy inicializace je to, že nepoužívá některé cenné zdroje (např DbConnection)? Není to tak, že ILoginService by měla dostat nástroj, který jí při volání metody Login dovolí získat DbConnection – tedy odkaz na továrnu, která vyrábí objekty s životním cyklem per request?
    Ty totiž na konci článku už HeavyLoginService vytváříš bez jakékoli závisloti (new LazyLoadInterceptor(() => new HeavyLoginService()));) a na začátku píšeš „bude se vždy do tohoto HomeControlleru injectovat nějaká ILoginService – a pokud má konkrétní použitá implementace závislost třeba na IDbConnection (připojení do databáze), tak to už může být problém, protože si třeba zbytečně vytáhneme z connection poolu připojení do databáze, které nám může na zatíženém webu chybět.“

    Takže u mě:
    Kompoziční root:
    ========================
    ILoginService – životní cyklus Singleton (nebo klidně jiný)
    IDBComponentFactory – životní cyklus Singleton
    HomeController – – životní cyklus Transient (pokaždé nová instance)

    V kódu metoda IDBComponentFactory.GetDbObject vždy vrátí objekt pro přístup do db s životním cyklem per request

    Konstruktory
    ================
    HomeController(ILoginService service)

    LoginService(IDBComponentFactory dbComponentFactory)

    metoda LoginService.Login
    ========================
    DbComponent component = dbComponentFactory.GetDbObject();

    Takžě se ptám – je tady proxy nutná? Jinými slovy – nepřenáší se na proxy ILoginService zprostředkovaně znalost, jak se zachází s nějakým cenným zdrojem (DbConnection), když my můžeme toto šetrné zacházení zapouzdřit do dbComponentFactory? Nebo je jiný důvod, proč existuje proxy?

    Snad je to teď srozumitelnější – mně se použití proxy líbí, jen mi přijde, že proxy pro ILoginService je použita proto, že se předpokládá, že konkrétní LoginService používá nějakpou závislost, jejíž životní cyklus je jiný než životní cyklus ILoginService.

    Pokud tě špatně interpretuju, tak se omlouvám.

    To se mi líbí

  5. René: není tohle tedy stejné nebo hodně podobné řešení jako to co navrhoval David? Akorát, že on nazýval továrnu accessorem a predaval si ji rovnou do presenteru misto do servisy? A nedopustím se tak potom zase porušení Demetera?

    To se mi líbí

  6. @Jiří Landsmam: Davidovo řešení jsem moc nezkoumal, teď jsem prolétl jen diskuzi.

    Pro bych porušoval zákon bohyně Demeter? A opravdu se budeme
    mlátit tímto zákonem v kontextu DI, když tento „zákon“ klidně říká, že je povoleno používat:

    „a global variable, accessible by O, in the scope of M“

    Tento zákon jen sděluje, že by neměly v aplikaci vznikat závislosti na třídách/komponentách/modulech, které pro vás nemají význam a jejichž refaktorizace by se vás tedy neměla dotknout.

    „Nebudete chtít záviset na modulech, jejichž interní implementace ani externí reprezentace (rozhraní) vás nezajímá a jejichž změna se vás nesmí dotknout. “ Tak zní mé doporučení/přikázání. 🙂

    Redukovat zákon božské Demetr:) na „počet teček“ ve výrazu svědčí spíš o povrchnosti kritiků Davidova řešení, než o problematičnosti řešení samotného.

    Závislosti typu.
    m_customer.Type.DefaultCategory.DefaultKoeficient;

    Proč byste měl v objednávce vědět, jak vypadá modul zákazníků a jak jsou objekty v tomto seskládány do objektového grafu? Vás zajímá jen výchozí koeficient, ke kterému se lze dostat přes delegaci. Opravdu je tento problém shodný s tím, co popisuju výše a co pravděpodobně popisuje David?

    Dám Vám analogický příklad k tomu, co jsem psal:
    V aplikaci potřebuju v určitém okamžiku vyrobit view. MainViewModel bude potřevovat tři různá view v závislosti na tom, co si uživatel vybral v menu.

    1) Mám si „pro jistotou“ nechat vyrobit view rovnou v konstruktoru MainViewModelu, abych je mohl použít? Proč? To je přece nesmysl.
    2) nebo je lepší nechat si injektovat viewFactory, na které tepprve., když view potřebuju, zavolám:
    var view = viewFactory.CreateView();
    view.Show();

    Továrna pro výrobu view je nutná závislost, protože bez ní nejsem schopen dodávat view- změna rozhraní se mě určitě dotkne, změna implementace (někdy) také.

    Na view závisím také – nemusím záviset na celém rozhraní IChildView, ale minimálně na nějakém společném jmenovateli IView s metodami Show, Close.

    Přesto stále platí, že:
    1) Nepoužívám DI kontajner jako univerzální Service locator, který bych předával do konstruktoru objektu. DI kontajner je použit jen v kompozičním rootu a v infrastrukturních objektech – IViewFactory třeba Castle dokáže automaticky naimplementovat tak, že prohledává DI kontajner

    2) Konstruktor MainViewModelu jednoznačně signalizuje, že má závislost na IViewFactory, což je výmluvnější než předání všech možných view a lépe odpovídá způsobu práce s view uvnitř třídy.

    3) Třída MainViewModelu nemá povědomí o tom, jak view vznikají, jaký je jejich životní cyklus, ale spoléhá jen na IViewFactory.

    4) Při přidání dalších view do systému:

    a) Nemusíme měnit konstruktor třídy MainViewModel, aby dokázala s těmito view pracovat.

    b) Neměníme interní implementaci vytváření view ve viewFactory ani v MainViewModel.

    Jsme schopni změnit i DI kontajner, protože se používá jen v kompozičním rootu a přinejhorším po změně DI kontajneru jen znovu implementujeme rozhraní IViewFactory, které interně použije API DI kontajneru.
    Kde je problém s božskou Demeter?:)

    To se mi líbí

  7. A ještě dodám:
    Pokud by Vám stejně vadila ta viewfactory, klidně si za ní dosaďte viewService, na které rovnou zavoláte viewService.Show (demeter, tell dont ask, blabla) a nebude vás zajímat, jak viewService váš nárok splní. Těchto abstrakcí (aka různých zdržovadel reálné práce a líbivých fasád) se dá napsat mnoho, otázkou je, k čemu takové množství abstrakcí vede, protože na nějaké místě v app musí být vykonánan reálná práce.

    To se mi líbí

  8. Díky za tak obsáhlý rozbor 🙂 Budu si ho muset ještě v klidu několikrát projít. Ale co se týká posledního příspěvku tak se dá říct, že to je přesně ono. Představoval jsem si místo factory jen nějkou servisu kterou zavolám a nestarám se co dál a jak dělá.

    To se mi líbí

  9. Jiří Landsman & Rene: naše řešení se liší v použití továrničky na DB oproti továrničce na Login. Úplně dobře srovnávat to nejde, protože PHP restartuje aplikaci s každým požadavkem a btw, já ani nevhodnější umístění továrničky neřešil.

    Šlo mi jen ukázat, jak továrničku za pomoci DI implementovat a hlavně: otevřít téma, že samotné označení továrnička pro něco s metodou GetDbObject() je vlastně zavádějící a chtělo by to termín vhodnější.

    To se mi líbí

  10. @David. Rozumím.

    Je pravda, že termín factory zde může být zavádějící, protože většina lidí svidí jako factory třídu, která vždy vyrobí novou instanci – pak by se asi hodilo DbService.GetCurrent() apod. (což je asi tvůj akcesor)

    Faktem ale je, že různé „factory“ a „buildery“ bývají obaleny „cache apod.“ dekorátory, takže mně pojmenovávání „factory“ zas tak nevadí.

    To se mi líbí

  11. René, už chápu, plně s tebou souhlasím a moc děkuji za nakopnutí 🙂 Ostatně takovéhle poučení od ostatních je důvod, proč veřejně píšu 🙂

    Zavedení HeavyLoginService bylo (zjevně nevhodné) zjednodušení – chtěl jsem tím názvem naznačit, že už ta HeavyLoginService je sama o sobě cenným zdrojem. Měl jsem raději jít dál a použít lehkou LoginService se závislostí na IDbConnection (u kterého nikdo nepochybuje, že je cenné). Když k tomu přidám zmínku o IDbConnection v úvodu článku, chápu, že jsem tě tím asi úplně zmátl 🙂

    Není to tak, že ILoginService by měla dostat nástroj, který jí při volání metody Login dovolí získat DbConnection – tedy odkaz na továrnu, která vyrábí objekty s životním cyklem per request?

    No to je právě otázka – měla by implementace ILoginService dostat přímo nainjectované IDbConnection nebo by měla dostat jen nějakou factory?

    Protože můžeme předpokládat, že IDbConnection je cenný zdroj, bylo by správné předat jen továrnu – ta by nám umožnila IDbConnection nejen získat, ale i po použití vrátit (Release).

    V případě PerWebRequest lifestyle pro IDbConnection bychom sice Release volat nemuseli (protože nebude nic dělat), ale pokud chceme mít implementaci ILoginService supr-dupr čistou a univerzální, měli bychom s metodou Release počítat.

    To je pěkná teorie, ale jak pak bude vypadat implementace metody Login? Na jejím začátku si vyzvedeme z factory IDbConnection a na konci metody (ve finally) ho vrátíme? To sice dává smysl, ale přijde mi to jako zbytečný infrastrukturní šum. Jak tohle pěkně vyřešit? Nebo se s tímhle smířit?

    Když se opět odprostíme od univerzálnosti a smíříme se s použitím jen ve webové aplikaci (~PerWebRequest), pak můžeme odstranit metodu Release (protože nic nedělá). Takže nám zůstane jen vyzvednutí IDbConnection na začátku metody Login. Pokud jsem to dobře pochopil, René, tak takhle vypadá tvé řešení.

    Já bych si ho jen dovolil zkusit (nic proti němu ale nemám, jen je třeba myslet na to PerWebRequest-specifikum) o kousek vylepšit.

    Udělal bych to tak, že implementace ILoginService by nezávisela na oné továrně, ale přímo na IDbConnection. Tím by nám z metody Login odpadlo vyzvednutí IDbConnection pomocí factory a metoda by byla pěkně čistá a dělala jen svou práci.

    Teď je otázka, jak tuto závislost nakonfigurovat. Pokud brzy po vytvoření instance ILoginService dochází k volání Login, nainjectoval bych tam přímo nějakou živou implementaci IDbConnection.
    Pokud tam je ale nějaká prodleva, která nám vadí (příp. se Login nemusí zavolat vůbec), pak právě zde bych nasadil lazy loading pomocí proxy. Tj. nainjectoval bych lazy-load-proxy pro IDbConnection.

    Snad je to teď srozumitelnější – mně se použití proxy líbí, jen mi přijde, že proxy pro ILoginService je použita proto, že se předpokládá, že konkrétní LoginService používá nějakpou závislost, jejíž životní cyklus je jiný než životní cyklus ILoginService.
    Ano, máš naprostou pravdu. Resp. jak píšu v úvodu komentáře – samotnou HeavyLoginService jsem považoval za cenný zdroj.

    Takže abych to za sebe shrnul – lazy-load-proxy je vhodná jen na cenné zdroje a to za předpokladu, že cenný zdroj má PerWebRequest lifestyle (resp. nemá cenu volat Release na factory).

    To se mi líbí

  12. Ptám se, protože bych tomu rád porozumněl:

    jestliže se rozhodneme nedávat továrnu/accessor do konstruktoru controlleru/presenteru protože implementace závislostí není jejich starost (controller říká „na čem závisí“, ne „jak to, na čem závisí, bude napsané“) a to, že závislost použijí jenom někdy v jedné své metodě je vedlejší, tak jak potom proti stejnému principu obstojí fakt vkládat do ILoginService továrnu kvůli tomu, že závislost této třídy je náročná (IDbConnection) a může se použít jen někdy v jediné metodě (login)?

    To se mi líbí

  13. Už na to narážel Jiří Knesl. V konstruktoru nic nedělat, jen uložit proměnné. Rád bych na to navázal. Neměl by se IDbConnection postarat, aby byl lazy?

    Tím pádem je pak jedno jestli zavolám:
    1. new Controler(new LoginServiceProxy(function(continer) {container.getDbConnection()}))
    2. new Container(new LoginService(new DbConnectionFactory()))
    3. new Container(new LoginService(new DbConnection()))

    Pokaždé jde pouze o vytvoření třídy, která si uloží parametry z konstruktoru.

    Z toho plyne otázka. Neměly by se pouze objekty obstarávající cenné zdroje chovat lazy?

    To se mi líbí

  14. Martin: Nad otázkou, zda záviset přímo na IDbConnection nebo jen na továrně pro IDbConnection, jsem se snažil zamyslet v mém posledním komentáři.
    Nejlepší, k čemu jsem došel, je to, že když zavádíme nějakou dependency, tak musíme vědět, zda je ta dependence cenná (~factory) nebo ne. Nenapadá mě možnost, jak navrhnout API tak, aby se zabránilo tomu, aby někdo přímo závisel na cenném zdroji (tj. jak vynutit použití factory).

    bene: Ano – přesně to píšu v posledním odstavci mého posledního komentáře 🙂
    Btw. k těm třem možnostem:
    1) Ta LoginServiceProxy je zbytečná. Kdybys tam místo toho měl LoginService, bylo by to OK a bylo by to velmi podobné jako 2)
    3) Toto je ok, pokud je new DbConnection() levná operace, nebo pokud je to těžká operace, ale to IDbConnection se vždy použije. Pokud by se použilo jen občas, raději bych použil tu lazy-load-proxy pro IDbConnection.

    To se mi líbí

  15. Martin, Augi: „controller říká „na čem závisí“, ne „jak to, na čem závisí, bude napsané“ plně souhlasím. Proto jsem také do controlleru dal továrnu z důvodu, že controller ví, že ji potřebuje jen výjimečně. Chápal bych oponenturu ve smyslu, že to je předčasná optimalizace, nicméně ona dle zadání předčasná není.

    To se mi líbí

  16. Augi: Ty tři možnosti jsem uvedl proto, že se všechny objevily jako možná řešení. Ale ve finále pokud se sama IDbConnection bude chovat lazy, tak vlastně všechny tři jsou ekvivalentně náročné na vytvoření. Tudíž první dvě zbytečné a vlastně se řeší něco co nemusí vůbec nastat 🙂

    To se mi líbí

  17. Augi: Jak potom zjistit náročnost onoho zdroje? Chci říct, pokud bych se držel principů OOP, tak bych jako třída implementující ILoginService neměl mít ani potuchy o tom, co se děje uvnitř IDbConnection. Dokonce v reálné situaci by místo IDbConnection mohla záviset na IStorage nebo IRepository – v takovém případě by zjištění cennosti byl skutečný oříšek.

    Zdá se, že toto řeší cesta, kterou nastínil bene a Jirka Knesl, tj. že každá třída si náročnost svých operací hlídá sama a dle potřeby je může také kešovat.

    DavidGrudl: Ač se víc kloním k řešení popsanému výše, tady mi přišlo divné, aby někdo odmítl tvé řešení a jako lepší variantu přesunul „továrnu“ z konstruktoru presenteru do konstruktoru třídy implementující ILoginService. Nevidím rozdíl mezi „presenter ví o náročnosti konkrétní třídy, na které závisí“ a „služba ví o náročnosti konkrétní třídy, na které závisí“.

    To se mi líbí

  18. @David: To je přesně to, co jsem se snažil popsat i v příspěvku výše – ty nevíš, jestli budeš zdroj využívat a já u view popisuju případ, kdy nevím přesně která view a v jakém scénáři budu používat.
    O žádné předčasné optimalizaci nemůže být řeč.

    Jsou tady dvě pojetí:

    1) Vy víte, že tohle je náročný zdroj, a proto nám cpete továrnu/akcesor, což je předčasná asbtrakce, kterou řešíte hned na začátku zbytečně továrnou.

    Versus

    2) Zdroj používám jen někdy (db connection?) a/nebo používám různé zdroje za stejným rozhraním, a proto jsem se rozhodl, že injektovan8 továrna/akcesor přesně vystihuje, co chci dělat.

    @Martin: Neím, jestli ta poznámka „tady mi přišlo divné, aby někdo odmítl tvé řešení a jako lepší variantu přesunul „továrnu“ z konstruktoru presenteru do konstruktoru třídy implementující ILoginService. “ byla cílena na mě, ale raději podotknu, že jsem Davidovo řešení neodmítal, protože jsem jej ani při psaní příspěvků nečetl a vycházel jsem z toho, co psal Augi.

    To se mi líbí

  19. A k té db komponentě:
    Jaké mohu mít motivace předávat akcesor/továrnu na výrobu db connection.

    1) Db connection je cenný zdroj, který má speciální životní cyklus.

    2) Tento speciální životní cyklus zdroje by neměl řídit každý vývojář v každém místě v aplikaci, protože to povede k chybám a výkonnostním problémům.

    3) A proto vytvořím abstrakci nad zdrojem, která (i bez DI kontajneru) garantuje např:

    a) Že zdroj žije per request (například).

    b) Pokud není zdroj v průběhu requestu použit, není ani instanciován.

    c) Bude uvolněn (vrácen do connection poolu) po každém requestu.

    V aplikacích s DI kontajnerem sám DI kontajner dokáže spravovat životní cyklus registrovaných tříd, ale i zde můžeme chtít použít továrnu/akcesor:

    Tato abstrakce, ať už jí říkáme továrna/akcesor garantuje, že:

    1) V aplikaci si nevynucujeme instanciování zdroje tak, že bychom zdroj přijímali v konstruktoru, ale požadujeme akcesor/továrnu, která
    zdroj vrátí. Pokud v konstruktoru vyžadujeme továrnu, říkáme tím, že použití zdroje nemusí být povinné a/nebo máme další nároky na vytvoření zdroje, které ale budeme znát až v okamžiku volání metody továrny.

    Tato továrna i ve spolupráci s DI kontajnerem garantuje, že preferovaný životní cyklus zdroje je řízen automaticky.

    2) Můžeme se rozhodnoot, jestli zdroj vystavíme přímo:

    a) Pokud ano, registrujeme jej v DI kontajneru (per request) a někdo může v konstruktoru vyžadovat odkaz na zdroj. Pak konstruktorem říkáme, že naše závislost na zdroji je povinná a že bez něj nejsme schopni splnit žádné scénáře.

    b) Zdroj ani přes DI kontajner dostupný není a my si vynucujeme přístup z jiných objektů přes továrnu, protože továrna je odpovědná ještě za dodatečnou inicializaci/správu objektu.

    Všechny tyto varianty se dají použít, záleží jen na konkrétním scénáři a momentálni chuti křižovat někoho, s kým z principu nesouhlasíme.:)

    Mimochodem, některé „starší“ jazyky by kupodivu některé scénáře řešily mnohem elegantněji. V C++ bych mohl předat svůj „smart pointer“, který lazy inicializaci zapouzdří a nemusím ani použít proxy, ani Lazy, abych měl stejné rozhraní.

    To se mi líbí

  20. Augi: Jak potom zjistit náročnost onoho zdroje? Chci říct, pokud bych se držel principů OOP, tak bych jako třída implementující ILoginService neměl mít ani potuchy o tom, co se děje uvnitř IDbConnection. Dokonce v reálné situaci by místo IDbConnection mohla záviset na IStorage nebo IRepository – v takovém případě by zjištění cennosti byl skutečný oříšek.
    Ano, mně se to také nelíbí, že by uživatel měl rozhodovat, jestli je ta dependence cenná nebo ne. Proto jsem pro to, aby třída vždy závisela přímo na tom, co potřebuje. Pokud je to cenný zdroj, může se provést lazy loading na pozadí pomocí té proxy, jak jsem to popsal v článku. A to je záležitost konfigurace cenného zdroje (v kompozičním rootu).

    Problém ale je, že když bych tu třídu chtěl použít v ne-webovém prostředí, kde může být životnost objektu jiná než per-web-request, tak už budu potřebovat továrnu, abych přes ní mohl získaný zdroj i uvolnit. Možná tohle je ten problém, který nevidíš, protože děláš v php, kde je maximální životnost objektu per-web-request?

    Zdá se, že toto řeší cesta, kterou nastínil bene a Jirka Knesl, tj. že každá třída si náročnost svých operací hlídá sama a dle potřeby je může také kešovat.
    Plně souhlasím s tím, co napsal Jirka Kosek – taky mě napadlo, že by se všechno mohlo vytvářet lazy 🙂 Vlastně by mi stačilo, aby mi kontejner vytvářel všechno lazy přes tu mou proxy, tj. při prvním sáhnutí na proxy by vytvořil živou instanci. Pak by odpadlo přemýšlení nad tím, jaký zdroj je cenný nebo ne. Ale tu fine-grained konfiguraci v kompozičním rootu klidně udělám…

    To se mi líbí

  21. Augi: Neřeší tohle tenhle a jemu předcházející články? Tj. ta tovarna je v te proxy. Ale nejsem na .net žádnej expert takže možná jen blbě chápu obsah článku 🙂

    To se mi líbí

  22. Jo, tenhle a odkazované články jsem taky včera četl 😉
    Ano – že továrna je v proxy mám i v mém článku – tu továrnu mám ve formě lambdy accessor.

    To se mi líbí

Napsat komentář

Tento web používá Akismet na redukci spamu. Zjistěte více o tom, jak jsou data z komentářů zpracovávána.