Table of Contents

Dependency Injection – Grundlagen

Dieses Kapitel beleuchtet die theoretischen Grundlagen von Dependency Injection (DI). Dabei werden zentrale Konzepte, Vorteile und typische Einsatzszenarien von DI erläutert. Das Ziel ist es, ein grundlegendes Verständnis für die Bedeutung und Anwendung von DI in der Softwareentwicklung zu vermitteln.

Framework Studio spezifische Aspekte werden im Kapitel Dependency-Injection in Framework Studio behandelt.

1. Einführung: Warum ist Dependency Injection relevant?

Dependency Injection (DI) ist ein zentrales Prinzip moderner Softwareentwicklung. Es hilft, die Kopplung zwischen Komponenten zu reduzieren, fördert die Testbarkeit und Flexibilität des Codes.

Probleme ohne DI:

  • Enge Kopplung: Klassen sind fest miteinander verbunden. Änderungen an einer Klasse erfordern oft Anpassungen an vielen anderen Stellen.
  • Schwierige Testbarkeit: Abhängigkeiten sind fest im Code verdrahtet. Mocking oder das Ersetzen von Komponenten für Tests wird schwierig.
  • Wenig Wiederverwendbarkeit: Komponenten können außerhalb ihres ursprünglichen Kontexts nur schwer verwendet werden.

2. Praktische Beispiele ohne Dependency Injection

Klassisches Vorgehen: Direkte Instanziierung

public class OrderService
{
    private readonly PaymentService _paymentService;
    private readonly Logger _logger;

    public OrderService()
    {
        _paymentService = new PaymentService();
        _logger = new Logger();
    }

    public void ProcessOrder(Order order)
    {
        _logger.Log("Order processing started.");
        _paymentService.ProcessPayment(order);
        _logger.Log("Order processing finished.");
    }
}

Nachteile dieses Ansatzes

  • Testbarkeit: Für Unit-Tests kann der PaymentService und Logger nicht einfach durch Mocks ersetzt werden.
  • Starre Architektur: Sollte z. B. ein anderer Logger benötigt werden, muss der Code angepasst und neu deployed werden.
  • Verletzung von SOLID-Prinzipien: Besonders das Open/Closed Principle und Dependency Inversion Principle werden missachtet.

Anmerkung: Framework Studio setzt auf Interfaces und ...Factory.Create(). Das löst aber nicht das Problem, weil wir "praktisch" von den Klassen abhängen.

3. Einführung in Dependency Injection

Grundidee und Funktionsweise

DI trennt die Erzeugung und Verwendung von Abhängigkeiten. Die benötigten Komponenten werden der Klasse übergeben – meist beim Erstellen (Constructor Injection).

  • Dependency: Eine Komponente, die eine andere benötigt (z. B. OrderService benötigt PaymentService).
  • Injection: Die Abhängigkeit wird von außen bereitgestellt, anstatt sie selbst zu erzeugen.

Arten der DI

  • Constructor Injection: Empfehlung, da alle Abhängigkeiten beim Erstellen vorhanden sind.

    public class OrderService
    {
        private readonly IPaymentService _paymentService;
        private readonly ILogger _logger;
    
        public OrderService(IPaymentService paymentService, ILogger logger)
        {
            _paymentService = paymentService;
            _logger = logger;
        }
    
        // ...
    }
    
  • Setter Injection: Abhängigkeiten werden über Set-Methoden zugewiesen.

    public class OrderService
    {
        public IPaymentService PaymentService { get; set; }
        public ILogger Logger { get; set; }
    }
    
  • Interface Injection: Die Abhängigkeit wird durch eine Schnittstelle gesetzt (selten genutzt).

4. Alternativen zu Dependency Injection

(Exkurs...)
  • Service Locator: wird als Anti-Pattern betrachtet, Eine zentrale Instanz („Service Locator“) hält Referenzen auf alle Services. Wenn eine Klasse eine Abhängigkeit benötigt, fragt sie den Service Locator danach.

    public class OrderService
    {
        private readonly PaymentService _paymentService;
        private readonly Logger _logger;
    
        public OrderService()
        {
            _paymentService = ServiceLocator.Get<PaymentService>();
            _logger = ServiceLocator.Get<Logger>();
        }
    }
    

    Vor- und Nachteile:

    • Vorteil: Einfach zu implementieren, überschaubar in kleinen Projekten.
    • Nachteil: Versteckte Abhängigkeiten, erschwerte Testbarkeit (Mocks schwer einzubringen), Code ist weniger transparent und lose gekoppelt.
  • Manuelle Instanziierung / Factory Pattern: Abhängigkeiten werden direkt in der Klasse erzeugt oder über Factories bereitgestellt.

    public class OrderService
    {
        private readonly PaymentService _paymentService;
    
        public OrderService()
        {
            _paymentService = PaymentServiceFactory.Create();
        }
    }
    

    Vor- und Nachteile:

    • Vorteil: Einfach, keine externe Infrastruktur nötig.
    • Nachteil: Enge Kopplung, schwierige Testbarkeit, nicht flexibel bei unterschiedlichen Implementierungen (z. B. für Tests).
  • Singleton Pattern: Einzelne Services werden als Singleton implementiert und global bereitgestellt.

    public class Logger
    {
        private static Logger _instance = new Logger();
        public static Logger Instance => _instance;
    
        private Logger() { }
    }
    
    public class OrderService
    {
        public void PlaceOrder(Order order)
        {
            Logger.Instance.Log("...");
        }
    }
    
    

    Vor- und Nachteile:

    • Vorteil: Globale Verfügbarkeit eines Services.
    • Nachteil: Globale Zustände sind oft schwer zu testen und führen zu versteckten Abhängigkeiten.

