IoC/DI, ramverk eller inte? (del 3 av 3)

I den här den sista delen i serien om IoC och DI, iallafall för nu, så diskuteras om man behöver något ramverk för att få till en bra hantering av komponenters och klassers beroenden mha DI och IoC.

Bakgrund

Det är egentligen frågan om ramverk eller inte ramverk och dom otaliga diskussionerna kring detta som ligger till grund för hela den här serien. Jag vågar upprepa påstående att dom flesta utvecklarna ute på marknaden har hamnat i någon sådan diskussion någon gång.

Diskussionerna där ute i utvecklarteamen handlar tyvärr alltför ofta om vilket ramverk som ska användas istället för om vi ska använda något ramverk alls. Det är den senare frågan som vi ska diskutera här. Frågan om vilket ramverk, när man väl har kommit fram till att man bör ha ett, handlar till stor del om tycke och smak och vad man är van vid, både vad utvecklarna själva är vana vid men även vad organisationen som ska förvalta och administrera systemet är van vid. Vilket ramverk man väljer är alltså mer eller mindre sekundärt, dom etablerade där ute innehåller ungefär samma funktioner och finesser och skulle dom inte ha just den finessen som eftersöks så kan dom flesta byggas ut med egna funktioner.

I den här posten diskuteras om man ska eller behöver använda ett färdigt ramverk för att följa IoC/DI-designfilosofin i ett system eller inte. Såklart finns det inget universellt svar till detta, men jag skulle vilja att fler där ute väljer bort färdiga ramverk, iallafall initialt under utvecklingen av ett system och istället fokuserar på att lösa problemen i domänen snarare än diverse strul som kan uppkomma när ett ramverk införs.

Diskussion

Innan argumenten börjar hagla så vill jag poängtera att den här posten är min egen personliga åsikt, baserad på erfarenheter, bra som dåliga, av IoC-ramverk. Det finns inget som är rätt eller fel, iallafall inte så länge man håller sig till filosofin bakom DI och IoC. Diskussionen här kommer iallafall försöka slå hål på argumenten att man måste plocka med ett ramverk i sitt system, helst från dag 1.

Man SKA ha ett ramverk, alla har ju det

Nej, eller ja, eller va? Det finns inget som säger att man SKA ha ett ramverk. Förvisso har dom flesta ett, men det är ju just det som fått mig att skriva den här serien poster.

Det blir ju så enkelt att byta ut t.ex databasen

Nej, det blir enkelt om man väljer att designa systemet enligt SOLID och därmed får med sig DI och IoC. Att se till att man anammar DI och IoC har inget alls med ett färdigt ramverk att göra. Och seriöst... Hur många gånger har man bytt ut databasen utan att byta ut eller bygga om resten av systemet?

Det går inte att utveckla med hjälp av TDD annars

Jo, det är återigen inte frågan om att ha ett ramverk eller inte. Det är fortfarande DI och IoC som gör det lättare att utveckla mha TDD, inte IoC-ramverket. Ramverket gör eventuellt det lättare att köra igång det slutliga färdiga systemet, men inte ens där behövs det något ramverk. Det kommer ett exempel nedan som visar på hur man enkelt kan anamma DI och IoC i ett vanligt ASP.NET MVC-projekt, helt utan ramverk.

Det är ju så smidigt att registrera sina interface och implementationer och sen funkar det bara

Det är just det här som får mig att fundera ett extra varv i frågan; är systemets implementation så pass komplex så att man inte kan hålla reda på klassers och komponenters beroenden till varandra? Visst är det smidigt att registrera sina interface tillsammans med implementationerna och låta ramverket lösa beroendena mellan dessa, men det är samtidigt lätt att man då invaggar sig själv i en falsk trygghet att systemets design är stringent och bra.

Jag skulle vilja utmana genom att ställa ytterligare en fråga: Hur många gånger har man flyttat kod till en egen klass, extraherat ett interface och registrerat dessa i IoC-ramverket register? Klassen har man sedan plockat in genom att injicera ett beroende och på så sätt tror man att man abstraherat och låtit systemet växa på ett sunt sätt.

Ovanstående "smarta drag" har jag själv gjort flera gånger. För stunden har det oftast löst problemet, men att komma tillbaka en liten tid senare och rätta ett fel eller bygga till ny funktion har varit riktigt svårt. Just det här förfarandet har inte med ramverken i sig att göra, men det underlättar eftersom klassen, aka kodmassan, inte känns lika synlig när man själv slipper injicera instansen i klassen där kodmassan tidigare låg. Det jobbiga med kodmassor är att dom ofta döljer diverse fel av olika slag, implementationsfel, fel i kravställningen, ibland kanske dom till och med döljer fel i systemets design. Det senare kan bli väldigt smärtsamt och tyvärr kan inget ramverk i världen hjälpa till nu.

Men annars får man ju inte in sina beroenden i t.ex. ASP.NET-sajten

Jo, hur gjorde man förut innan ramverken fanns? Man skrev väl new MyClass() någonstans, kanske till och med på flera ställen... Det här för oss in på det extremt banala exemplet som kommer nedan.

Exempelsystem

Jag tänkte nu visa ett väldigt enkelt exempel på hur en ASP.NET Web Application fortfarande kan ha DI och IoC och enhetstestas utan att plocka inte något ramverk för hanteringen av beroenden. Den kompletta koden finns här https://github.com/HeadlightAB/DependencyInjectionASPNET.

Man BEHÖVER verkligen inte ett ramverk

