Hur man använder inversion av kontroll i C #

Både inversion av kontroll och injektionsberoende gör att du kan bryta beroenden mellan komponenterna i din applikation och göra din applikation lättare att testa och underhålla. Inversion av kontroll och beroendeberoende är dock inte detsamma - det finns subtila skillnader mellan de två.

I den här artikeln kommer vi att undersöka inversionen av kontrollmönster och förstå hur det skiljer sig från beroendeinjektion med relevanta kodexempel i C #.

För att arbeta med kodexemplen i den här artikeln bör du ha Visual Studio 2019 installerat i ditt system. Om du inte redan har en kopia kan du ladda ner Visual Studio 2019 här. 

Skapa ett konsolapplikationsprojekt i Visual Studio

Först och främst, låt oss skapa ett .NET Core-konsolapplikationsprojekt i Visual Studio. Förutsatt att Visual Studio 2019 är installerat i ditt system, följ stegen nedan för att skapa ett nytt .NET Core-konsolapplikationsprojekt i Visual Studio.

  1. Starta Visual Studio IDE.
  2. Klicka på "Skapa nytt projekt."
  3. I fönstret "Skapa nytt projekt" väljer du "Konsolapp (.NET Core)" från listan över mallar som visas.
  4. Klicka på Nästa. 
  5. I fönstret "Konfigurera ditt nya projekt" som visas nedan anger du namn och plats för det nya projektet.
  6. Klicka på Skapa. 

Detta skapar ett nytt .NET Core-konsolapplikationsprojekt i Visual Studio 2019. Vi använder detta projekt för att utforska inversion av kontroll i de efterföljande avsnitten i den här artikeln.

Vad är inversion av kontroll?

Inversion of control (IoC) är ett designmönster där ett programs styrflöde är inverterat. Du kan dra nytta av inversionen av kontrollmönstret för att koppla bort komponenterna i din applikation, byta beroendeimplementeringar, håna beroenden och göra din applikation modulär och testbar.

Beroendeinjektion är en delmängd av principen för inversion av kontroll. Med andra ord är beroendeinjektion bara ett sätt att genomföra inversion av kontroll. Du kan också implementera inversion av kontroll med exempelvis händelser, delegater, mallmönster, fabriksmetod eller servicelokaliserare.

Inversionen av kontrolldesignmönster säger att objekt inte ska skapa objekt som de är beroende av för att utföra någon aktivitet. Istället bör de hämta dessa föremål från en extern tjänst eller en container. Idén är analog med Hollywood-principen som säger "Ring oss inte, vi ringer till dig." Som ett exempel, istället för att applikationen anropar metoderna i ett ramverk, skulle ramverket kalla implementeringen som har tillhandahållits av applikationen. 

Inversion av kontrollexempel i C #

Antag att du bygger en orderhanteringsapplikation och att du vill implementera loggning. För enkelhetens skull antar vi att loggmålet är en textfil. Välj det konsolapplikationsprojekt du just skapade i fönstret Solution Explorer och skapa två filer, med namnet ProductService.cs och FileLogger.cs.

    offentlig klass ProductService

    {

        privat readonly FileLogger _fileLogger = ny FileLogger ();

        public void Log (strängmeddelande)

        {

            _fileLogger.Log (meddelande);

        }

    }

    offentlig klass FileLogger

    {

        public void Log (strängmeddelande)

        {

            Console.WriteLine ("Inside Log-metoden för FileLogger.");

            LogToFile (meddelande);

        }

        privat ogiltigt LogToFile (strängmeddelande)

        {

            Console.WriteLine ("Metod: LogToFile, Text: {0}", meddelande);

        }

    }

Implementeringen som visas i föregående kodavsnitt är korrekt men det finns en begränsning. Du är begränsad till att bara logga data till en textfil. Du kan inte på något sätt logga data till andra datakällor eller olika loggmål.

En oflexibel implementering av loggning

Vad händer om du vill logga data till en databastabell? Det befintliga genomförandet stöder inte detta och du skulle tvingas ändra implementeringen. Du kan ändra implementeringen av FileLogger-klassen eller skapa en ny klass, till exempel, DatabaseLogger.

    public class DatabaseLogger

    {

        public void Log (strängmeddelande)

        {

            Console.WriteLine ("Inside Log-metoden för DatabaseLogger.");

            LogToDatabase (meddelande);

        }

        privat ogiltigt LogToDatabase (strängmeddelande)

        {

            Console.WriteLine ("Metod: LogToDatabase, Text: {0}", meddelande);

        }

    }

