IoC a Dependency Injection

Popis a ukázka jednoduché implementace v C#

Co je to IoC (Inversion of control)?

IoC je skupina návrhových vzorů. Česky obrácené řízení. A je tím myšleno, že objekty které na něčem závisí si tuto závislost neřídí samy. Neboli tyto objekty si samy nevytvářejí instance toho co potřebují, nesahají si pro to do statických proměnných a pod. Ale naopak je jim to přiděleno automatickým mechanismem, o kterém tyto třídy vnitřně nevědí.

Co je to DI (Dependency Injection)?

Česky injektáž závislostí je konkrétní implementace principu IoC. DI se skládá z kontejneru, mechanismu který závislosti předává a způsobu jakým se závislosti předávají - zde se budu věnovat jen Constructor injection, což je i defaultní způsob vestavěný v .NET Core.

Co je to závislost?

Závislostí v tomto kontextu myslíme například to že Controller potřebuje přečíst nějaká data z databáze aby je odeslal do View. K tomu potřebuje nějakou třídu, která z databáze umí číst. Tudíž Controller závisí například na třídě Database.

Ukázka implementace IoC/DI s vlastním IoC containerem v C#

Nejprve si připravíme tři třídy a k ním rozhraní, dle architektury MVC neboli model, view, controller.

Jednoduchý příklad "data access" třídy, která něco čte z databáze. Ta představuje Model.


interface IDatabase
{
    string ReadData();
}
class Database : IDatabase
{
    public string ReadData()
    {
        return "data z databáze";
    }
}
    

Tato třída nám bude představovat View.


interface IView
{
    string Data { get; set; }
}
class View : IView
{
    public View(string data)
    {
        Data = data;
    }
    public string Data { get; set; }
}
    

Controller vyžaduje v konstruktoru Databázi aby z ní mohl číst data a naplnit těmito daty View.


interface IController
{
    IView GetView();
}
class Controller : IController
{
    private IDatabase _Database;
    public Controller(IDatabase database)
    {
        _Database = database;
    }
    public IView GetView()
    {
        string data = _Database.ReadData();
        return new View(data);
    }
}            
    

Pro implementaci IoC/DI potřebujeme kontejner. Náš kontejner obsahuje dvě veřejné metody, první "RegisterService" zaregistruje službu, a "ResolveService" vrací hotovou instanci s vyřešenými závislostmi.
Po vzoru .NET Coru chceme po uživateli interface služby a konkrétní implementaci služby (rozhraní by mohlo v budoucnu ulehčit výměnu služby za jinou, a nebo náhradu služby za "developement" službu), proto zde máme ještě slovník s přiřazením interface-implementace _Types.


class IocContainer
{
    private readonly Dictionary<Type, Type> _Types = new Dictionary<Type, Type>();
    
    public void RegisterService<TServiceInterface, TImplementation>() 
        where TServiceInterface : class 
        where TImplementation : class, TServiceInterface
    {
        ...
    }
    
    public TServiceInterface ResolveService<TServiceInterface>() where TServiceInterface : class
    {
        ...
    }
}
    

Přidání služby samo o sobě nic moc nedělá, kouzlo přichází až po zavolání "ResolveService" a to nejdůležitější se děje v privátní metodě "Resolve" Která rekurzivně řeší závislosti požadované třídy.
Rekurzivně proto že třída může vyžadovat v konstruktoru jinou třídu, která ale v konstruktoru také může něco požadovat...a tak dále. Způsobu kdy jsou závislosti řešeny přes konstruktor se říká Constructor Injection. A je to i výchozí způsob při použití ASP.NET Core Frameworku (Nejsem si dokonce jistý jestli samotný .NET Core vůbec umožňuje dělat to jinak, např. použít property injection, neboli atributem označené properties do kterých se závislosti obdobným způsobem injektují).


class IocContainer
{
    private readonly Dictionary<Type, Type> _Types = new Dictionary<Type, Type>();

