Gå til indholdet

Afkobling (DI)

Afkobling, også kendt som Dependency Injection (DI), er en teknik, der bruges til at reducere afhængigheder mellem objekter i objektorienteret programmering. Det gør det lettere at vedligeholde og teste kode.

Hvad er afkobling?

Afkobling er en metode til at reducere direkte afhængigheder mellem objekter i en applikation. Ved at gøre dette bliver applikationen mere fleksibel, lettere at vedligeholde og nemmere at teste. Afkobling opnås ved at introducere et abstraktionslag mellem objekter, så de ikke behøver at kende til de konkrete implementeringer af deres afhængigheder.

Der er mange fordele ved afkobling:

  • Det gør det nemmere at ændre og udvide kode, da ændringer i en klasse ikke nødvendigvis påvirker andre klasser
  • Det forbedrer testbarheden, da det er nemmere at erstatte en afhængighed med en mock eller stub under test
  • Det fremmer genbrug af kode og gør det lettere at dele funktioner mellem forskellige dele af en applikation

Eksempel

Prøv at se følgende klasse:

public class Terning 
{
    public int Værdi { get; private set; }
    private Random rnd;
    public void Ryst()
    {
        this.Værdi = rnd.Next(1, 7);
    }
    public Terning()
    {
        this.rnd = new Random();
        this.Ryst();
    }
}

Det er jo en helt almindelig terning, men rent arkitektonisk er klassen hårdt bundet til System.Random, som bruges til at skabe tilfældige tal. Det kan give problemer i senere versioner, hvis du (eller brugerne af klassen) ønsker at skifte tilfældighedsgenerator, fordi det vil kræve en rekompilering af koden. Men hvis du nu går et skridt op i abstraktionsniveau og benytter et interface, kan du løse problemet. Her er et interface med en enkelt metode:

public interface ITilfældighedsGenerator {
    int FindTilfældigtTal();
}

Hvis klassen terning tilrettes således, at det ikke er System.Random, der kodes mod, men i stedet ITilfældighedsGenerator ser det således ud:

public class Terning 
{
    public int Værdi { get; private set; }
    private ITilfældighedsGenerator rnd;
    public void Ryst()
    {
        this.Værdi = rnd.FindTilfældigtTal(); ;
    }
    public Terning(ITilfældighedsGenerator generator)
    {
        this.rnd = generator;
        this.Ryst();
    }
}

Nu er System.Random skiftet ud med ITilfældighedsGenerator, og i konstruktøren skal en instans, der implementerer dette interface, medsendes. Det kræver jo en klasse eller struktur, som implementerer dette interface – og der er helt frit valg i implementationen. Det eneste kompileren sikrer er, at metoden FindTilfældigtTal skal være til stede.

Her er et par forslag:

class TilfældighedsGeneratorRandom : ITilfældighedsGenerator
{
    public int FindTilfældigtTal()
    {
        return new System.Random().Next(1, 7);
    }
}

class TilfældighedsGeneratorSnyd : ITilfældighedsGenerator
{
    public int FindTilfældigtTal()
    {
        return 6;
    }
}

class TilfældighedsGeneratorRandomOrg : ITilfældighedsGenerator
{
    public int FindTilfældigtTal()
    {
        string url = "https://www.random.org/integers/" +
            "?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new";
        using (System.Net.WebClient w = new System.Net.WebClient())
        {
            string s = w.DownloadString(url);
            return Convert.ToInt32(s);
        }
    }
}

Den ene klasse benytter System.Random, en anden returnerer blot 6, og den sidste henter et tilfældigt tal fra random.org – så implemente-ringen er forskellig, men interface er det samme. Og det kan så ud-nyttes, når der skabes en instans af Terning:

Terning t1 = new Terning(new TilfældighedsGeneratorRandom());
Terning t2 = new Terning(new TilfældighedsGeneratorSnyd());
Terning t3 = new Terning(new TilfældighedsGeneratorRandomOrg());

Det er nu brugeren af Terning, som bestemmer hvilken tilfældigheds-generator, der skal bruges, og brugeren kan sågar skabe og benytte sin egen klasse, så længe den implementerer det nævnte interface.

