#Utveckling & Arkitektur

Inversion of Control hjälper dig att uppfylla SOLID-principerna, Del 3

Här kommer tredje och sista delen i vår serie om hur Inversion of Control hjälper dig att uppfylla SOLID-principerna. Har du inte läst de två tidigare avsnitten kan det vara bra att först börja med del 1 och sedan del 2

IoC-Containers 

IoC och DI har som sagt  ett par komplicerande faktorer när det kommer till själva implementationen. Eftersom klasserna i en objektorienterat byggd applikation kan anropa varandra under förhållandevis fria förutsättningar, så blir själva anropandet lätt otympligt. Betänk till exempel följande anropsträd:   


IMainApplication anropar IPackingRunner, som använder sig av IBagFiller och IBoxPacker, där IBagFiller behöver tillgång till IBagProvider och där IBoxPacker behöver tillgång till IBoxBuilder och (i vissa fall) IBoxLidCloser. Anropet på IPackingRunner från IMainApplication skulle då behöva injicera implementationer av alla dessa olika interface, eftersom det är IMainApplication som har styrningen över dem. Detta skulle alltså innebära att konstruktorn för IPackingRunner skulle se ut såhär: 

new IPackingRunner(IBagFiller bagFiller, IBagProvider bagProvider, IBoxPacker boxPacker, IBoxBuilder boxBuilder, IBoxLidCloser boxLidCloser) – Som vi ser en allt annat än lätthanterlig struktur. 

 

robot_chase.png

 

Att komma runt problematiken ovan är dock enkelt, genom att man överlåter arbetet till en IoC-container. I grunden är IoC-containern ett uppslagsverk över vilka klasser som implementerar respektive interface, vilka klasser som behöver tillgång till varandra, när en viss implementation av flera tillgängliga ska väljas, hur dessa implementationer ska initialiseras och så vidare. I förstone känns detta komplext, men det fina är att IoC-containern behåller denna komplexitet inom sig; utanpå är användandet gjort för att vara så enkelt som möjligt. 

Grundläggande IoC-container 

Att konstruera en IoC-container är i det grundläggande fallet väldigt enkelt. Studera DemoContainer nedan: 

public class DemoContainer 

{ 

    public delegate object Creator(DemoContainer container); 

    private readonly Dictionary<Type, Creator> typeToCreator = new Dictionary<Type, Creator>(); 

    public void Register<T>(Creator creator) 

    { 

        typeToCreator.Add(typeof(T), creator); 

    } 

    public T Resolve<T>() 

    { 

        return (T)typeToCreator[typeof(T)](this); 

    } 

} 

 

Denna klass är en trivial form av IoC-container. Den innehåller en enkel uppslagslista över interface och implementationer, samt de två essentiella funktionerna för en sådan: En Register-metod för att registrera ett interface-implementation-par, och en Resolve-metod för att slå upp en implementation givet ett interface.  

Att använda containern är förhållandevis rättframt; man använder dess Register-metod för att konfigurera korrekt implementation av respektive interface eller konstruktoranrop: 

var container = new DemoContainer(); 

container.Register<IBagFiller>(delegate { return new BagFiller(); }); 

container.Register<IBoxPacker>(delegate { return new BoxPacker(); }); 

container.Register<PackingRunnerUsingIoC>(delegate 

{ 

       return new PackingRunnerUsingIoC(container.Resolve<IBagFiller>(), container.Resolve<IBoxPacker>()); 

}); 

 

Sedan konsumerar man dess resolve-metod för att få sina klasser: 

var packingRunner = container.Resolve<PackingEngine.PackingRunnerUsingIoC>(); 

var box = PackingRunner.FillBagInBox(); 

De här två raderna kan förekomma vart som helst i applikationen, med det enda kravet att containern är konfigurerad och tillgänglig. Det finns dock uppenbara fördelar med att ta fram den en enda gång, i starten av applikationen eller systemet, och sedan låta den fylla på med allt annat vartefter det behövs.

Notera även att vi, när vi initialiserar packingRunner-objektet genom att ropa på Resolve-funktionen, inte behöver förhålla oss alls till vilka parametrar konstruktorn behöver – allt lades på plats när vi först konfigurerade containern. När vi sedan behöver använda packingRunner, så behöver vi bara se till att den konfigurerade containern finns på plats och be denna plocka fram vår instans av PackingRunnerUsingIoC, så fyller den ”auto-magiskt” på med övriga implementationer. 

Kommersiella IoC-containers 

Naturligtvis finns det många fall där denna triviala containerklass inte räcker till, och det finns kommersiellt byggda IoC-containers som är mycket mer utvecklade och genomarbetade. Här nedan ges motsvarande konfigurationskod för IoC-containern Castle Windsor: 

 

var container = new WindsorContainer(); 

container.Register(Component.For<PackingRunnerUsingIoC>()); 

container.Register(Component.For<IBagFiller>().ImplementedBy<BagFiller>()); 

container.Register(Component.For<IBoxPacker>().ImplementedBy<BoxPacker>()); 

 

Motsvarande rader för en annan IoC-container, Unity, är snarlika: 

 

var container = new UnityContainer(); 

container.RegisterType<IBagFiller, BagFiller>(); 

container.RegisterType<IBoxPacker, BoxPacker>(); 

container.RegisterInstance<PackingRunnerUsingIoC>(new PackingRunnerUsingIoC(container.Resolve<IBagFiller>(), container.Resolve<IBoxPacker>())); 

 

I dessa exempel är till exempel notationen mer utstuderad och därmed lättare att läsa, även om den upplevda läsbarheten såklart till viss del beror på den enskilde utvecklarens preferenser. 

Kommersiella IoC-containers har även många andra fördelar, bland annat funktioner för att: 

  • registrera och hantera konfigurationsvärden
  • skapa och hantera endpoints för ASP.NET, WCF och andra flertrådade server-ramverk
  • hantera property- och metodinjektion
  • livscykelhantering för instanser (Singleton, Transient, Scoped mfl.)

 

Sammanfattning 

Med hjälp av inversion of control och dependency injection kan man med förhållandevis små medel lösa upp kopplingarna mellan olika komponenter i en objektorienterad applikation. Detta gör det busenkelt att ersätta en lådpackares lådor med testlådor eller att byta en packarklo mot en annan.

Införandet av en IoC-container tillåter en enhetlig åtkomst till varje komponent, styrning av vilka komponenter som används med mera. Precis som en scouts ryggsäck finns den där, alltid redo med fickorna fulla av lådpackare av rätt sort.

Detta ger utökade möjligheter att skapa system som är överskådliga, testbara, lätta att underhålla och har en ökad potentiell livslängd. Systemen får ett större värde och kan lättare byggas så att de verkligen kan göra det som de är bäst på.

Om du inte redan läst de andra bloggposterna i samma serie, hittar du del 1 här och del 2 här.

Hoppas det har varit intressant läsning, om det någon som blir nyfiken på hur det är att jobba på Agero får ni gärna läsa vidare under jobba hos oss fliken

 



 

Publicerad: 2016-03-24