Du kan till och med skapa en instans av klassen DatabaseLogger i ProductService-klassen som visas i kodavsnittet nedan.

offentlig klass ProductService

    {

        privat readonly FileLogger _fileLogger = ny FileLogger ();

        privat readonly DatabaseLogger _databaseLogger =

         ny DatabaseLogger ();

        offentligt ogiltigt LogToFile (strängmeddelande)

        {

            _fileLogger.Log (meddelande);

        }

        offentligt ogiltigt LogToDatabase (strängmeddelande)

        {

            _fileLogger.Log (meddelande);

        }

    }

Men även om detta skulle fungera, vad händer om du behöver logga in applikationens data till EventLog? Din design är inte flexibel och du kommer att tvingas ändra ProductService-klassen varje gång du behöver logga in på ett nytt loggmål. Detta är inte bara besvärligt utan gör det också extremt svårt för dig att hantera ProductService-klassen över tid.

Lägg till flexibilitet med ett gränssnitt 

Lösningen på detta problem är att använda ett gränssnitt som betongloggerklasserna skulle implementera. Följande kodavsnitt visar ett gränssnitt som heter ILogger. Detta gränssnitt skulle implementeras av de två konkreta klasserna FileLogger och DatabaseLogger.

offentligt gränssnitt ILogger

{

    ogiltig logg (strängmeddelande);

}

De uppdaterade versionerna av klasserna FileLogger och DatabaseLogger ges nedan.

offentlig klass FileLogger: ILogger

    {

        public void Log (strängmeddelande)

        {

            Console.WriteLine ("Inside Log-metoden för FileLogger.");

            LogToFile (meddelande);

        }

        privat ogiltigt LogToFile (strängmeddelande)

        {

            Console.WriteLine ("Metod: LogToFile, Text: {0}", meddelande);

        }

    }

public class DatabaseLogger: ILogger

    {

        public void Log (strängmeddelande)

        {

            Console.WriteLine ("Inside Log-metoden för DatabaseLogger.");

            LogToDatabase (meddelande);

        }

        privat ogiltigt LogToDatabase (strängmeddelande)

        {

            Console.WriteLine ("Metod: LogToDatabase, Text: {0}", meddelande);

        }

    }

Du kan nu använda eller ändra den konkreta implementeringen av ILogger-gränssnittet när det behövs. Följande kodavsnitt visar klassen ProductService med en implementering av Log-metoden.

offentlig klass ProductService

    {

        public void Log (strängmeddelande)

        {

            ILogger logger = ny FileLogger ();

            logger.Log (meddelande);

        }

    }

Än så länge är allt bra. Men vad händer om du vill använda DatabaseLogger i stället för FileLogger i loggmetoden i ProductService-klassen? Du kan ändra implementeringen av Log-metoden i klassen ProductService för att uppfylla kraven, men det gör inte designen flexibel. Låt oss nu göra designen mer flexibel genom att använda inversion av kontroll och beroendeinjektion.

Invertera kontrollen med hjälp av beroendeinjektion

Följande kodavsnitt illustrerar hur du kan dra nytta av beroendeinsprutning för att skicka en instans av en betongloggarklass med hjälp av konstruktionsinjektion.

offentlig klass ProductService

    {

        privat readonly ILogger _logger;

        public ProductService (ILogger logger)

        {

            _logger = logger;

        }

        public void Log (strängmeddelande)

        {

            _logger.Log (meddelande);

        }

    }

Slutligen, låt oss se hur vi kan skicka en implementering av ILogger-gränssnittet till ProductService-klassen. Följande kodavsnitt visar hur du kan skapa en instans av FileLogger-klassen och använda konstruktörinjektion för att klara beroendet.

static void Main (sträng [] args)

{

    ILogger logger = ny FileLogger ();

    ProductService productService = ny ProductService (logger);

    productService.Log ("Hello World!");

}

Genom att göra det har vi inverterat kontrollen. ProductService-klassen ansvarar inte längre för att skapa en instans av en implementering av ILogger-gränssnittet eller till och med bestämma vilket implementering av ILogger-gränssnittet ska användas.

Inversion av kontroll och beroendeberoende hjälper dig med automatisk instantiering och livscykelhantering av dina objekt. ASP.NET Core innehåller en enkel, inbyggd inversion av kontrollcontainer med en begränsad uppsättning funktioner. Du kan använda den här inbyggda IoC-behållaren om dina behov är enkla eller använda en tredjepartsbehållare om du vill utnyttja ytterligare funktioner.

Du kan läsa mer om hur du arbetar med inversion av kontroll och beroendeinjektion i ASP.NET Core i mitt tidigare inlägg här.