Om man är van vid att använda ett ramverk för att injicera instanser av diverse interfaceimplementationer så kan det kännas ovant i början att utelämna det. Det kan kännas spretigt när det gäller hanteringen av beroenden mellan klasser och komponenter att, som dom flesta IoC-ramverken erbjuder, inte ha en central plats att registrera implementationer på. Med hjälp av ramverkens hantering av dessa registreringar kan man enkelt byta ut en implementation av ett interface mot en annan. Jag vågar dock påstå att detta fallet inte så ofta uppträder, dvs hur många gånger byter man implementation och hur många gånger överskuggas detta jobb av jobbet att byta registreringen av denna implementation?

I exempelsystemet används ett lite annorlunda sätt att angripa problemet med beroendehantering. En klass eller komponents beroenden ligger helt och hållet i klassen själv, dvs så nära användningen av berodendet som man kan komma. Detta uppnås genom att man i dom parameterlösa konstruktorerna skapar instanser av klassens beroenden. Den parameterlösa konstruktorn passar skapandet vidare till kontruktorn som tar klassens beroenden som inparametrar. Denna konstruktorn är den som används i enhetstesterna och på så sätt kan man injicera enhetens, dvs klassens, beroenden där man har full kontroll på dessas beteenden. Se exempel på detta nedan:

public class HomeController : Controller  
{
    private readonly IVehicleService _service;

    public HomeController() : this(new VehicleService())
    {}

    public HomeController(IVehicleService service)
    {
        _service = service;
    }

    public ActionResult Index()
    {
        var vehicles = _service.GetAll();

        return View(vehicles);
    }
}

Här finns det ett par grejor som är värda att poängtera:

  • Den valda klassen ovan är en ASP.NET Controller-klass, vars instanser helt och hållet hanteras av ASP.NET-ramverket och indirekt webbservern. ASP.NET kräver en parameterlös konstruktor för att kunna instansiera controllers. Detta ligger helt i linje med metoden för IoC utan ramverk, som bygger på att varje klass på alla nivåer i systemet har dubbla konstruktorer (se punkten nedan). Dom flesta IoC-ramverken måste ta över hanteringen av skapandet av controller-instanser för att systemen ska fungera. Ibland kan detta övertagande försvåra felsökningen av ett system.
  • Klassen har dubbla konstruktorer, en parameterlös som kommer att anropas när systemet körs och en som tar klassens beroenden som inparametrar. Den parameterlösa konstruktorns kropp är helt korrekt tom. Klassens beroende, IVehicleService, skapas och passas vidare direkt till konstruktorn som tar beroendet som inparameter. Om man kroppen i den parameterlösa konstruktorn skulle behöva kod så är detta en varningsklocka att något kanske är fel. Den parameterlösa konstruktorn är själva ersättaren till ramverket, där beroendena till en klass skapas.
  • Skulle man en dag vilja eller behöva plocka in ett ramverk för beroendehantering och IoC så ska den parameterlösa konstruktorn kunna plockas bort, utan att någon kod ska gå om intet. Inga testfall ska då heller påverkas, då dessa använder sig av samma konstruktor som det kommande ramverket kommer att använda. Se enhetstestet nedan för klassen.
  • Konsturktionen med dubbla konstruktorer kommer alltså att vara genomgående återkommande på alla nivåer i systemet. Se klassen VehicleService som implementerar IVehicleService så återfinns exakt samma mönster, https://github.com/HeadlightAB/DependencyInjectionASPNET/blob/master/Business/VehicleService.cs, där beroendet till datalagret hanteras.

Nedan följer ett enhetstest för en metod i controller-klassen ovan:

[Fact]
public void ShouldInvokeServiceWhenIndex()  
{
    var service = Substitute.For<IVehicleService>();
    var sut = new HomeController(service);

    var _ = sut.Index();

    service.Received().GetAll();
}
  • Här ser man att enheten som ska testas, sut, en instans av HomeController skapas med hjälp av konstruktorn som tar beroendet som inparameter.
  • Testet påverkas inte alls även om den parameterlösa konstruktorn skulle plockas bort.
  • Systemet kommer att vara fullständigt regressionstestbart den dagen man skulle välja att inför ett ramverk för IoC, under förutsättningen att den parameterlösa konstruktorns kropp är helt tom vid införandet och enbart passar vidare kontrollen till konstruktorn med beroenden som inparametrar.

Finns det aldrig lägen där ramverk överglänser egenskriven kod?

Jag skulle vilja svara nej på den frågan, men kan inte helt och hållet stänga dörren för ett ramverk. Hur som helst så lär dom personerna som bygger ramverken vara ganska kompetenta och veta vad dom håller på med.

När det gäller hantering av instansers livslängd så kan det skapa huvydbry hos många. Det kan försvåra felsökning och felvhjälpning en hel del. I vissa fall skulle ett ramverk komma väl till pass, i dom fallen när annat än "vanlig hantering av livslängd" hos instaner önskas. Vad som är en "vanlig hantering" beror på vilken typ av system som avses, det är t.ex. olika livslängd på objekt i en webbapplikation och en fet klient installerad på en PC.

En viktig och ack så känslig del av ett system är hanteringen av uppkopplingar mot datalagret. Detta är kanske ett ämne i ett kommande inlägg här?

Det finns såklart andra lägen då ett ramverk skulle komma väl till pass. Exempelsystemet här i inlägget är extremt banalt och lite och kan därför kännas väldigt tillrättalagt. Större system med komplexare beroenden kan vara enklare att sköta om man låter ett ramverk hantera saken. Men då kommer man tillbaka till den initiala frågeställningen, som fått mig att skriva den här serien:

Är verkligen det kommande systemet så pass komplext så att man inte själv kan skriva koden som hanterar beroendena?

Jag vill fortfarande påstå att den frågan alldeles för ofta förbises och vips så har man plockat in ett ramverk.

Artiklar i serien