Verwendung der Steuerumkehrung in C #

Sowohl die Umkehrung der Steuerung als auch die Abhängigkeitsinjektion ermöglichen es Ihnen, Abhängigkeiten zwischen den Komponenten in Ihrer Anwendung aufzulösen und das Testen und Verwalten Ihrer Anwendung zu vereinfachen. Die Inversion der Kontrolle und die Abhängigkeitsinjektion sind jedoch nicht gleich - es gibt subtile Unterschiede zwischen den beiden.

In diesem Artikel untersuchen wir die Inversion des Kontrollmusters und verstehen anhand relevanter Codebeispiele in C #, wie es sich von der Abhängigkeitsinjektion unterscheidet.

Um mit den in diesem Artikel bereitgestellten Codebeispielen arbeiten zu können, muss Visual Studio 2019 auf Ihrem System installiert sein. Wenn Sie noch keine Kopie haben, können Sie Visual Studio 2019 hier herunterladen. 

Erstellen Sie ein Konsolenanwendungsprojekt in Visual Studio

Zunächst erstellen wir ein .NET Core-Konsolenanwendungsprojekt in Visual Studio. Angenommen, Visual Studio 2019 ist auf Ihrem System installiert, führen Sie die folgenden Schritte aus, um ein neues .NET Core-Konsolenanwendungsprojekt in Visual Studio zu erstellen.

  1. Starten Sie die Visual Studio-IDE.
  2. Klicken Sie auf "Neues Projekt erstellen".
  3. Wählen Sie im Fenster "Neues Projekt erstellen" aus der Liste der angezeigten Vorlagen "Konsolen-App (.NET Core)" aus.
  4. Weiter klicken. 
  5. Geben Sie im nächsten Fenster "Konfigurieren Sie Ihr neues Projekt" den Namen und den Speicherort für das neue Projekt an.
  6. Klicken Sie auf Erstellen. 

Dadurch wird ein neues .NET Core-Konsolenanwendungsprojekt in Visual Studio 2019 erstellt. In diesem Projekt wird in den folgenden Abschnitten dieses Artikels die Umkehrung der Steuerung untersucht.

Was ist Umkehrung der Kontrolle?

Inversion of Control (IoC) ist ein Entwurfsmuster, bei dem der Steuerungsfluss eines Programms invertiert wird. Sie können die Inversion des Steuerungsmusters nutzen, um die Komponenten Ihrer Anwendung zu entkoppeln, Abhängigkeitsimplementierungen auszutauschen, Abhängigkeiten zu verspotten und Ihre Anwendung modular und testbar zu machen.

Die Abhängigkeitsinjektion ist eine Teilmenge des Inversionsregelungsprinzips. Mit anderen Worten, die Abhängigkeitsinjektion ist nur eine Möglichkeit, die Umkehrung der Steuerung zu implementieren. Sie können die Umkehrung der Steuerung auch mithilfe von Ereignissen, Delegaten, Vorlagenmustern, Factory-Methoden oder Service Locator implementieren.

Die Umkehrung des Steuerungsentwurfsmusters besagt, dass Objekte keine Objekte erstellen sollten, von denen sie abhängen, um eine Aktivität auszuführen. Stattdessen sollten sie diese Objekte von einem externen Dienst oder einem Container erhalten. Die Idee ist analog zum Hollywood-Prinzip: "Rufen Sie uns nicht an, wir rufen Sie an." Beispielsweise würde das Framework anstelle der Anwendung, die die Methoden in einem Framework aufruft, die Implementierung aufrufen, die von der Anwendung bereitgestellt wurde. 

Inversion des Kontrollbeispiels in C #

Angenommen, Sie erstellen eine Auftragsabwicklungsanwendung und möchten die Protokollierung implementieren. Nehmen wir der Einfachheit halber an, dass das Protokollziel eine Textdatei ist. Wählen Sie das soeben erstellte Konsolenanwendungsprojekt im Projektmappen-Explorer aus und erstellen Sie zwei Dateien mit den Namen ProductService.cs und FileLogger.cs.

    öffentliche Klasse ProductService

    {

        privat schreibgeschützt FileLogger _fileLogger = new FileLogger ();

        public void Log (String-Nachricht)

        {

            _fileLogger.Log (Nachricht);

        }}

    }}

    öffentliche Klasse FileLogger

    {

        public void Log (String-Nachricht)

        {

            Console.WriteLine ("Inside Log-Methode von FileLogger.");

            LogToFile (Nachricht);

        }}

        private void LogToFile (String-Nachricht)

        {

            Console.WriteLine ("Methode: LogToFile, Text: {0}", Nachricht);

        }}

    }}

Die im vorhergehenden Code-Snippet gezeigte Implementierung ist korrekt, es gibt jedoch eine Einschränkung. Sie dürfen nur Daten in einer Textdatei protokollieren. Sie können Daten in keiner Weise in anderen Datenquellen oder anderen Protokollzielen protokollieren.

Eine unflexible Implementierung der Protokollierung

Was ist, wenn Sie Daten in einer Datenbanktabelle protokollieren möchten? Die vorhandene Implementierung würde dies nicht unterstützen und Sie wären gezwungen, die Implementierung zu ändern. Sie können die Implementierung der FileLogger-Klasse ändern oder eine neue Klasse erstellen, z. B. DatabaseLogger.

    öffentliche Klasse DatabaseLogger

    {

        public void Log (String-Nachricht)

        {

            Console.WriteLine ("Inside Log-Methode von DatabaseLogger.");

            LogToDatabase (Nachricht);

        }}

        private void LogToDatabase (Zeichenfolgenachricht)

        {

            Console.WriteLine ("Methode: LogToDatabase, Text: {0}", Nachricht);

        }}

    }}

