IoC/DI v .NET

V poslední době se hodně mluví a píše o moderních programovacích technikách (TDD, DDD) a o s nimi souvisejících technologiích. Mezi ně patří i poněkud konstrbatá zkratka IoC/DI – Inversion of Control/Dependency Injection, na kterou se podíváme v tomto článku. Předem podotýkám, že je článek některé skutečnosti zjednodušuje a slouží především jako úvod do problematiky.

Inversion of Control

Vzpomínám si, že už před lety jsem někde četl, že by se mělo programovat proti interfacům, ne proti konkrétním třídám. Inversion of Control myšlenku programování proti rozhraním mohutně používá a v jistých ohledech dotahuje do konce. V případě IoC de facto nevytváříme aplikaci jako monolit, ale jako sadu komponent, které implementují různá rozhraní. Tyto komponenty s okolním světem opět komunikují opět pouze pomocí rozhraní, bez závislosti na konkrétních implementacích. Komponenta tak řeší opravdu jen svoje problémy, nestará se o okolní komponenty.
Aplikaci pak vdechneme život tím, že určíme, jak propojit komponenty do fungující aplikace.
Rozbití aplikace na komponenty (rozhraní), jejichž konkrétní implementace (třídy) lze jednoduše vyměnit, je velmi užitečná vlastnost. Můžeme díky tomu jednoduše sestavit více podobných aplikací, které se liší např. tím, odkud načítají data (MySql, MsSql, soubor, webová služba, …). V případě testování je pak snadná výměna komponent aplikace k nezaplacení.

Klíčové je, že pracujeme s rozhraními a téměř nikde by se tak nemělo vyskytovat klíčové slovo new, protože tím bychom se fixovali na nějakou konkrétní implementaci. S IoC tento úkol delegujeme na tzv. IoC kontejner. To je třída, která ví, jak získat instanci třídy, která implementuje dané rozhraní. Rozhraní IoC kontejneru může tedy vypadat nějak takto:

public interface IIoCContainer
{
  TService GetInstance<TService>();
}

Takže místo ICar c = new Car() nyní voláme třeba ICar c = OurContainer.Current.GetInstance<ICar>().

Různé IoC kontejnery ale mají různá rozhraní a proto by případný přechod na jiný (lepší) IoC kontejner znamenal v lepším případě udělat replace přes celý projekt (např. “OurContainer.Current.GetInstance” -> “BetterContainer.Instance.GetNewInstance“). To je samozřejmě fuj, ale problém řeší projekt CommonServiceLocator, který přináší jednotné rozhraní pro nejrozšířenější IoC kontejnery. Ono rozhraní má sice více metod, ale de facto se jedná jen o overloady metody GetInstance z výše uvedeného rozhraní – podívejte se sami. Btw. CommonServiceLocator se tak jmenuje proto, že se jedná o implementaci návrhového vzoru Service locator, což je takový centrální bod aplikace.

Kontejner je třeba (typicky na začátku programu, při startu webové aplikace apod.) patřičně nakonfigurovat, aby věděl, jak získávat implementace požadovaných rozhraní. Každý IoC kontejner má různé možnosti konfigurace a vytvoření jednotného rozhraní pro konfiguraci by znamenalo, že se zbytečně omezíme (protože hledáme průnik). Ale k vytvoření jednotného rozhraní pro konfiguraci kontejneru ani není motivace – konfigurace je totiž obvykle jen v pár souborech, takže není takový problém ji přepsat.
Syntaxe konfigurace je tedy pro každý IoC kontejner unikátní a bývá uvedena buď v kódu nebo v nějakém konfiguračním souboru. Konfigurace kontejneru v kódu může vypadat např. takto:

myContainer.Register<ICar, Car>();

Tady říkáme, že pokud někdo bude chtít rozhraní ICar, má se použít třída Car.

V praxi se setkáváme s různými požadavky na životnost objektů – vždy nechceme, aby nám metoda GetInstance vrátila novou instanci. Např. můžeme chtít, aby ve webové aplikaci objekt žil po celou dobu jednoho požadavku, tedy aby metoda GetInstance vracela v průběhu jednoho požadavku stále tutéž instanci. Nebo můžeme chtít, aby se vracela tatáž instance v průběhu celého programu (tedy singleton). Všechno tohle kvalitní IoC kontejnery umí. Základem je ale schopnost konfigurace a získání instance implementující dané rozhraní.

