Ahhoz, hogy megértsük, hogyan is működik egy IoC konténer, érdemes egy sajátot készÃteni. Ez elsÅ‘ ránézésre ijesztÅ‘en hangozhat, azonban a feladat egyáltalán nem bonyolult.
ElÅ‘ször érdemes a tervezést egy interfész definiálásával kezdeni, ami leÃrja az IoC konténerünk feladatait. A konténernek két feladata van: TÃpusok regisztrálása (Register) és tÃpusok feloldása (Resolve).
A feloldás jelenthet példányosÃtást is, vagy már egy meglévÅ‘ példány visszaadását. A meglévÅ‘ példány visszaadása művelet lényegében singleton tÃpus kezelést biztosÃt anélkül, hogy a tÃpusainkat singleton mintára valósÃtottuk volna meg.
A példaként elkészÃtendÅ‘ IoC konténerben már a tÃpus regisztrálásának pillanatában el kell dönteni, hogy példányosÃtható tÃpust regisztrálunk, vagy singleton példányt. Az IoC konténert leÃró interfész legyen a következÅ‘:
namespace SimpleIoC
{
public interface IIoCContainer
{
void Register<TInterface, TClass>()
where TInterface : class
where TClass : class, TInterface;
void RegisterSingleton<TInterface, TClass>()
where TInterface : class
where TClass : class, TInterface;
TInterface Resolve<TInterface>()
where TInterface : class;
}
}
A regisztráló metódusok esetén meg kell adnunk egy interfészt és egy hozzá tartozó implementációt. Generikus megkötésekkel ki lett kötve, hogy csak olyan tÃpus fogadunk el implementációnak, ami ténylegesen az interfész tÃpusból öröklÅ‘dik. Azt, hogy a TInterface tÃpus valójában interfész-e azt csak futás idÅ‘ben tudjuk ellenÅ‘rizni, mivel nincs generikus megkötés arra, hogy egy tÃpus interfész vagy sem.
A feloldó metódus (Resolve) esetén csak az interfészt kell megadnunk.
Ahhoz, hogy rendes és kulturált hibakezelésünk legyen, definiáljunk egy saját kivétel osztályt, amibe majd az IoC konténer működése során keletkezÅ‘ hibákat csomagoljuk. Az osztály definÃciója a következÅ‘:
using System;
namespace SimpleIoC
{
[Serializable]
public class ResolveException : Exception
{
public ResolveException(Type t) :
base($"A következÅ‘ tÃpust nem lehet feloldani: {type.FullName}") { }
public ResolveException() : base() { }
public ResolveException(string message) :
base(message) { }
public ResolveException(string message, Exception innerException) :
base(message, innerException) { }
}
}
A konténer implementációja a következő:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace SimpleIoC
{
public class IoCContainer : IIoCContainer
{
private Dictionary<Type, Type> _instances;
private Dictionary<Type, object> _singletons;
public IoCContainer()
{
_instances = new Dictionary<Type, Type>();
_singletons = new Dictionary<Type, object>();
}
public void Register<TInterface, TClass>()
where TInterface : class
where TClass : class, TInterface
{
_instances.Add(typeof(TInterface), typeof(TClass));
}
public void RegisterSingleton<TInterface, TClass>()
where TInterface : class
where TClass : class, TInterface
{
_singletons.Add(typeof(TInterface), CreteInstance(typeof(TClass)));
}
public TInterface Resolve<TInterface>() where TInterface : class
{
Type requested = typeof(TInterface);
return Resolve(requested) as TInterface;
}
private object Resolve(Type requested)
{
if (_singletons.ContainsKey(requested))
{
return _singletons[requested];
}
if (_instances.ContainsKey(requested))
{
return CreteInstance(_instances[requested]);
}
throw new ResolveException(requested);
}
private object CreteInstance(Type requested)
{
ConstructorInfo[] constructors = requested.GetConstructors();
ConstructorInfo full = (from constructor in constructors
where
constructor.IsPublic
orderby
constructor.GetParameters().Length descending
select
constructor).FirstOrDefault();
if (full == null)
throw new ResolveException($"A következÅ‘ tÃpusnak nincs publikus konstruktora: {requestedType.FullName}");
try
{
var parameters = full.GetParameters();
var dependencies = new List<object>(parameters.Length);
foreach (var parameter in parameters)
{
object resolved = Resolve(parameter.ParameterType);
dependencies.Add(resolved);
}
return Activator.CreateInstance(requested, dependencies.ToArray());
}
catch (Exception ex)
{
throw new ResolveException($"A következÅ‘ tÃpust nem lehet létrehozni: {requestedType.FullName}", ex);
}
}
}
}
A program a példányokat és a singleton tÃpusokat két külön Dictionary tÃpusban tárolja. TÃpus feloldásnál a singleton tÃpusok élveznek prioritást.
A Register és a RegisterSingleton metódusokban ellenÅ‘rizve van, hogy a TInterface valóban interfész-e. Ha ez nem interfész, vagy már korábban regisztrálva lett a tÃpus, akkor hibát kapunk.
A tÃpusfeloldás egy privát, object tÃpust visszaadó Resolve metóduson keresztül történik Type információk alapján. Erre azért van szükség, mert a tÃpus konstruktorának hÃvásakor reflection-nel ilyen formában kapjuk vissza a tÃpusinformációkat. A CreteInstance metódus a tÃpus implementációjában megkeresi a legtöbb paraméterrel rendelkezÅ‘ konstruktort, majd a belsÅ‘ Resolve metódust használva rekurzÃv módon összegyűjti a függÅ‘ségeket. A Ezt követÅ‘en példányosÃtja a kért tÃpust.
A konténert a következÅ‘ példakóddal teszteljük, amelyben a RandomInstance nevű osztály függ egy ILog és egy IRandomProvider példánytól. A kapott ILog implementációval naplózni tudunk, az IRandomProvider segÃtségével pedig véletlenszámot tudunk elkérni. Az osztály és a hozzá tartozó interfész a következÅ‘:
namespace SimpleIoCPelda
{
public interface IRandomInstance
{
void Print();
}
public class RandomInstance : IRandomInstance
{
private readonly ILog _log;
private readonly int _number;
public RandomInstance(ILog log, IRandomProvider randomProvider)
{
_log = log;
_number = randomProvider.RandomGenerator.Next(0, 100);
}
public void Print()
{
_log.Write("Instance created. Random number: {0}", _number);
}
}
}
A fő program az ILog és az IRandomProvider interfészek implementációit Singleton módban regisztrálja, mert ezekből egy lesz a program teljes életciklusa során. Ezt követően a program regisztrálja a RandomInstance osztály implementációját is annak interfészével. Ezt követően 10 alkalommal feloldja az osztályhoz tarozó interfészt az IoC konténeren keresztül, ami a függőségek feloldása és injektálása után egy példányt fog visszaadni.
Ezen a példányon a Print metódus meghÃvásával ellenÅ‘rizzük, hogy a függÅ‘ségek feloldása és a példányosÃtás sikeresen megtörtént-e. A tesztprogram kódja a következÅ‘:
using SimpleIoC;
using System;
namespace SimpleIoCPelda
{
class Program
{
static void Main(string[] args)
{
try
{
var container = new IoCContainer();
container.RegisterSingleton<IRandomProvider, RandomProvider>();
container.RegisterSingleton<ILog, Log>();
container.Register<IRandomInstance, RandomInstance>();
for (int i = 0; i < 10; i++)
{
IRandomInstance instance = container.Resolve<IRandomInstance>();
instance.Print();
}
}
catch (ResolveException ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
}
}
A program kimenete:
Instance created. Random number: 11
Instance created. Random number: 42
Instance created. Random number: 46
Instance created. Random number: 84
Instance created. Random number: 13
Instance created. Random number: 55
Instance created. Random number: 64
Instance created. Random number: 9
Instance created. Random number: 88
Instance created. Random number: 90
Az ILog és IRandomProvider interfészek és implementációik:
using System;
namespace SimpleIoCPelda
{
public interface ILog
{
void Write(string format, params object[] parameters);
}
public class Log : ILog
{
public void Write(string format, params object[] parameters)
{
Console.WriteLine(format, parameters);
}
}
public interface IRandomProvider
{
Random RandomGenerator { get; }
}
public class RandomProvider : IRandomProvider
{
public RandomProvider()
{
RandomGenerator = new Random();
}
public Random RandomGenerator { get; }
}
}
IoC konténer projektek
.NET alatt számos IoC konténer implementáció létezik különböző keretrendszerek részeként, vagy önállóan. Ezért, ha nincsenek nagyon speciális igényeink, akkor a projektünkben érdemes egy bejáratott, több funkcióval rendelkező IoC konténert alkalmaznunk. A legnépszerűbb konténer projektek sorrendtől függetlenül:
-
Microsoft.Extensions.DependencyInjection – https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection
-
Castle.Windsor – https://github.com/castleproject/Windsor
-
Ninject – http://www.ninject.org/
-
Prism – http://prismlibrary.github.io
Ezen konténerek közös jellemzÅ‘je, hogy akár egy szerelvény összes tÃpusát regisztrálni tudják, illetve a konstruktor hÃvás és példányosÃtás különbözÅ‘ tÃpusok esetén stratégiákkal módosÃtható, konfigurálható.