5. Praxisbeispiel mit Dependency Injection

Wir nutzen weiterhin das Beispiel mit OrderService, PaymentService und Logger.

Schrittweise Umwandlung mit DI

1. Definiere die Interfaces

public interface IPaymentService
{
    void ProcessPayment(Order order);
}

public interface ILogger
{
    void Log(string message);
}

2. Implementierungen

public class PaymentService : IPaymentService
{
    public void ProcessPayment(Order order)
    {
        // Zahlungslogik
    }
}

public class Logger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

3. Anwendung von Constructor Injection

public class OrderService
{
    private readonly IPaymentService _paymentService;
    private readonly ILogger _logger;

    public OrderService(IPaymentService paymentService, ILogger logger)
    {
        _paymentService = paymentService;
        _logger = logger;
    }

    public void ProcessOrder(Order order)
    {
        _logger.Log("Order processing started.");
        _paymentService.ProcessPayment(order);
        _logger.Log("Order processing finished.");
    }
}

4. Vorteile herausarbeiten

  • Flexibilität: Verschiedene Implementierungen (z. B. TestPaymentService, FileLogger) können leicht eingebunden werden.

  • Testbarkeit: Durch die Verwendung von Interfaces können Mocks oder alternative Implementierungen verwendet werden.

    [TestMethod]
    public void PlaceOrder_Should_Call_PaymentService_And_Logger()
    {
        // Arrange
        var paymentServiceMock = new Mock<IPaymentService>();
        var loggerMock = new Mock<ILogger>();
        var order = new Order();
    
        var orderService = new OrderService(paymentServiceMock.Object, loggerMock.Object);
    
        // Act
        orderService.PlaceOrder(order);
    
        // Assert
        paymentServiceMock.Verify(ps => ps.ProcessPayment(order), Times.Once);
        loggerMock.Verify(l => l.Log("Order placed"), Times.Once);
    }
    

6. Dependency Injection Frameworks und Tools

Es gibt verschiedene DI-Frameworks:

  • Microsoft.Extensions.DependencyInjection (Standard in ASP.NET Core)
  • Autofac
  • Ninject
  • uvm ...

Ein DI-Framework übernimmt viele Aufgaben:

  • Automatische Instanziierung und Bereitstellung von Abhängigkeiten
    Das Framework erstellt benötigte Objekte und versorgt Klassen mit deren Dependencies. Die Klassen sind nicht selbst für die Erzeugung verantwortlich.

  • Verwaltung des Lebenszyklus (Scopes) der Objekte
    Das DI-Framework steuert, wie lange und in welchem Kontext Instanzen existieren (z. B. Singleton, Scoped, Transient).

  • Konfiguration und Auflösung von Abhängigkeitsketten
    Das Framework stellt sicher, dass alle benötigten Services korrekt konfiguriert, registriert und bei Bedarf aufgelöst werden. Dies gilt auch bei komplexen Verschachtelungen.

In Framework Studio setzen wir auf den Microsoft-Standard.

Beispiel für Infrastruktur und fachliche Services

var services = new ServiceCollection();
services.AddSingleton<ILogger, Logger>();                 // Infrastruktur
services.AddScoped<IOrderService, OrderService>();        // Fachlicher Service
services.AddTransient<IPaymentService, PaymentService>(); // Fachlicher Service

Hinweis: Infrastruktur-Komponenten wie Logger werden typischerweise als Singleton registriert.

7. Guideline: Daten-Objekte über Factory oder Repository erzeugen

Daten-Objekte dürfen nicht über DI aufgelöst werden.

Nicht:

public class SomeService
{
    private readonly Order _order;

    public SomeService(Order order)
    {
        _order = order;
    }
}

Stattdessen:

public interface IOrderFactory
{
    Order Create();
}

Die Factory wird über DI injiziert und erzeugt das Objekt. Dies erleichtert die Erweiterbarkeit und Testbarkeit, da die Erzeugungslogik gekapselt ist.

Trennung von Logik und Daten ist wichtig.

8. IDisposable in Bezug auf die Lifetimes

Wenn ein Service IDisposable implementiert, sorgt das DI-Framework dafür, dass Dispose korrekt aufgerufen wird. Dies geschieht abhängig vom gewählten Lifetime.

public class Logger : ILogger, IDisposable
{
    public void Log(string message) { /* ... */ }
    public void Dispose() { /* Ressourcen freigeben */ }
}
  • Singleton: Dispose wird beim Beenden der Anwendung aufgerufen.
  • Scoped: Dispose wird am Ende des Scope (z. B. Request) aufgerufen.
  • Transient: Dispose wird am Ende des Scope (z. B. Request) aufgerufen. (Wenn möglich vermeiden)

Fazit und Zusammenfassung

  • Dependency Injection fördert die Entkopplung und Testbarkeit.
  • Die Wahl des passenden Lifetime ist entscheidend für Ressourcenmanagement.
  • Fachliche Datenobjekte sollten über Factories/Repositories erzeugt werden.
  • Infrastruktur und fachliche Services können einheitlich über DI verwaltet werden.
  • DI macht den Code flexibel, wartbar und testbar – und ist ein zentraler Bestandteil moderner Softwareentwicklung mit C#.