Die SRP stellt ohne Zweifel fest, dass eine Klasse immer nur einen Grund haben sollte, sich zu ändern.
Bei der Dekonstruktion der Klasse "report" in der Frage gibt es drei Methoden:
printReport
getReportData
formatReport
Wenn man die Redundanz ignoriert Report
, die in jeder Methode verwendet wird, ist leicht zu erkennen, warum dies gegen die SRP verstößt:
Der Begriff "Drucken" impliziert eine Art Benutzeroberfläche oder einen tatsächlichen Drucker. Diese Klasse enthält daher eine gewisse Menge an Benutzeroberfläche oder Präsentationslogik. Eine Änderung der UI-Anforderungen erfordert eine Änderung der Report
Klasse.
Der Begriff "Daten" impliziert eine Datenstruktur, gibt jedoch nicht wirklich an, was (XML? JSON? CSV?). Unabhängig davon, ob sich der "Inhalt" des Berichts jemals ändert, wird sich diese Methode auch ändern. Es besteht entweder eine Kopplung an eine Datenbank oder eine Domäne.
formatReport
ist nur ein schrecklicher Name für eine Methode im Allgemeinen, aber ich würde davon ausgehen, dass sie wieder etwas mit der Benutzeroberfläche zu tun hat und wahrscheinlich einen anderen Aspekt der Benutzeroberfläche als printReport
. Also ein weiterer, nicht verwandter Grund, sich zu ändern.
Diese eine Klasse ist also möglicherweise mit einer Datenbank, einem Bildschirm- / Druckergerät und einer internen Formatierungslogik für Protokolle oder Dateiausgaben oder so weiter gekoppelt. Wenn Sie alle drei Funktionen in einer Klasse haben, multiplizieren Sie die Anzahl der Abhängigkeiten und verdreifachen die Wahrscheinlichkeit, dass eine Abhängigkeits- oder Anforderungsänderung diese Klasse (oder etwas anderes, das davon abhängt) zerstört.
Ein Teil des Problems hier ist, dass Sie ein besonders heikles Beispiel ausgewählt haben. Sie sollten wahrscheinlich keine Klasse namens haben Report
, auch wenn sie nur eines tut , weil ... welcher Bericht? Sind nicht alle "Berichte" völlig unterschiedliche Bestien, basierend auf unterschiedlichen Daten und unterschiedlichen Anforderungen? Und ist es nicht ein Bericht etwas , das ist bereits formatiert worden ist , entweder für Bildschirm oder für den Druck?
IncomeStatement
Wenn man jedoch darüber hinausblickt und einen hypothetischen konkreten Namen formuliert - nennen wir es (ein sehr häufiger Bericht) -, hätte eine richtige "SRPed" -Architektur drei Typen:
IncomeStatement
- die Domänen- und / oder Modellklasse , die die Informationen enthält und / oder berechnet , die in formatierten Berichten angezeigt werden.
IncomeStatementPrinter
, die wahrscheinlich einige Standardschnittstellen wie implementieren würde IPrintable<T>
. Verfügt über eine Schlüsselmethode Print(IncomeStatement)
und möglicherweise einige andere Methoden oder Eigenschaften zum Konfigurieren druckspezifischer Einstellungen.
IncomeStatementRenderer
, das das Rendern von Bildschirmen übernimmt und der Druckerklasse sehr ähnlich ist.
Sie können eventuell auch weitere funktionsspezifische Klassen wie IncomeStatementExporter
/ hinzufügen IExportable<TReport, TFormat>
.
Dies wird in modernen Sprachen durch die Einführung von Generika und IoC-Containern erheblich erleichtert. Der größte Teil Ihres Anwendungscodes muss nicht auf die jeweilige IncomeStatementPrinter
Klasse angewiesen sein , sondern kann jede Art von druckbarem Bericht verwenden IPrintable<T>
und damit arbeiten. Dadurch erhalten Sie alle wahrgenommenen Vorteile einer Basisklasse mit einer Methode und keiner der üblichen SRP-Verstöße . Die tatsächliche Implementierung muss nur einmal in der IoC-Containerregistrierung deklariert werden.Report
print
Einige Leute antworten, wenn sie mit dem obigen Design konfrontiert werden, mit etwas wie: "Aber das sieht aus wie prozeduraler Code, und der springende Punkt bei OOP war, uns von der Trennung von Daten und Verhalten fernzuhalten!" Zu dem sage ich: falsch .
Das IncomeStatement
sind nicht nur "Daten", und der oben erwähnte Fehler führt dazu, dass viele OOP-Leute das Gefühl haben, dass sie etwas falsch machen, indem sie eine solche "transparente" Klasse erstellen und anschließend alle Arten von nicht verwandten Funktionen in die IncomeStatement
( na ja, das, jammen) einbinden und allgemeine Faulheit). Diese Klasse beginnt möglicherweise nur als Daten, wird aber im Laufe der Zeit garantiert eher zu einem Modell .
Eine reale Gewinn- und Verlustrechnung enthält beispielsweise Gesamteinnahmen , Gesamtausgaben und Nettogewinnlinien . Ein ordnungsgemäß gestaltetes Finanzsystem speichert diese höchstwahrscheinlich nicht , da es sich nicht um Transaktionsdaten handelt. Tatsächlich ändern sie sich aufgrund der Hinzufügung neuer Transaktionsdaten. Die Berechnung dieser Zeilen ist jedoch immer exakt gleich, unabhängig davon, ob Sie den Bericht drucken, rendern oder exportieren. So Ihre IncomeStatement
Klasse wird eine angemessene Menge von Verhalten , um es in Form haben getTotalRevenues()
, getTotalExpenses()
und getNetIncome()
Methoden, und wahrscheinlich einige andere. Es ist ein echtes Objekt im OOP-Stil mit eigenem Verhalten, auch wenn es nicht wirklich viel zu "tun" scheint.
Aber die format
und print
Methoden haben nichts mit den Informationen selbst zu tun. In der Tat ist es nicht allzu unwahrscheinlich, dass Sie mehrere Implementierungen dieser Methoden wünschen , z. B. eine detaillierte Erklärung für das Management und eine nicht so detaillierte Erklärung für die Aktionäre. Durch die Aufteilung dieser unabhängigen Funktionen in verschiedene Klassen können Sie zur Laufzeit verschiedene Implementierungen auswählen, ohne die Last einer einheitlichen print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
Methode. Yuck!
Hoffentlich können Sie sehen, wo die oben beschriebene, massiv parametrisierte Methode schief geht und wo die einzelnen Implementierungen richtig laufen. Im Fall eines einzelnen Objekts müssen Sie jedes Mal, wenn Sie der Drucklogik eine neue Falte hinzufügen, Ihr Domänenmodell ändern ( Tim in Finance möchte Seitenzahlen, aber nur im internen Bericht, können Sie das hinzufügen? ) Fügen Sie stattdessen einfach einer oder zwei Satellitenklassen eine Konfigurationseigenschaft hinzu.
Bei der ordnungsgemäßen Implementierung des SRP geht es darum, Abhängigkeiten zu verwalten . Kurz gesagt, wenn eine Klasse bereits etwas Nützliches tut und Sie überlegen, eine andere Methode hinzuzufügen, die eine neue Abhängigkeit einführt (z. B. eine Benutzeroberfläche, einen Drucker, ein Netzwerk, eine Datei usw.), tun Sie dies nicht . Überlegen Sie, wie Sie diese Funktionalität stattdessen in eine neue Klasse einfügen und wie Sie diese neue Klasse in Ihre Gesamtarchitektur einfügen können (es ist ziemlich einfach, wenn Sie sich mit Abhängigkeitsinjektion befassen). Das ist das allgemeine Prinzip / der allgemeine Prozess.
Randnotiz: Wie Robert lehne ich offen die Vorstellung ab, dass eine SRP-kompatible Klasse nur eine oder zwei Zustandsvariablen haben sollte. Von solch einer dünnen Hülle konnte selten erwartet werden, dass sie etwas wirklich Nützliches bewirkt. Gehen Sie also nicht über Bord.