Dependency Injection

IoC kontejner většinou funguje tak, jak jsem popsal – řekneme mu, jaká třída implementuje jaké rozhraní (což by si btw. mohl zjistit pomocí reflexe sám, ale to může vést na nejednoznačnosti). Většinou ale explicitně neříkáme, jak konkrétně se má daná třída vytvořit – s tím se musí kontejner nějak vypořádat sám. Pokud by Vás zajímalo, co se může dít uvnitř, mrkněte na můj článek o vytváření instancí v době kompilace (v době kompilace IoC kontejneru!) neznámých typů za běhu.
Pokud má třída bezparametrický konstruktor, tak je to úloha poměrně snadná. Ale co když má konstruktor nějaké parametry? Pak přichází ke slovu tzv. constructor injection. Kontejner si zjistí, jaké parametry jakých typů konstruktor má. Konkrétní hodnoty parametrů pak získá tak, že “zavolá GetInstance<TParameter> sám na sebe”. Pak již nic nebrání tomu zavolat konstruktor se správnými parametry.
Dále se používá ještě setter injection, kdy se IoC kontejner postará po vytvoření objektu ještě o přiřazení správné hodnoty do property, která má public setter (a samozřejmě i nějaký getter).

Praxe

Dejme tomu, že máme auto, které má motor, a chceme nastartovat nějaké auto s nějakým motorem:

public interface IEngine
{
  void Start();
}

public interface ICar
{
}

public class Engine : IEngine
{
  public void Start()
  {
    Console.WriteLine("engine started");
  }
}

public class Car : ICar
{
  public Car(IEngine engine)
  {
     engine.Start();
  }
}

public static class Bootstrapper
{
  public static void Run()
  {
    // vytvoreni kontejneru
    var container = new OurContainer();
  
    // propojeni naseho IoC kontejneru na Service locator
	// using Microsoft.Practices.ServiceLocation;
    ServiceLocator.SetLocatorProvider(c => container);
	
    container.Register<ICar, Car>();	
    container.Register<IEngine, Engine>();
  }
}

public static class Application
{
  public static void Main()
  {
    Bootstrapper.Run(); // konfigurace
	
    // kontejner vi, ze ma pouzit tridu Car
    // take vi, ze rozhrani IEngine implementuje trida Engine
	// using Microsoft.Practices.ServiceLocation;
    ICar c = ServiceLocator.Current.GetInstance<ICar>();
  }
}

Je to velmi jednoduché – registraci provedeme pomocí metody Register, získání instance pak pomocí metody GetInstance.

V praxi je na výběr z mnoha IoC/DI kontejnerů. Mezi nejznámější patří Unity (přímo od Microsoftu), Microkernel a Windsor Container od Castle, Spring.NET, StructureMap, Autofac, Ninject, PicoContainer.NET
Ale není nic extrémně složitého vytvořit si vlastní jednoduchý IoC(/DI) kontejner, takže pojďme na to:

public class OurContainer
{
  // mapovani z typu (nejcasteji rozhrani) na metodu, ktera vraci instanci implementujici dane rozhrani
  private readonly Dictionary<Type, Func<OurContainer, object>> _TypeCreators = new Dictionary<Type, Func<OurContainer, object>>();

  // registace typu
  public void Register(Type type, Func<OurContainer, object> creator)
  {
    _TypeCreators[type] = creator;
  }

  // vraceni instance
  public object GetInstance(Type type)
  {
     return _TypeCreators[type](this);
  }
  
  // genericke varianty predchozich
  public void Register<T>(Func<OurContainer, object> creator)
  {
    Register(typeof(T), creator);
  }

  public T GetInstance<T>()
  {
    return (T)GetInstance(typeof(T));
  }  

}

Jádrem našeho řešení je to, že pro každý typ (interface) máme přiřazeného delegáta, který umí vytvořit instanci třídy implementující dané rozhraní.
Tento náš kontejner můžeme nakonfigurovat tímto způsobem:

var container = new OurContainer();
container.Register<IEngine>(c => new Engine());
container.Register<ICar>(c => new Car(c.GetInstance<IEngine>()));

Zajímavý je poslední řádek – konstruktor třídy Car vyžaduje jeden parametr typu IEngine, takže při volání musíme nějak získat instanci třídy implementující toho rozhraní. A není nic krásnějšího než k této úloze použít “rekurzivně” IoC kontejner.

Nyní tedy máme k dispozici IoC kontejner, který sice neumí DI, automatické vytváření instancí (dodáváme delegáta, který se o to postará), ani některé další vymoženosti (např. řízení životnosti objektů), ale přesto může být užitečný (už jen proto, že to není žádný zbytečně přebujelý moloch).
Pojďme dále implementovat automatické vytváření instancí, včetně constructor injection:

public void Register<T, TImplementation>()
{
  // ziskame konstruktory implementujici tridy
  var ctors = typeof(TImplementation).GetConstructors();
  if (ctors == null || ctors.Length == 0)
  {
    throw new InvalidOperationException(string.Format(
	  "Appropriate constructor not found in type '{0}'.",
	  typeof(TImplementation).FullName));
  }
  var ctor = ctors[0]; // vezmeme prvni konstruktor

  // zkonstruujeme lambda expression, ktera vytvori instanci implementacni tridy
  // lambda ma jeden vstupni parametr typu OutContainer
  var containerParameter = Expression.Parameter(typeof(OurContainer), "container");
  var creator = Expression.Lambda(typeof(Func<OurContainer, object>),
    Expression.New(ctor, 
      ctor.GetParameters()
	  // pro kazdy parametr konstruktoru vygenerujeme volani GetInstance na kontejneru
	  .Select(p => (Expression)Expression.Call(containerParameter, "GetInstance", new[] { p.ParameterType }))),
      containerParameter);

  // zaregistrujeme zkompilovanou lambdu
  Register<T>((Func<OurContainer, object>)creator.Compile());
}

Kód funguje tak, že si při registraci sestavíme lambda expression, kterou následně zkompilujeme a použijeme jako delegáta, který umí vytvořit třídu implementující dané rozhraní. Nic složitého.
Konfigurace je nyní o poznáni pěknější:

var container = new OurContainer();
container.Register<IEngine, Engine>();
container.Register<ICar, Car>();

Tento náš IoC/DI můžeme dále vylepšovat. Např. podpora pro instance žijící během jednoho HTTP požadavku lze jednoduše implementovat pomocí kolekce System.Web.HttpContext.Current.Items.
Dalším logickým krokem by mohlo být upravení našeho kontejneru tak, abychom ho mohli použít pomocí CommonServiceLocatoru – v tomto případě stačí odvodit naši třídu od abstraktní ServiceLocatorImplBase a implementovat dvě metody – DoGetInstance a DoGetAllInstances. To už ale jistě zvládnete sami 😉


Příspěvek byl publikován v rubrice Programování a jeho autorem je Augi. Můžete si jeho odkaz uložit mezi své oblíbené záložky nebo ho sdílet s přáteli.

