Einleitung: Wenn SOLID aufhört, Theorie zu sein
Die meisten Entwickler kennen das Akronym SOLID auf der Ebene "eine Klasse sollte für eine Sache verantwortlich sein". In realen Projekten mit C# (.NET), Dependency Injection, Microservices und asynchronen Patterns führt die naive Befolgung der Prinzipien jedoch zum gegenteiligen Effekt – Überkomplexität, Aufblähung von Schnittstellen und Verletzung der Kapselung.
In diesem Artikel betrachten wir SOLID nicht als eine Reihe von Dogmen, sondern als Werkzeug für architektonische Entscheidungen. Sie erfahren, wie Sie die Prinzipien in fortgeschrittenen Szenarien richtig anwenden: Arbeit mit IoC-Containern, Komposition statt Vererbung, Fehlerbehandlung über Schnittstellen und Testen von Sagas.
1. Single Responsibility: Verantwortungsgrenzen in DDD
Das Prinzip der einzigen Verantwortung (SRP) wird oft mit "eine Klasse – eine Funktion" verwechselt. Tatsächlich geht es um den Änderungsgrund (reason to change). Im Kontext von C# und Domain-Driven Design bedeutet dies, dass eine Klasse sich nur aus einem Grund ändern sollte, der mit einem einzigen Akteur oder einer Geschäftsregel zusammenhängt.
Fortgeschrittenes Antipattern: "Göttliche Entität"
Betrachten wir ein typisches Beispiel für die Verletzung von SRP in .NET – die Klasse OrderEntity, die gleichzeitig den Bestellstatus verwaltet, Steuern berechnet, E-Mails sendet und Daten über Entity Framework in der DB speichert.
// ❌ Verletzung von SRPpublic class OrderEntity{ public int Id { get; set; } public decimal Total { get; set; } public string Email { get; set; }
public void CalculateTax() { /* Steuerlogik */ } public void SendConfirmation() { /* E-Mail-Versand */ } public void SaveToDatabase() { /* EF Core SaveChanges */ }}Problem: Eine Änderung der Geschäftsregel für Steuern (z. B. ein neues Gesetz) führt zu einer Änderung der Klasse, die auch für den Versand von Benachrichtigungen zuständig ist. Dies erhöht das Risiko von Regressionen.
Lösung: Auslagerung von Diensten über DI
// ✅ SRP über Dienstepublic class Order{ public int Id { get; set; } public decimal Total { get; set; } // Nur Zustand, keine Logik}
public class TaxCalculator : ITaxCalculator{ public decimal Calculate(Order order) { /* ... */ }}
public class EmailService : INotificationService{ public Task SendConfirmation(Order order) { /* ... */ }}
public class OrderRepository : IRepository<Order>{ public Task SaveAsync(Order order) { /* EF Core */ }}Jetzt hat jede Klasse genau einen Grund für eine Änderung. Der DI-Container (Microsoft.Extensions.DependencyInjection) verbindet sie über Konstruktoren.
2. Open/Closed: Erweiterung durch Strategie und Dekorierer
Das Prinzip der Offenheit/Geschlossenheit (OCP) besagt: Eine Klasse sollte für Erweiterungen offen, aber für Modifikationen geschlossen sein. In .NET wird dies nicht nur durch Vererbung, sondern auch durch Komposition mit den Patterns Strategy und Decorator erreicht.
Fortgeschrittener Fall: Validierung mit wachsenden Regeln
Stellen Sie sich vor, Sie haben ein System zur Bestellvalidierung. Die erste Version prüfte nur den Betrag und die Adresse. Einen Monat später kam die Regel hinzu "Bestellungen über dem Limit sind nicht erlaubt". Anstatt die Klasse OrderValidator zu ändern, verwenden wir Komposition.
// ❌ Verletzung von OCP – Modifikation einer bestehenden Klassepublic class OrderValidator{ public bool Validate(Order order) { if (order.Total < 0) return false; if (string.IsNullOrEmpty(order.Address)) return false; // Neue Regel – wir modifizieren die Klasse if (order.Total > 10000) return false; return true; }}// ✅ OCP durch Komposition von Validatorenpublic interface IValidator<T>{ bool Validate(T entity);}
public class TotalValidator : IValidator<Order>{ public bool Validate(Order order) => order.Total > 0;}
pu