Az Inversion of Control (röviden IoC) alapelv egyfajta tervezési minta. Magyarul szokás emlegetni "kontrol megfordÃtása mintának". Sok esetben a Dependency Injection mintával jár együtt, amit szokás nevezni "függÅ‘ség injektálásnak". Mindkét minta célja a modularitás növelése azáltal, hogy a kódunkból az osztályok létrehozását, paraméterezését kiemeljük és egy külsÅ‘, erre a célra Ãrt komponensre bÃzzuk.
Nézzünk egy példát. Tételezzük fel, hogy van egy osztályunk, ami belül példányosÃt még egy osztályt:
class Pelda
{
private Pelda2 _belsoOsztaly;
public Pelda()
{
_belsoOsztaly = new Pelda2();
}
}
A fenti kódrészlettel az lesz a probléma, hogy a két osztály között direkt függÅ‘séget képez, ami a kód tesztelhetÅ‘ségét nehezÃti. Ha tesztelni akarjuk a Pelda osztályt, akkor Pelda2 osztályt is teszteljük indirekt módon, Ãgy önmagáról a Pelda osztály helyes működésérÅ‘l nem tudunk meggyÅ‘zÅ‘dni, csak feltételezhetjük, hogy minden esetben jól fog működni.
Itt jön képbe a Dependency Injection. Pelda2 osztály példányosÃtását kiemeljük és a konstruktorhoz külsÅ‘ függÅ‘ségként hozzáadjuk.
class Pelda
{
private Pelda2 _belsoOsztaly;
public Pelda(Pelda2 fuggoseg)
{
_belsoOsztaly = fuggoseg;
}
}
Ez egy fokkal jobb megoldás, de még mindig direkt függÅ‘ségünk van a Pelda2 osztályra. Éppen ezért a függÅ‘ségeket interfészekre szokás cserélni. Az interfész leÃrja az osztály viselkedését, de az implementációját nem határozza meg. Ezáltal a kód újrahasználhatósága is növekszik.
class Pelda
{
private IPelda2 _belsoFelulet;
public Pelda(IPelda2 fuggoseg)
{
_belsoFelulet = fuggoseg;
}
}
Az IoC és a Dependency Injection tipikus megvalósÃtási módja az, hogy használunk egy IoC konténert. Az IoC konténerben bekonfiguráljuk, hogy melyik interfész megvalósÃtásért melyik osztályt szeretnénk példányosÃttatni. Ezek után a konténerbÅ‘l kérjük el az interfészekhez tartozó példányokat. Ekkor a konténer megnézi, hogy az adott interfészhez melyik tÃpust kell visszaadni. A kért tÃpus és a konstruktor paramétereinek tÃpusa alapján már létre tudja hozni az interfésznek megfelelÅ‘ objektumot.
A .NET beépÃtetten nem tartalmaz IoC konténert, de számos .NET könyvtár létezik a célra. Azonban IoC konténer nélkül is tudunk IoC-t készÃteni reflection segÃtségével.1
Az alábbi kódrészlet egy egyszerű moduláris parancsértelmezÅ‘t mutat be. A modulok alapja az IModule interfész. Ez két taggal rendelkezik: Egy string tÃpusú tulajdonsággal, ami a parancs nevét tárolja és egy Run metódussal, ami a modul kódját futtatja.
namespace PeldaReflection2
{
interface IModule
{
string Name { get; }
void Run();
}
}
Ezt az interfészt két egyszerű osztály is megvalósÃtja.
using System;
namespace PeldaReflection2
{
public class ModuleHello : IModule
{
public string Name => "Hello";
public void Run()
{
Console.WriteLine("Hello");
}
}
public class ModuleCls : IModule
{
public string Name => "Clear";
public void Run()
{
Console.Clear();
}
}
}
Az érdekesség a fÅ‘ programban található. Ez elÅ‘ször az IModule tÃpusa alapján beazonosÃtja azt a szerelvényt (dll vagy exe fájl), ami tartalmazza a tÃpust. Majd a szerelvénybÅ‘l lekérdezi az összes olyan tÃpust, ami létrehozható az IModule tÃpusból. Itt érdemes megjegyezni, hogy a reflection szempontjából az interfészek is létrehozhatóak a szűrésnél használt interfészbÅ‘l, ezért érdemes tovább szűkÃteni a kört osztályokra és azokon belül is nem absztrakt osztályokra.
Ezt követÅ‘en már csak a példányok létrehozására van szükség. PéldányosÃtani az Activator osztály CreateInstance metódusával tudunk, ami jelen esetben az alapértelmezett, paraméter nélküli konstruktort hÃvja. De természetesen extra logikával fel tudnánk vértezni és függÅ‘ségeket is tudnánk injektálni az osztályunkba.
A fő logikája a programnak egy ciklusból áll, ami a tárolt példányok közül kikeresi a parancshoz tartozó példányt és végrehajtja a hozzárendelt kódot. A kilépés parancsát "beégetetten" definiálja a program.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace PeldaReflection2
{
class Program
{
static void Main(string[] args)
{
//jelenlegi szerelvény lekérése
Assembly current = typeof(IModule).Assembly;
//Minden IModule interfészt megvalósÃtó osztály-tÃpus lekérése,
//ami osztály
var modules = from type in current.GetTypes()
where
typeof(IModule).IsAssignableFrom(type)
&& type.IsClass
&& !type.IsAbstract
select
type;
//Lista a példányok tárolására
List<IModule> instances = new List<IModule>();
foreach (var module in modules)
{
//PéldányosÃtás paraméter nélküli konstruktorral
IModule instance = Activator.CreateInstance(module) as IModule;
if (instance != null)
{
//Példány tárolása
instances.Add(instance);
}
}
Console.WriteLine("Parancsok: {0}", instances.Count);
//Példányok közül megadott névvel rendelkezÅ‘ keresése, majd a Run metódusának hÃvása
do
{
Console.Write("Parancs: ");
var cmd = Console.ReadLine();
var selected = instances.Where(i => i.Name == cmd).FirstOrDefault();
if (selected != null)
{
selected.Run();
}
else if (cmd == "Exit")
{
break;
}
else
{
Console.WriteLine("Nem található: {0}", cmd);
}
}
while (true);
}
}
}
A program egy lehetséges kimenete:
Parncsok: 2
Command: Hello
Hello
Command: Exit
Függőség injektálást egyébként tudunk tulajdonságokon keresztül is végezni. Ez manuális injektálásnál vagy IoC konténer használatának mellőzésekor hasznos, de egy veszélyt rejt magában. A konstruktor injektálással biztosak lehetünk benne, hogy megkaptuk a függőségeket, mert máskülönben az osztály példánya létre se jönne, helyette valahol kivétel keletkezne.
Azonban, ha tulajdonságokon keresztül adunk át függÅ‘séget, akkor nem lehetünk benne biztosak, hogy a függÅ‘séget valóban megkaptuk. Ezért az ilyen tÃpusú függÅ‘ség injektálásnál ügyeljünk a null ellenÅ‘rzésre minden használat elÅ‘tt, mert a függÅ‘ségünk Ãgy külsÅ‘ beavatkozás következtében is null értéket vehet fel menet közben.
-
IoC konténer reflection nélkül is megvalósÃtható. Ebben az esetben azonban egy központi helyen kell konfigurálnunk a függÅ‘ségek és a függÅ‘ségeket felhasználó osztályok létrehozását. A modern és nagy tudású IoC könyvtárakban a reflection-t automatikus konfigurációra és a konténertÅ‘l kért tÃpusok példányosÃtására használják.↩