Sie können sogar eine Instanz der DatabaseLogger-Klasse innerhalb der ProductService-Klasse erstellen, wie im folgenden Codeausschnitt gezeigt.

öffentliche Klasse ProductService

    {

        privat schreibgeschützt FileLogger _fileLogger = new FileLogger ();

        privat schreibgeschützt DatabaseLogger _databaseLogger =

         neuer DatabaseLogger ();

        public void LogToFile (Zeichenfolgenachricht)

        {

            _fileLogger.Log (Nachricht);

        }}

        public void LogToDatabase (Zeichenfolgenachricht)

        {

            _fileLogger.Log (Nachricht);

        }}

    }}

Obwohl dies funktionieren würde, was wäre, wenn Sie die Daten Ihrer Anwendung in EventLog protokollieren müssten? Ihr Design ist nicht flexibel und Sie müssen die ProductService-Klasse jedes Mal ändern, wenn Sie sich bei einem neuen Protokollziel anmelden müssen. Dies ist nicht nur umständlich, sondern erschwert Ihnen auch die Verwaltung der ProductService-Klasse im Laufe der Zeit.

Fügen Sie Flexibilität mit einer Schnittstelle hinzu 

Die Lösung für dieses Problem besteht darin, eine Schnittstelle zu verwenden, die die konkreten Logger-Klassen implementieren würden. Das folgende Codefragment zeigt eine Schnittstelle namens ILogger. Diese Schnittstelle würde von den beiden konkreten Klassen FileLogger und DatabaseLogger implementiert.

öffentliche Schnittstelle ILogger

{

    void Log (String-Nachricht);

}}

Die aktualisierten Versionen der Klassen FileLogger und DatabaseLogger sind unten angegeben.

öffentliche Klasse FileLogger: ILogger

    {

        public void Log (String-Nachricht)

        {

            Console.WriteLine ("Inside Log-Methode von FileLogger.");

            LogToFile (Nachricht);

        }}

        private void LogToFile (String-Nachricht)

        {

            Console.WriteLine ("Methode: LogToFile, Text: {0}", Nachricht);

        }}

    }}

öffentliche Klasse DatabaseLogger: ILogger

    {

        public void Log (String-Nachricht)

        {

            Console.WriteLine ("Inside Log-Methode von DatabaseLogger.");

            LogToDatabase (Nachricht);

        }}

        private void LogToDatabase (Zeichenfolgenachricht)

        {

            Console.WriteLine ("Methode: LogToDatabase, Text: {0}", Nachricht);

        }}

    }}

Sie können jetzt die konkrete Implementierung der ILogger-Schnittstelle bei Bedarf verwenden oder ändern. Das folgende Codefragment zeigt die ProductService-Klasse mit einer Implementierung der Log-Methode.

öffentliche Klasse ProductService

    {

        public void Log (String-Nachricht)

        {

            ILogger logger = neuer FileLogger ();

            logger.Log (Nachricht);

        }}

    }}

So weit, ist es gut. Was ist jedoch, wenn Sie den DatabaseLogger anstelle des FileLogger in der Log-Methode der ProductService-Klasse verwenden möchten? Sie können die Implementierung der Log-Methode in der ProductService-Klasse ändern, um die Anforderungen zu erfüllen. Dies macht das Design jedoch nicht flexibel. Lassen Sie uns nun das Design flexibler gestalten, indem wir die Inversion der Steuerung und die Abhängigkeitsinjektion verwenden.

Invertieren Sie das Steuerelement mithilfe der Abhängigkeitsinjektion

Das folgende Codefragment zeigt, wie Sie die Abhängigkeitsinjektion nutzen können, um eine Instanz einer konkreten Logger-Klasse mithilfe der Konstruktorinjektion zu übergeben.

öffentliche Klasse ProductService

    {

        privat schreibgeschützt ILogger _logger;

        öffentlicher ProductService (ILogger-Logger)

        {

            _logger = logger;

        }}

        public void Log (String-Nachricht)

        {

            _logger.Log (Nachricht);

        }}

    }}

Zuletzt wollen wir sehen, wie wir eine Implementierung der ILogger-Schnittstelle an die ProductService-Klasse übergeben können. Das folgende Codefragment zeigt, wie Sie eine Instanz der FileLogger-Klasse erstellen und die Abhängigkeit mithilfe der Konstruktorinjektion übergeben können.

statische Leere Main (string [] args)

{

    ILogger logger = neuer FileLogger ();

    ProductService productService = neuer ProductService (Logger);

    productService.Log ("Hallo Welt!");

}}

Dabei haben wir die Steuerung umgekehrt. Die ProductService-Klasse ist nicht mehr dafür verantwortlich, eine Instanz einer Implementierung der ILogger-Schnittstelle zu erstellen oder sogar zu entscheiden, welche Implementierung der ILogger-Schnittstelle verwendet werden soll.

Die Umkehrung der Steuerung und die Abhängigkeitsinjektion unterstützen Sie bei der automatischen Instanziierung und dem Lebenszyklusmanagement Ihrer Objekte. ASP.NET Core enthält eine einfache, integrierte Inversion des Steuercontainers mit einer begrenzten Anzahl von Funktionen. Sie können diesen integrierten IoC-Container verwenden, wenn Ihre Anforderungen einfach sind, oder einen Container eines Drittanbieters verwenden, wenn Sie zusätzliche Funktionen nutzen möchten.

Weitere Informationen zum Arbeiten mit der Inversion der Steuerung und der Abhängigkeitsinjektion in ASP.NET Core finden Sie in meinem früheren Beitrag hier.