SOLID-Überprüfung
Autor: Guerth Castro (github.com/GuerthCastro). Lizenziert unter MIT.
Überprüfungsprozess
Wenn Code zur Überprüfung bereitgestellt wird:
- Identifiziere die Sprache und Schicht (C#-Domänenentität, Anwendungshandler, Angular-Service, React-Komponente usw.).
- Bewerte jedes der fünf Prinzipien unabhängig.
- Erstelle einen strukturierten Bericht mit dem untenstehenden Format.
- Ordne Verstöße nach Schweregrad: Kritisch (bricht Architektur) vor Schwerwiegend (verschlechtert Wartbarkeit) vor Geringfügig (Stilpräferenz).
- Gib für jeden kritischen oder schwerwiegenden Verstoß eine konkrete refaktorierte Version an.
Fülle den Bericht nicht auf. Wenn ein Prinzip tatsächlich erfüllt ist, schreibe PASS und fahre fort.
Die fünf Prinzipien
S: Single-Responsibility-Prinzip
Eine Klasse, ein Grund zur Änderung.
Verstöße, auf die geachtet werden sollte:
- Eine Klasse führt sowohl Datenzugriff als auch Geschäftslogik durch.
- Ein Dienst sendet E-Mails, validiert Daten und schreibt in die Datenbank.
- Eine Komponente ruft Daten ab, formatiert sie und verwaltet den UI-Zustand.
- Ein Handler orchestriert mehrere nicht zusammenhängende Operationen.
- God-Klassen mit zehn oder mehr Methoden, die mehrere Domänen umfassen.
VERSTOSS. OrderService validiert, persistiert und benachrichtigt. Der Benachrichtigungsaufruf ist derjenige, der nicht dazugehört.
public class OrderService(IOrderRepository repository, IEmailService email)
{
public async Task<bool> AddItem(long orderId, long productId)
{
Order order = await repository.GetById(orderId);
if (order.ItemCount >= order.MaxItems)
{
return false;
}
await repository.AddItem(orderId, productId);
await email.SendOrderUpdated(orderId);
return true;
}
}
Behebung: Extrahiere einen Benachrichtigungsdienst und lasse den Handler die beiden Aufrufe koordinieren.
O: Offen/Geschlossen-Prinzip
Offen für Erweiterung, geschlossen für Modifikation.
Verstöße, auf die geachtet werden sollte:
if/else if-Ketten oderswitch-Blöcke auf Typ-Strings oder Enums, die mit jeder Funktion wachsen.- Das Hinzufügen eines neuen Berichtstyps erfordert die Bearbeitung des bestehenden Berichtsdienstes.
- Das Hinzufügen einer neuen Zahlungsmethode erfordert die Bearbeitung des Checkout-Handlers.
- Konkrete Abhängigkeiten, wo eine Abstraktion hingehört.
VERSTOSS. Ein neues Exportformat bedeutet, diese Methode zu bearbeiten.
public byte[] ExportInvoice(string format, IEnumerable<InvoiceLine> lines)
{
if (format == "pdf")
{
return GeneratePdf(lines);
}
if (format == "excel")
{
return GenerateExcel(lines);
}
throw new NotSupportedException(format);
}
Behebung: Eine IInvoiceExporter-Abstraktion mit PdfInvoiceExporter- und ExcelInvoiceExporter-
Implementierungen, die per Schlüssel aus dem Container aufgelöst werden.
L: Liskovsches Substitutionsprinzip
Untertypen müssen für ihre Basistypen substituierbar sein.
Verstöße, auf die geachtet werden sollte:
- Eine abgeleitete Klasse wirft
NotImplementedExceptionfür ein geerbtes Member. - Eine Überschreibung ändert das erwartete Verhalten: ein stiller No-Op, eine andere Rückgabeform, ein neuer Ausnahmetyp.
- Ein Interface deklariert Member, die einige Implementierungen nicht sinnvoll implementieren können.
- Casten auf einen konkreten Typ, um Verhalten zu erreichen:
if (repository is SqlOrderRepository sql).
I: Schnittstellentrennungsprinzip
Keine Klasse sollte gezwungen sein, Member zu implementieren, die sie nicht verwendet.
Verstöße, auf die geachtet werden sollte:
- Fette Interfaces mit fünfzehn oder mehr Membern, bei denen Implementierungen drei verwenden.
- Ein generisches
IRepository<T>, dasGetAll,GetById,Create,Update,Delete,Paginate,SearchundBulkInsertfür jede Entität erzwingt. - Ein Dienst-Interface, das Lese- und Schreiboperationen mischt, wenn Verbraucher nur eine Seite benötigen.
VERSTOSS. Ein reiner Lesebericht-Verbraucher ist gezwungen, Create, Update und Delete zu sehen.
public interface IOrderRepository
{
Task<Order> GetById(long id);
Task<IEnumerable<Order>> GetAll();
Task Create(Order order);
Task Update(Order order);
Task Delete(long id);
}
Behebung: Aufteilen in IOrderReader und IOrderWriter. Zusammensetzen, wo beide benötigt werden.
D: Abhängigkeitsumkehrprinzip
Hänge von Abstraktionen ab, nicht von Konkretionen.
Verstöße, auf die geachtet werden sollte:
newinstanziiert einen Dienst oder ein Repository innerhalb der Geschäftslogik.- Eine direkte Abhängigkeit von
SqlConnectionoderNpgsqlConnectioninnerhalb eines Dienstes. - Die Anwendungsschicht importiert Infrastrukturtypen.
- Statische Dienstklassen mit veränderlichem Zustand.
VERSTOSS. Der Handler baut sein eigenes konkretes Repository und seine eigene Verbindung.
public class GetOrderHandler
{
public async Task<OrderDto> Handle(long id)
{
OrderRepository repository = new(new SqlConnection("..."));
Order order = await repository.GetById(id);
return order.MapToDto();
}
}
Behebung: Injiziere IOrderRepository über den primären Konstruktor und registriere es im
Container an der Compositenwurzel.
Berichtsformat
Erzeuge diese Struktur für jede Überprüfung:
## SOLID-Überprüfung: [Klassenname oder Dateiname]
**Sprache/Schicht:** C#-Anwendungshandler, Angular-Service usw.
### S: Single Responsibility
Status: PASS / VERSTOSS (Kritisch, Schwerwiegend, Geringfügig)
Befund: [was die Klasse über ihre einzige Verantwortung hinaus tut]
Behebung: [konkrete Aktion]
### O: Offen/Geschlossen
Status: PASS / VERSTOSS
Befund: ...
Behebung: ...
### L: Liskov Substitution
Status: PASS / VERSTOSS / N/A (keine Vererbung)
Befund: ...
### I: Schnittstellentrennung
Status: PASS / VERSTOSS / N/A (keine Interfaces)
Befund: ...
### D: Abhängigkeitsumkehr
Status: PASS / VERSTOSS
Befund: ...
Behebung: ...
### Prioritäre Verstöße
1. [Kritisch] DIP: Handler instanziiert ein konkretes Repository
2. [Schwerwiegend] SRP: Dienst sendet auch die Benachrichtigungs-E-Mail
### Refaktorierte Version
[vollständiger korrigierter Code-Block]
Verstöße gegen die Schichtung der Clean Architecture
Über SOLID hinaus, markiere diese übergreifenden Probleme:
- Eine Domänenentität importiert Anwendungs- oder Infrastrukturtypen (Verstoß gegen die Abhängigkeitsrichtung).
- Ein Controller enthält Geschäftslogik, die in einen Handler gehört.
- Ein Repository gibt DTOs anstelle von Domänenentitäten zurück. Mapping gehört in die Anwendung.
- Infrastruktur ruft eine externe API direkt auf, anstatt ein Interface zu implementieren, das in der Anwendung oder Domäne deklariert wurde.
- Ein Handler greift auf
HttpContextzu, was ein Infrastruktur-Bedenken in die Anwendungsschicht durchsickern lässt. - Eine DI-Registrierung in der falschen Assembly, sodass eine Schicht nach unten referenzieren muss, um zu kompilieren.