SOLID-Designüberprüfung
Weist den Agenten an, eine C#-Klasse oder ein Modul anhand jedes SOLID-Prinzips mit konkreten .NET-Linsen zu bewerten, den Code-Geruch und das verletzte Prinzip zu benennen und die minimale Refaktorisierung vorzuschlagen – wobei Verstöße priorisiert werden, die echte Kopplung oder Änderungsaufwand verursachen, nicht akademische Spitzfindigkeiten.
Wann verwenden
- Eine Klasse wird unhandlich – sie umfasst I/O, Geschäftslogik, Mapping und Seiteneffekte in einer einzigen Datei.
- Ein PR führt eine neue Typvariante ein, und eine vorhandene
switch- oderif-else-Kette muss bearbeitet werden, um sie zu unterstützen. - Konstruktoren erzeugen ihre eigenen Abhängigkeiten mit
newoder referenzieren direktDateTime.Now/static-Helfer. - Ein Interface hat Member angesammelt, um mehrere nicht verwandte Aufrufer zu bedienen, was No-op-Implementierungen erzwingt.
Ablauf
- Identifiziere die zu überprüfende Klasse/das Modul. Nenne ihre erklärte Verantwortung. Wenn der Name „und" oder „oder" zur Beschreibung benötigt, ist das bereits ein Signal.
- Gehe jedes SOLID-Prinzip der Reihe nach durch unter Verwendung der nachstehenden C#-Linsen. Behandle jeden Durchgang separat; stoppe nicht nach dem ersten Fund.
- Benenne den Geruch und das verletzte Prinzip. „Fetter Konstruktor, der sein eigenes
HttpClientundSqlConnectionerstellt" → DIP; „Basis-Override, dasNotSupportedExceptionwirft" → LSP. Sei präzise. - Schlage die minimale Refaktorisierung mit dem C#-Mechanismus vor. Nenne den Typ, das Muster oder die Änderung der DI-Registrierung – nicht nur „wende das Prinzip an." Allgemeine „Befolge SOLID"-Ratschläge sind kein Fund.
- Priorisiere nach tatsächlicher Auswirkung. Ein God Service, der EF-Persistenz mit E-Mail-Versand mischt, verursacht in jedem Sprint Änderungsaufwand; ein
IOrderServicemit 12 Membern, die kein Aufrufer vollständig nutzt, ist eine echte ISP-Verletzung. Einesealed-Klasse, die zufällig zwei lose verwandte Anwendungsfälle erfüllt, ist kein SRP-Verstoß, wenn sie nur einen Grund zur Änderung hat. Unterscheide Verstöße, die schaden, von solchen, die nur unrein sind.
.NET / Azure-Prüfungen
SRP – Single Responsibility Principle
- God Services / God Classes. Eine einzelne Klasse, die EF Core (
_context.SaveChangesAsync), E-Mail-Versand (_emailSender.SendAsync), Geschäftsregeln validiert und in DTOs mapped, hat mehrere Gründe für Änderungen. Extrahiere einenIOrderValidator, einIOrderRepository(oder verwende den DbContext direkt in einer dedizierten Persistenzklasse) und einenIOrderNotifierals separate Kollaborateure. - Fette Controller. Ein ASP.NET Core-Controller, der
if/Validierungslogik enthält, direkt die Datenbank aufruft und Antwort-Payloads formatiert, erledigt drei Aufgaben. Controller sollten orchestrieren: einen Service aufrufen, das Ergebnis mappen,IActionResultzurückgeben. Geschäftsregeln gehören in die Anwendungs-/Domänenschicht. - „Und" im Typnamen oder in der Verantwortung.
OrderValidationAndPersistenceServiceist ein Namensgeruch, der auf SRP-Verstoß hinweist. Wenn du die Klasse nur mit „und" beschreiben kannst, teile sie auf. - Mehrere Änderungsgründe. Frage: „Müsste diese Klasse geändert werden, wenn wir den E-Mail-Anbieter wechseln? Wenn wir das Datenbankschema ändern? Wenn sich Geschäftsregeln ändern?" Mehr als ein „Ja" bedeutet mehr als eine Verantwortung.
OCP – Open/Closed Principle
- Wachsende
switch/if-elseauf einem Typunterscheidungsmerkmal. Einswitch(order.Type)-Block innerhalb eines Service, der für jeden neuenOrderType-Enumwert wieder geöffnet werden muss, verletzt OCP. Ersetze durch eine Strategie: DefiniereIOrderHandler(oderIOrderProcessor) und registriere eine Implementierung pro Typ; löse überIEnumerable<IOrderHandler>auf, die in den Koordinator injiziert wird, und dispatche basierend aufhandler.Handles(order.Type). - Hartcodierte Verhaltenserweiterung. Wenn das Hinzufügen einer neuen Rabattregel, eines Benachrichtigungskanals oder eines Exportformats die Bearbeitung einer vorhandenen Klasse erfordert, anstatt eine neue
IDiscountRuleoderIExportereinzufügen, ist die Klasse nicht für Modifikation geschlossen. Führe die Abstraktion ein, bevor der zweite Fall eintritt. - Anti-Pattern des Typunterscheidungsmerkmals.
if (order is PremiumOrder)oderif (typeof(T) == typeof(FooOrder))innerhalb einer Verarbeitungsklasse ist ein Code-Geruch, der dem obigenswitchentspricht. Polymorphie oder ein Strategie-Register eliminieren diese.
LSP – Liskov Substitution Principle
- Overrides, die
throw new NotSupportedException()/NotImplementedException()werfen. Ein Subtyp, der einen Basisvertrag nicht einhalten kann (z. B.ReadOnlyCollection<T>, das beiAddwirft), verletzt LSP. Wenn das Override nicht sinnvoll implementiert werden kann, ist die Hierarchie falsch – bevorzuge Komposition oder separate Interfaces. - Subtypen, die Nachbedingungen schwächen. Wenn die Basisklasse garantiert, dass
GetOrders()nienullzurückgibt, verletzt ein Override, dasnullzurückgibt, den Vertrag, auf den sich Aufrufer verlassen. Dokumentiere Invarianten (XML-Doc oder Contracts) und erzwinge sie in Overrides. - Subtypen, die Vorbedingungen verschärfen. Ein Override, das eine in der Basis nicht vorhandene Null-Prüfung hinzufügt oder einen gültigen Parameterbereich ablehnt, den die Basis akzeptierte, unterbricht Code, der gültige Basisvertragseingaben übergibt. Der Subtyp muss alles akzeptieren, was die Basis akzeptiert hat.
- Durchgängige
is/as-Downcasts im aufrufenden Code.if (handler is SpecialHandler sh) sh.SpecialMethod()im Aufrufer bedeutet, dass die Abstraktion kaputt ist – entweder gehört die Methode zum Basis-Interface, oder die Typen sollten keine gemeinsame Hierarchie haben. Häufige Downcasts sind ein Signal für LSP-/Abstraktionsfehler.
ISP – Interface Segregation Principle
- Fette Interfaces (10+ Member) mit nicht zusammenhängenden Belangen. Ein
IOrderService, dasCreateOrder,CancelOrder,GetInvoice,SendConfirmation,ExportToCsv,CalculateTax,ValidateAddressundGetShippingRatesbereitstellt, zwingt jeden Implementierer und Test-Double, Methoden zu stubben, die sie nicht verwenden. Teile in Rollen-Interfaces auf:IOrderWriter,IOrderReader,IOrderNotifier,IOrderExporter. - No-op-Implementierungen /
NotImplementedException-Stubs. Wenn eine Mock- oder Testimplementierung für die Hälfte der Interface-Memberthrow new NotImplementedException()zurückgibt, gehören diese Member zu einem anderen Rollen-Interface. Das Vorhandensein von No-ops ist ein direkter Beweis für ISP-Verstoß. - Clients, die Methoden importieren, die sie nie aufrufen. Wenn
PaymentServicevonIOrderServiceabhängt, aber nurGetOrderaufruft, sollte es vonIOrderReaderabhängen. Die Verengung der Abhängigkeit macht die Kopplung explizit und das Testen billiger.
DIP – Dependency Inversion Principle
new-Erzeugung konkreter Abhängigkeiten innerhalb einer Klasse.new HttpClient(), das innerhalb eines Service erstellt wird, erzeugt eine versteckte Abhängigkeit, umgeht den DI-Container, verursacht Socket-Leaks (kein Verbindungspooling) und macht die Klasse ohne Reflektionstricks untestbar. InjiziereIHttpClientFactory(typisierter Client) oder einen benannten Client über den Konstruktor. Gleiches Muster fürnew SqlConnection(connectionString)– injiziereIDbConnectionFactoryoder lasse EF Core / Dapper die Verbindung über den registriertenDbContextverwalten.- Abhängigkeit von Konkreta statt Abstraktionen. Ein Konstruktor, der auf
SqlOrderRepositorystattIOrderRepositorytypisiert ist, koppelt die Klasse an die Speichertechnologie. Jede Klasse, die wiederverwendbar oder isoliert testbar sein soll, darf nur von Interfaces oder abstrakten Typen abhängen. - Versteckte Kopplung an
DateTime.Now/ statischen Zustand.var now = DateTime.Nowinnerhalb einer Geschäftsmethode macht die Klasse nicht-deterministisch und ohne Zeitreise-Hacks untestbar. FühreISystemClockein (oder verwendeTimeProviderab .NET 8+) und injiziere es. Gleiches gilt fürEnvironment.MachineName,Random.Sharedoder andere ambient statische Werte. - Statische veränderliche Singletons. Ein
static Dictionary<string, Order> _cachein einer Serviceklasse ist versteckter gemeinsam genutzter Zustand, der das DI-Lebenszyklusmanagement umgeht und nicht sicher für gleichzeitige Nutzung ist. Ersetze durchIMemoryCache/IDistributedCache, die über DI injiziert werden und ordnungsgemäße Bereichsbildung und Räumung unterstützen.
Urteilsnotiz: Nicht jedes
newist ein DIP-Verstoß.new OrderDto(order)(ein einfacher Datenbehälter) odernew List<T>()(eine Standardauflistung) sind in Ordnung. DIP ist relevant, wenn das konstruierte Objekt Verhalten, externe I/O oder Konfiguration hat – d. h., wenn das Austauschen gegen ein Test-Double oder eine alternative Implementierung wertvoll wäre.
Rote Flaggen
| Signal | Warum es wichtig ist |
|---|---|
new HttpClient(), erstellt innerhalb einer Service-Methode oder eines Konstruktors | Umgeht das Verbindungspooling von IHttpClientFactory (Socket-Erschöpfung unter Last), versteckt die Abhängigkeit und macht die Klasse ohne Produktions-I/O untestbar. |
switch(order.Type) oder if (x is ConcreteType) in einer Koordinator-Klasse | Muss für jede neue Variante wieder geöffnet werden; Aufrufer wissen zu viel über die Typhierarchie. Ersetzen durch IEnumerable<IHandler>, das aus DI aufgelöst wird. |
throw new NotImplementedException() oder throw new NotSupportedException() in einem nicht-abstrakten Override | Der Subtyp kann den Basisvertrag nicht einhalten – LSP-Verstoß. Implementiere die Methode korrekt oder strukturiere die Hierarchie um. |
| Ein Interface mit 10+ Membern, von denen Aufrufer 2–3 verwenden | Erzwingt große Test-Doubles; signalisiert, dass mehrere Rollen-Interfaces in eines zusammengefasst wurden. |
Eine Klasse, die ihren eigenen DbContext über new AppDbContext(options) erstellt | Umgeht das DI-Lebenszyklusmanagement (Scoped vs. Transient vs. Singleton), verhindert Verbindungspooling und bricht die Testbarkeit. Injiziere AppDbContext oder IDbContextFactory<AppDbContext>. |
DateTime.Now, direkt in einer Geschäftsmethode aufgerufen | Nicht-deterministisch unter Tests; Kopplung an die Systemuhr verbirgt eine logische Abhängigkeit. Injiziere TimeProvider (.NET 8+) oder ISystemClock. |
Eine Controller-Aktion, die if/switch-Geschäftslogik oder EF-Abfragen enthält | Der Controller hat Verantwortungen übernommen, die in die Anwendungs-/Domänenschicht gehören; eine einzelne Änderung an Geschäftsregeln erfordert, den Controller zu berühren. |
Veränderliches static-Feld, das als prozessinterner Cache verwendet wird | Nicht threadsicher ohne explizite Sperren; umgeht DI-Scoping; kann nicht evakuiert oder beobachtet werden. Verwende IMemoryCache. |
Beispiel
Siehe examples/solid-review/.