I arkitektur kalder man mønsteret i dette eksempel for constructor based dependency injection, fordi brugeren tilføjer Terning den ønskede generator i konstruktøren. Det er den simpleste form for DI (også kaldet fattigmands DI), men beskriver meget godt, hvor effektivt brug af et interface kan være.

ServiceCollection og ServiceProvider

I .NET Core og .NET 5/6/7/8 har Microsoft indført en standard måde at implementere DI på gennem ServiceCollection og ServiceProvider klasserne.

  • ServiceCollection er en samling, der bruges til at registrere services. En service er typisk et interface eller en klasse. Når du registrerer en service, fortæller du DI-containeren, hvordan den skal oprette instanser af denne service.

  • ServiceProvider er en DI-container, der skaber og leverer services. Når du har registreret dine services med en ServiceCollection, kan du oprette en ServiceProvider for at få adgang til disse services.

Ideen er helt konkret, at du registrerer dine services i ServiceCollection og derefter opretter en ServiceProvider for at få adgang til disse services. Når du anmoder om en service fra ServiceProvider, vil den oprette en instans af denne service og returnere den til dig. Hvis du ønsker at en klasse skal bruge en service, skal du blot anmode om den i konstruktøren, og DI-containeren vil sørge for at levere den til dig. Dog skal du sørge for at registrere klassen i ServiceCollection først.

Registreringstyper

Der er forskellige måder at registrere services på, som bestemmer service-livscyklussen:

  • Singleton: Når en service registreres som Singleton, vil der kun blive skabt én instans af servicen, som genbruges på tværs af hele applikationen.

  • Scoped: Scoped services skabes en gang for hver request. Dette er særligt brugbart i webapplikationer, hvor du f.eks. ønsker en ny databaseforbindelse for hver HTTP-request.

  • Transient: Transiente services skabes hver gang, de anmodes om. Dette sikrer, at du får en ny instans hver gang en service anmodes.

Helt simpel C# Console med MS DI

For at anvende DI i en C# 8 console applikation, skal du først tilføje en reference til Microsoft.Extensions.DependencyInjection. Dette kan gøres gennem NuGet Package Manager.

Herefter kan et simpelt eksempel på anvendelse af DI i en console app se sådan ud:

using Microsoft.Extensions.DependencyInjection;

// Definér et interface til en service
public interface IGreetingService
{
    void Greet(string name);
}

// Implementér servicen
public class GreetingService : IGreetingService
{
    public void Greet(string name)
    {
        Console.WriteLine($"Hello, {name}!");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Opret en ServiceCollection
        var services = new ServiceCollection();

        // Registrer dine services
        services.AddSingleton<IGreetingService, GreetingService>();
        // Klassen Test og Test2 skal også registreres som en service for at få IGreetingService injiceret
        services.AddSingleton<Test>();
        services.AddSingleton<Test2>();

        // Byg ServiceProvider
        var serviceProvider = services.BuildServiceProvider();

        // Brug ServiceProvider til at få IGreetingService
        var greetingService = serviceProvider.GetService<IGreetingService>()!;
        greetingService.Greet("World");

        // Klassen Test kan selv få servicen injiceret
        Test test = serviceProvider.GetService<Test>()!;
        test.Print("World");

        // Klassen Test2 kan selv få servicen injiceret
        Test2 test2 = serviceProvider.GetService<Test2>()!;
        test2.Print("World");
    }
}

class Test {

    private readonly IGreetingService greetingService;

    public Test(IGreetingService greetingService)
    {
        this.greetingService = greetingService;
    }

    public void Print(string name)
    {
        greetingService.Greet(name);
    }
}

// Faktisk nemmest med en primary constructor (det er derfor den er tilføjet i C# 9)
class Test2(IGreetingService greetingService)
{
    public void Print(string name)
    {
        greetingService.Greet(name);
    }
}

