IoC/DI, vad är det? (del 1 av 3)

Många systemutvecklare runt omkring "där ute" har säkert hamnat i mer än en diskussion kring ramverk för IoC och/eller DI. Säkert har en del av dessa diskussioner påbörjats under antagandet att ett sådant ramverk ska finnas och det som debatteras är vilket som ska användas. Redan här är det i många fall läge att ta en paus och backa lite. Man borde antagligen börja diskussionen med att ställa frågan om man över huvud taget ska plocka in ett sådant ramverk. Det var under en av dom här diskussionerna som idén för den här serien poster föddes. Något måste göras, låt oss lyfta på locket på den kokande kastrullen och försöka förstå vad det egentligen handlar om!

Den här serien kommer att beskriva IoC och DI kortfattat, varför man vill ha stöd för DI och åstadkomma IoC och slutligen kommer den mer argumenterande delen för eller emot ramverk som stöd för IoC. Om man förstår DI och IoC så kommer det bli enklare att ta till sig diskussionen och även delta aktivt i diskussioner i sitt team när det handlar om system- och koddesign. DI och IoC är tämligen centrala begrepp att ha med sig in i ett system för att få en stabil grund att stå på, framför allt stabilt då den dagen kommer när man hittar den första kritiska buggen som måste rättas snabbt och säkert.

I den här första delen beskrivs begreppen DI och IoC, den andra delen argumenterar kort varför man med fördel ska använda DI och IoC och i den tredje och avslutande delen diskuteras om behöver använda ett ramverk för att hantera dessa båda programmeringstekniska designfilosofier.

Vad är IoC och DI?

Det här är ingen formell definition av dom båda akronymen utan mer en beskrivning i brödtext med tillhörande kodexempel vad dom innebär. Vi börjar med DI, Dependency Injection.

Dependency Injection (DI)

Direkt översatt med Google Translate betyder Dependency Injection - beroendeinjektion. Jag tycker det är en bra översättning, men kanske kräver den en kort förklaring. Antag att man har en klass med ett antal beroenden, dvs klassen är beroende av externa resurser, tjänster, funktioner. I exemplet nedan syns ett enkelt beroende, där klassen TheDependentService är beroende av StorageService:

public class TheDependentService  
{
    private readonly StorageService _storageService;

    private object _theData;

    public TheDependentService()
    {
        _storageService = new StorageService();
    }

    public void Save()
    {
        _storageService.PersistIt(_theData);
    }
}

Vi antar att StorageService har en metod implementerad, PersistIt.

I exemplet ovan sker dock ingen injektion av något beroende eftersom man i konstruktorn säger hårt _storageService = new StorageService();.

För att kunna injicera beroendet måste man "utifrån" kunna servera TheDependentService med en instans av StorageService, samtidigt som vi bryter det hårda beroendet till klassen StorageService genom att vi introducerar ett interface som endast deklarerar tjänstens metoder. Interfacet implementeras av någon klass, helt oviktigt av vilken just nu. Vi justerar koden litegrann, inför interfacet och ser till att konstruktorn i TheDependentService kan ta emot en instans av detsamma:

public interface IStorageService  
{
    void PersistIt(object theData);
}

public class TheDependentService  
{
    private readonly IStorageService _storageService;

    ...

    public TheDependentService(IStorageService storageService)
    {
        _storageService = storageService;
    }

    ...
}

Det här möjliggör att man, på det ställe man vill använda TheDependentService, kan skicka in, injicera, en instans av IStorageService:

public static void Main()  
{
    IStorageService storageService = new StorageService();
    var dependentService = new TheDependentService(storageService);

    ...

    dependentService.Save();
}

Nu har vi alltså uppnått beroendeinjektion, Dependency Injection. Ett annat sätt att formulera det vi har uppnått är att TheDependentService inte är "hårt beroende av sina beroenden", men då är vi på väg in i nästa begrepp att förklara nämligen Inversion of Control IoC. I vilket fall som helst så har vi med hjälp av DI luckrat upp TheDependentService beroende till StorageService en smula genom att injicera en instans av en implementation av ett interface istället. Vi har gjort TheDependentService oberoende av just implementationen StorageService.

Inversion of Control (IoC)

Om man ordagrant skulle översätta Inversion of Control skulle man få ungefär omkastning av kontroll. Det här låter kryptiskt men blir förhoppningsvis tydligare efter följande förklaring.

Vi använder oss av samma exempel som vi använde för att förklara DI:

public interface IStorageService  
{
    void PersistIt(object theData);
}

public class StorageService : IStorageService  
{
    public void PersistIt(object theData)
    {
        ...
    }
}

public class TheDependentService  
{
    private readonly IStorageService _storageService;

    private object _theData;

    public TheDependentService(IStorageService storageService)
    {
        _storageService = storageService;
    }

    public void Save()
    {
        _storageService.PersistIt(_theData);
    }
}

public class Client  
{
    public static void Main()
    {
        IStorageService storageService = new StorageService();
        var dependentService = new TheDependentService(storageService);
        ...
        dependentService.Save();
    }
}

Överst ser vi interfacet och en implementation av detsamma, som sedan används i klassen TheDependentService och i klassen Client längst ner.

Det intressanta att titta på nu är klassen Client, den klassen som använder sig av TheDependentService i metoden Main. Här ser vi att man skickar in en instans av IStorageService, som är just implementationen av interfacet som återfinns i klassen StorageService, till konstruktorn för TheDependetService. TheDependentService vet inget om den här implementationen och det är här som själva omkastningen av kontrollen syns. TheDependentService tar emot sitt beroende utifrån och den specifika implementationen av beroendet bestäms alltså av användaren, dvs klassen Client.

DI ≈ IoC ?

Borde man inte kunna säga att DI är ungefär samma sak som IoC? Nja, inte riktigt. För att uppnå IoC så måste man på något sätt ha DI, dvs IoC => DI och DI behövs för IoC, men DI ≠ IoC.

Den formen av DI som beskrivits här kallas för constructor injection. Det finns också en annan typ, inte lika vanlig, som kallas för setter injection. Den senare bygger på att man har publika set-metoder för en klass beroenden, som alltså kan tilldelas utifrån. Personligen gillar jag inte publika setters utan håller gärna mina klasser så privata som möjligt, helst också med beroendena som readonly och därmed endast möjliga att tilldela i samband med deklarationen eller i konstruktorn. Detta implicerar då constructor injection.

I nästa del i serien tar vi en titt på varför DI och IoC används.

Artiklar i serien