    public void RegisterService<TServiceInterface, TImplementation>() 
        where TServiceInterface : class 
        where TImplementation : class, TServiceInterface
    {
        _Types[typeof(TServiceInterface)] = typeof(TImplementation);
    }

    public TServiceInterface ResolveService<TServiceInterface>() where TServiceInterface : class
    {
        return (TServiceInterface)Resolve(typeof(TServiceInterface));
    }

    private object Resolve(Type interfaceType)
    {
            //konkrétní typ
            var concreteType = Types[interfaceType];
            //první-defaultní konstruktor
            var firstCtor = concreteType.GetConstructors()[0];
            //parametry konstruktoru (ParameterInfo[])
            var ctorParamInfos = firstCtor.GetParameters(); 
            
            //rekurzivní naplnění závislostí
            var parameters = new List<object>();
            foreach (var ctorParaInfo in ctorParamInfos)
            {
                parameters.Add(Resolve(ctorParaInfo.ParameterType));
            }
            //jednodušší řešení cyklu výše, ne moje:
            //var parameters = ctorParams.Select(param => Resolve(param.ParameterType)).ToArray();

            //vyvolání konstruktoru s parametry (vytvoření instance)
            return firstCtor.Invoke(parameters.ToArray()); 
    }
}
    

Ukázka použití našeho IoC containeru. Nejprve se služby zaregistrují. Důležité je zaregistrovat vše co bude potřeba. Pokud bude něco zaregistrováno a nebude to použito, nic zásadního se neděje - k inicializaci dojde až když je konkrétní služba vyžádána.


public static void Test()
{
    Console.WriteLine();
    Console.WriteLine("Test dependency injection");

    //inicializace kontejneru
    IocContainer services = new IocContainer();
    //přidání služeb
    services.RegisterService<IDatabase, Database>();
    services.RegisterService<IController, Controller>();
    //vypořádání závislostí a vytvoření instance
    var controller = services.ResolveService<IController>();

    //hotový kontroler si z databáze vyžádá data naplní jimi view a vrací ho
    var view = controller.GetView();
    var result = view.Data;

    //výstup
    Console.WriteLine(result);
    Console.ReadKey();
}
    

Co nám chybí

Chybí nám onen vyšší mechanismus viz popis DI výše. Co to je a kde to vzít? ...přesně nevím, ale předpokládám že onen mechanismus je součástí .NET Core, protože v metodě ConfigureServices třídy Startup přidáváme služby (např services.AddTransient...), ale již se nestaráme o vytváření instancí Controllerů.
V našem případě tento mechanismus píšeme velmi zjednodušeně přímo do statické metody Test().
Dále nám chybí řešení lifetime (životního cyklu) služeb v našem kontejneru (to co děláme v .NET Coru pomocí AddSingleton, .AddTransition, .AddScoped). I přes tyto nedostatky si myslím že pro pochopení je toto dobrý začátek.

Doplnění

Jako ověření funkčnosti si můžete ještě přidat například interface a třídu ILogger a Logger, kterou bude vyžadovat třída Databáze v konstruktoru. Tím už bychom měli závislosti trochu zajímavější Controller závisí na Databaze a ta na Logger. Kód by pak vypadal nějak takto:


//Logger
interface ILogger
{
    void Log(string message);
}
class Logger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"Log:{message}");
    }
}

//Databáze
class Database : IDatabase
{
    ILogger _Logger;
    public Database(ILogger logger)
    {
        _Logger = logger;
    }
    public string ReadData()
    {
        _Logger.Log("čtu data z databáze");
        return "data z databáze";
    }
}

//Test
public static void Test()
{
    ...
    services.RegisterService<ILogger, Logger>();
    ...
}
    

Výsledek testu pak vypadá následovně:

Ukázka jednoduché implementace IoC containeru

Použito:

  • Pro zobrazení C# kódu použito highlight.pack.js link