Welche praktischen Techniken werden verwendet, um zu überprüfen, ob eine Klasse gegen das Prinzip der Einzelverantwortung verstößt?
Ich weiß, dass eine Klasse nur einen Grund haben sollte, sich zu ändern, aber diesem Satz fehlt ein praktischer Weg, dies wirklich umzusetzen.
Der einzige Weg , ich fand , ist , den Satz zu verwenden „Der ......... sollte sich ..........“ Dabei ist das erste Leerzeichen der Klassenname und das spätere der Name der Methode (Verantwortung).
Manchmal ist es jedoch schwierig herauszufinden, ob eine Verantwortung wirklich gegen die SRP verstößt.
Gibt es weitere Möglichkeiten, um nach dem SRP zu suchen?
Hinweis:
Die Frage ist nicht, was die SRP bedeutet, sondern eine praktische Methodik oder eine Reihe von Schritten zur Überprüfung und Implementierung der SRP.
AKTUALISIEREN
Ich habe eine Beispielklasse hinzugefügt, die eindeutig gegen die SRP verstößt. Es wäre großartig, wenn die Menschen anhand dieses Beispiels erklären könnten, wie sie sich dem Prinzip der Einzelverantwortung nähern.
Das Beispiel ist von hier .
Antworten:
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 alsprintReport
. 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ürdeIPrintable<T>
. Verfügt über eine SchlüsselmethodePrint(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ügenIExportable<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 verwendenIPrintable<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 dieIncomeStatement
( 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 habengetTotalRevenues()
,getTotalExpenses()
undgetNetIncome()
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
undprint
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 einheitlichenprint(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.
quelle
IncomeStatement
. Gibt es in Ihrer vorgeschlagene Konstruktion bedeutet , dass dieIncomeStatement
Instanzen habenIncomeStatementPrinter
undIncomeStatementRenderer
so , dass , wenn ich rufeprint()
aufIncomeStatement
den Anruf wird delegierenIncomeStatementPrinter
statt?IncomeStatement
Klasse nicht hat eineprint
Methode oder einformat
Verfahren oder eine andere Methode , die nicht direkt mit Kontrolle oder Manipulation der Berichtsdaten selbst befasst. Dafür sind diese anderen Klassen da. Wenn Sie eine drucken möchten, übernehmen Sie eine Abhängigkeit von derIPrintable<IncomeStatement>
Schnittstelle, die im Container registriert ist.Printer
Instanz in dieIncomeStatement
Klasse einfüge? So wie ich es mir vorstelle, wirdIncomeStatement.print()
es delegiert, wenn ich es anrufeIncomeStatementPrinter.print(this, format)
. Was ist falsch an diesem Ansatz? ... Eine weitere Frage, die Sie erwähnt haben undIncomeStatement
die Informationen enthalten sollte, die in formatierten Berichten angezeigt werden, wenn ich möchte, dass sie aus der Datenbank oder aus einer XML-Datei gelesen werden, sollte ich die Methode extrahieren, mit der die Daten geladen werden in eine separate Klasse und delegieren Sie den Anruf an sie inIncomeStatement
?IncomeStatementPrinter
je nachIncomeStatement
undIncomeStatement
je nachIncomeStatementPrinter
. Das ist eine zyklische Abhängigkeit. Und es ist nur schlechtes Design; Es gibt überhaupt keinen GrundIncomeStatement
, etwas über einPrinter
oder zu wissenIncomeStatementPrinter
- es ist ein Domänenmodell, es befasst sich nicht mit dem Drucken, und die Delegierung ist sinnlos, da jede andere Klasse ein erstellen oder erwerben kannIncomeStatementPrinter
. Es gibt keinen guten Grund, im Domain-Modell eine Vorstellung vom Drucken zu haben.IncomeStatement
aus der Datenbank (oder XML-Datei) laden - normalerweise wird dies von einem Repository und / oder Mapper verwaltet, nicht von der Domäne, und Sie delegieren dies wiederum nicht in der Domäne. Wenn eine andere Klasse eines dieser Modelle lesen muss, fragt sie explizit nach diesem Repository . Es sei denn, Sie implementieren das Active Record-Muster, aber ich bin wirklich kein Fan.Ich überprüfe die SRP, indem ich jede Methode (Verantwortung) einer Klasse überprüfe und die folgende Frage stelle:
"Muss ich jemals die Art und Weise ändern, wie ich diese Funktion implementiere?"
Wenn ich eine Funktion finde, die ich auf unterschiedliche Weise implementieren muss (abhängig von einer Konfiguration oder Bedingung), weiß ich mit Sicherheit, dass ich eine zusätzliche Klasse benötige, um diese Verantwortung zu übernehmen.
quelle
Hier ist ein Zitat aus Regel 8 der Objektkalisthenik :
Angesichts dieser (etwas idealistischen) Ansicht könnte man sagen, dass jede Klasse, die nur eine oder zwei Zustandsvariablen enthält, die SRP wahrscheinlich nicht verletzt. Sie können auch sagen, dass jede Klasse, die mehr als zwei Statusvariablen enthält, die SRP verletzen kann .
quelle
Eine mögliche Implementierung (in Java). Ich habe mir bei den Rückgabetypen Freiheiten genommen, aber insgesamt denke ich, dass dies die Frage beantwortet. TBH Ich denke nicht, dass die Schnittstelle zur Report-Klasse so schlecht ist, obwohl ein besserer Name angebracht sein könnte. Der Kürze halber habe ich Wachaussagen und Behauptungen ausgelassen.
EDIT: Beachten Sie auch, dass die Klasse unveränderlich ist. Sobald es erstellt ist, können Sie nichts mehr ändern. Sie könnten einen setFormatter () und einen setPrinter () hinzufügen und nicht zu viel Ärger bekommen. Der Schlüssel, IMHO, besteht darin, die Rohdaten nach der Instanziierung nicht zu ändern.
quelle
if (reportData == null)
, die Sie vermutlichdata
stattdessen meinen . Zweitens hatte ich gehofft zu wissen, wie Sie zu dieser Implementierung gekommen sind. Zum Beispiel, warum Sie beschlossen haben, alle Aufrufe stattdessen an andere Objekte zu delegieren. Eine weitere Sache, über die ich mich immer gewundert habe: Ist es wirklich die Verantwortung eines Berichts, sich selbst zu drucken?! Warum haben Sie keine separateprinter
Klasse erstellt, die einenreport
Konstruktor enthält?Printer
Klasse, die einen Bericht oder eineReport
Klasse, die einen Drucker nimmt? Ich bin zuvor auf ein ähnliches Problem gestoßen, bei dem ich einen Bericht analysieren musste, und habe mit meiner TL darüber gestritten, ob wir einen Parser erstellen sollten, der einen Bericht enthält, oder ob der Bericht einen Parser enthalten sollte und derparse()
Aufruf an ihn delegiert wird.In Ihrem Beispiel ist nicht klar, ob SRP verletzt wird. Vielleicht sollte der Bericht in der Lage sein, sich selbst zu formatieren und zu drucken, wenn sie relativ einfach sind:
Die Methoden sind so einfach , es keinen Sinn zu haben , macht
ReportFormatter
oderReportPrinter
Klassen. Das einzige eklatante Problem in der Benutzeroberfläche besteht darin,getReportData
dass es gegen ask not tell für nicht wertvolle Objekte verstößt.Wenn andererseits die Methoden sehr kompliziert sind oder es viele Möglichkeiten gibt, eine zu formatieren oder zu drucken,
Report
ist es sinnvoll, die Verantwortung zu delegieren (auch testbarer):SRP ist ein Entwurfsprinzip, kein philosophisches Konzept, und basiert daher auf dem tatsächlichen Code, mit dem Sie arbeiten. Semantisch können Sie eine Klasse in beliebig viele Verantwortlichkeiten unterteilen oder gruppieren. Aus praktischen Gründen sollte SRP Ihnen jedoch dabei helfen, den Code zu finden, den Sie ändern müssen . Anzeichen dafür, dass Sie gegen SRP verstoßen, sind:
Sie können diese Probleme durch Refactoring beheben, indem Sie Namen verbessern, ähnlichen Code gruppieren, Doppelarbeit vermeiden, ein mehrschichtiges Design verwenden und Klassen nach Bedarf aufteilen / kombinieren. Der beste Weg, um SRP zu lernen, besteht darin, in eine Codebasis einzutauchen und den Schmerz zu beseitigen.
quelle
Printer
Klasse, die einen Bericht oder eineReport
Klasse, die einen Drucker nimmt? Oft stehe ich vor einer solchen Designfrage, bevor ich herausfinde, ob sich der Code als komplex herausstellt oder nicht.Das Prinzip der Einzelverantwortung ist eng mit dem Begriff des Zusammenhalts verbunden . Um eine sehr zusammenhängende Klasse zu haben, müssen Sie eine Abhängigkeit zwischen den Instanzvariablen der Klasse und ihren Methoden haben. Das heißt, jede der Methoden sollte so viele Instanzvariablen wie möglich bearbeiten. Je mehr Variablen eine Methode verwendet, desto kohärenter ist ihre Klasse. Ein maximaler Zusammenhalt ist normalerweise nicht erreichbar.
Um SRP gut anzuwenden, verstehen Sie auch die Geschäftslogikdomäne gut. zu wissen, was jede Abstraktion tun sollte. Die geschichtete Architektur hängt auch mit SRP zusammen, indem jede Schicht eine bestimmte Aufgabe ausführt (die Datenquellenschicht sollte Daten bereitstellen usw.).
Zurück zum Zusammenhalt, auch wenn Ihre Methoden nicht alle Variablen verwenden, sollten sie gekoppelt werden:
Sie sollten nicht so etwas wie den folgenden Code haben, bei dem ein Teil der Instanzvariablen in einem Teil der Methoden und der andere Teil der Variablen im anderen Teil der Methoden verwendet wird (hier sollten Sie zwei Klassen für haben jeder Teil der Variablen).
quelle