I dette eksempel registreres GreetingService som en Singleton service i ServiceCollection. Når ServiceProvider oprettes, kan vi anmode om en instans af IGreetingService, og GreetingService vil blive returneret. Da det er registreret som Singleton, vil hver anmodning returnere den samme instans. Bemærk, at klassen Test og Test2 også skal registreres som en service for at få IGreetingService injiceret.

Terning eksempel med MS DI

Her er samme Terning eksempel som tidligere - nu bare med brug af ServiceCollection og ServiceProvider:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddSingleton<Terning>();

// hvis et eller andet er sandt så:
services.AddSingleton<ITilfældighedsGenerator, TilfældighedsGeneratorRandom>();

// ellers
// services.AddSingleton<ITilfældighedsGenerator, TilfældighedsGeneratorSnyd>();

// ellers
// services.AddSingleton<ITilfældighedsGenerator, TilfældighedsGeneratorRandomOrg>();

var serviceProvider = services.BuildServiceProvider();

Terning? t = serviceProvider.GetService<Terning>();
for (int i = 0; i < 10; i++)
{
    Console.WriteLine(t?.Værdi);
    t?.Ryst();
}

class TilfældighedsGeneratorRandom : ITilfældighedsGenerator
{
    public int FindTilfældigtTal()
    {
        return new System.Random().Next(1, 7);
    }
}

class TilfældighedsGeneratorSnyd : ITilfældighedsGenerator
{
    public int FindTilfældigtTal()
    {
        return 6;
    }
}

class TilfældighedsGeneratorRandomOrg : ITilfældighedsGenerator
{
    public int FindTilfældigtTal()
    {
        string url = "https://www.random.org/integers/" +
            "?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new";
        using (System.Net.WebClient w = new System.Net.WebClient())
        {
            string s = w.DownloadString(url);
            return Convert.ToInt32(s);
        }
    }
}

public class Terning
{
    public int Værdi { get; private set; }
    private ITilfældighedsGenerator rnd;
    public void Ryst()
    {
        this.Værdi = rnd.FindTilfældigtTal(); ;
    }
    public Terning(ITilfældighedsGenerator generator)
    {
        this.rnd = generator;
        this.Ryst();
    }
}

public interface ITilfældighedsGenerator
{
    int FindTilfældigtTal();
}

De mest populære DI NuGet-pakker

Der er flere Dependency Injection (DI) NuGet-pakker tilgængelige, som kan hjælpe med at implementere afkobling i C# projekter. Her er en kort beskrivelse af de 4 mest populære:

Info

Hvis du har lyst kan du finde ovennævnte Terning eksempel implementeret i flere at de populære DI biblioteker - se devcronberg/DiceDI

Microsoft.Extensions.DependencyInjection

Dette er den officielle DI-pakke fra Microsoft og er en del af ASP.NET Core. Den er letvægts og nem at bruge og er velegnet til både små og store projekter. Denne pakke kan også bruges uden for ASP.NET Core-projekter og er et godt valg for udviklere, der ønsker at arbejde med en standard DI-container. - Microsoft.Extensions.DependencyInjection på NuGet.org

Autofac

Autofac er en kraftfuld og fleksibel DI-container, der tilbyder avancerede funktioner som modulopbygning, property injection og mere. Den er velegnet til komplekse projekter, hvor der er behov for avanceret konfiguration og kontrol over afhængighedsinjektionen. - Autofac på NuGet.org

Ninject

Ninject er en letvægts og nem at bruge DI-container, der understreger konvention over konfiguration. Den tilbyder en simpel og ren kodebase og er velegnet til små til mellemstore projekter, hvor udviklere ønsker at komme i gang hurtigt og nemt. - Ninject på NuGet.org

Unity

Unity er en DI-container udviklet af Microsoft Patterns & Practices team. Den er designet til at være let at bruge og konfigurere og tilbyder funktioner som interception og policy injection. Unity er velegnet til både små og store projekter og er især populær blandt udviklere, der arbejder med Microsoft-relaterede teknologier. - Unity på NuGet.org

Valget af en DI-container afhænger af projektets krav og udviklerens præferencer. Det er vigtigt at vælge en container, der understøtter de ønskede funktioner og passer godt ind i projektets arkitektur og kodebase.