13 komentářů u „IoC/DI v .NET

  1. Pěkný článek. V poslední době jsem se zajímal o opravdový význam pojmů IoC, Dependency injection(DI) a Dependency inversion principle(DIP). Ze zkoumání článků od Fowlera:

    http://martinfowler.com/articles/injection.html
    http://martinfowler.com/bliki/InversionOfControl.html

    a wikipedie jsem dospěl k závěru, že IoC není jenom o decouplingu přes interface, ale jak píše Fowler obecně o změně(inverzi) řízení programu oproti procedurálnímu přístupu. Takže IoC by byl nadřazený pojem k DIP, který je právě už o tom decouplingu přes interface a dělí se na Dependency Injection (constructor, setter, …) a Service Locator.

    Nakonec není důležité, znát drobné rozdíly a tahat se za slovíčka, ale pro zajímavost: co si o tom myslíte?

  2. Ano ano, vidím to úplně stejně. DIP je konkrétní “implementace” IoC.
    Článek jsem chtěl pojmout spíše prakticky, tak jsem tam nechtěl zatahovat moc abstraktních pojmů, které by čtenáře mohly rušit.
    Pro mě je dosud neznámé téma vždy stravitelnější, když nejdřív vidím reálné výhody. Pak teprve mám motivaci pustit se do bližšího zkoumání. Z toho jsem vycházel při psaní článku…
    Btw. Tvůj (moc pěkný) článek o DI jsem četl 🙂

  3. Díky. Myslím si, že v tomhle směru “názvosloví” lehce kolísá, tak jsem udělal malý výzkum a zajímal mě i Tvůj názor.

    Jinak souhlasím s tím, že takové detaily by byly v tomto článku spíše ku škodě. Nejdůležitější nakonec je principům rozumět a umět je použít. 🙂

  4. K názvosloví: IoC je záhadně znějící, naprosto neintuitivní termín, a asi proto se chytnul 🙂

  5. Dovolím si kontroverzní poznámku k TDD. Jsem dlouhodobě skeptický k tomuto přístupu. Pokusím se vyjádřit proč. Pokud něco vyvíjím, mám požadavek na to, aby mi to prošlo pro všechny varianty vstupních údajů. Ne pro jednu. Pokud něco modifikuju nebo přidávám, mám opět požadavek, aby to, co je nové, fungovalo za všech okolností, ale také, aby to, co je staré fungovalo beze změny. A tomu přizpůsobuju samotné strukturování aplikace a kódování. K čemu mi budou testy pro jednu variantu vstupů? Maximálně k tomu, abych s nimi ztrácel čas. Že mi někdy odhalí chybu, pokud udělám omylem zásah do stávající funkčnosti? Opakuju – snažím se programovat tak, abych stávající funčnost neovlivnil a stává se mi to minimálně. Pravděpodobnost, že se mi to stane a takový test to odhalí je tak malá, že statistická časová výhoda psaní takových testů je méně než nulová. A že mi takové testy pomohou lépe strukturovat můj kód. Mám o tom silnou pochybnost, ale už to nechci víc prodlužovat… 😉

  6. A TDD tě nutí k tomu, abys psal jen testy, které testují jen jednu variantu? IMHO ne…
    A jsi skeptický jen vůči TDD (takže test-first) nebo testování obecně?

  7. Právě naopak. Považuju vlastní komplexní testování za nedílnou součást vývoje a práce vývojáře. V posledních letech se v jenom trochu lepších sw firmách stávají standardem testeři. A bohužel má zkušenost s tím je negativní v tom smyslu, že vývojáře to často svádí k tomu vyplivnout kód a předat ho testerovi, aniž jsem si ověřil, jestli funguje. “Však on mi to tester otestuje”. Mám ten názor, že to, jestli tester ve firmě je nebo není nemá mít na práci programátora vliv. Programátor když něco pouští, tak má být přesvědčen o tom, že je to ok.

  8. Trochu jsem se teď díval na články o TDD a našel jsem jeden pěkný
    http://www.fi.muni.cz/usr/jkucera/pv109/2005/xvlcek1.htm
    A je tam:
    1.Napište test, napoprvé je jasné, že test určitě selže nebo nepůjde vůbec zkompilovat.
    2.Rychle implementujte testovanou logiku tak, aby test proběhnul.
    3.Pomocí refaktorování a opakovaného spouštění testu upravte kód do přijatelné podoby.
    Bod 1 je ok, ale body 2 a 3 jsou tragické. Je opravdu toto TDD? 🙂

  9. Ale pokud se to dělá inkrementálně a body 1,2,3 se opakují, tak to může být užitečné. Asi měním (zatím trochu) názor. Bylo by zajímavé nahradit celou prezentační vrstvu testovacími rutinami.

  10. Ale v TDD by měl to kolečko 1-3 rozhodně dělat vývojář. Testeři z QA by IMHO neměli primárně řešit selhávající unit-testy (TDD je jen o unit-testech).

  11. Jenom pro upřesnění. Komentář jsem ťukal na mobilu, takže byl stručný a zřejmě zavádějící 😉 To není narážka na Augiho, ale na marvapa 😉 Na jeho postupných komentářích je vidět určitá názorová geneze a konvergence k TDD 😉 Proto ta moje poznámka. Bez diskuze a bez kritiky není progrese.

Napsat komentář

Vaše emailová adresa nebude zveřejněna.