Klären Sie das Prinzip der Einzelverantwortung

64

Das Prinzip der Einzelverantwortung besagt, dass eine Klasse nur eines tun sollte. Einige Fälle sind ziemlich eindeutig. Andere sind jedoch schwierig, weil das, was auf einer bestimmten Abstraktionsebene als "eine Sache" betrachtet wird, auf einer niedrigeren Ebene mehrere Dinge sein kann. Ich befürchte auch, dass, wenn das Prinzip der Einzelverantwortung auf den niedrigeren Ebenen eingehalten wird, dies zu einem übermäßig entkoppelten, ausführlichen Ravioli-Code führen kann, bei dem mehr Zeilen für die Erstellung winziger Klassen für alles und für die Installation von Informationen verwendet werden, als zur tatsächlichen Lösung des vorliegenden Problems.

Wie würden Sie beschreiben, was "eins" bedeutet? Was sind einige konkrete Anzeichen dafür, dass eine Klasse wirklich mehr als "eine Sache" tut?

dsimcha
quelle
6
+1 für übertriebenen "Ravioli-Code". Zu Beginn meiner Karriere war ich einer der Menschen, die es zu weit gebracht haben. Nicht nur mit Klassen, sondern auch mit Methodenmodularisierung. Mein Code war gespickt mit Tonnen kleiner Methoden, die etwas Einfaches taten, nur um ein Problem in kleine Teile aufzuteilen, die auf den Bildschirm passen, ohne zu scrollen. Offensichtlich ging das oft viel zu weit.
Bobby Tables

Antworten:

50

Mir gefällt die Art und Weise, wie Robert C. Martin (Onkel Bob) das Prinzip der Einzelverantwortung (verknüpft mit PDF) neu formuliert :

Es sollte nie mehr als einen Grund geben, warum sich eine Klasse ändert

Es unterscheidet sich auf subtile Weise von der traditionellen Definition "sollte nur eine Sache tun", und das gefällt mir, weil es Sie dazu zwingt, die Art und Weise, wie Sie über Ihre Klasse denken, zu ändern. Anstatt darüber nachzudenken, ob dies eine Sache ist, überlegen Sie, was sich ändern kann und wie sich diese Änderungen auf Ihre Klasse auswirken. Wenn sich also beispielsweise die Datenbank ändert, muss sich Ihre Klasse ändern? Was passiert, wenn sich das Ausgabegerät ändert (z. B. ein Bildschirm, ein mobiles Gerät oder ein Drucker)? Wenn sich Ihre Klasse aufgrund von Änderungen aus vielen anderen Richtungen ändern muss, ist dies ein Zeichen dafür, dass Ihre Klasse zu viele Verantwortlichkeiten hat.

In dem verlinkten Artikel kommt Onkel Bob zu dem Schluss:

Die SRP ist eines der einfachsten Prinzipien und eines der am schwierigsten zu korrigierenden. Das Zusammenführen von Verantwortlichkeiten ist eine Selbstverständlichkeit. Das Auffinden und Trennen dieser Verantwortlichkeiten voneinander ist ein wesentlicher Aspekt des Softwaredesigns.

Paddyslacker
quelle
2
Mir gefällt die Art und Weise, wie er es auch ausdrückt, es scheint leichter zu überprüfen, wenn es um Veränderungen geht, als um die abstrakte "Verantwortung".
Matthieu M.
Das ist eigentlich eine ausgezeichnete Art und Weise, es auszudrücken. Ich liebe das. Normalerweise neige ich dazu, die SRP als viel stärker auf Methoden anwendend zu betrachten. Manchmal muss eine Klasse nur zwei Dinge tun (vielleicht ist es die Klasse, die zwei Domänen überbrückt), aber eine Methode sollte fast nie mehr tun, als Sie kurz und bündig durch ihre Typensignatur beschreiben können.
CodexArcanum
1
Ich habe das gerade meiner Grad gezeigt - großartige Lektüre und eine verdammt gute Erinnerung an mich.
Martijn Verburg
1
Ok, das macht eine Menge Sinn, wenn Sie es mit der Idee kombinieren, dass nicht überentwickelter Code nur Änderungen einplanen sollte, die in absehbarer Zukunft wahrscheinlich sind, nicht für jede mögliche Änderung. Ich würde das also ein wenig wiederholen, da "es nur einen Grund für einen Klassenwechsel geben sollte, der in absehbarer Zukunft wahrscheinlich ist". Dies fördert die Einfachheit in Teilen des Designs, die sich wahrscheinlich nicht ändern, und die Entkopplung in Teilen, die sich wahrscheinlich ändern.
Dsimcha
18

Ich frage mich immer wieder, welches Problem versucht SRP zu lösen? Wann hilft mir SRP? Folgendes habe ich mir ausgedacht:

Sie sollten Verantwortlichkeit / Funktionalität aus einer Klasse heraus umgestalten, wenn:

1) Sie haben die Funktionalität (DRY) dupliziert

2) Sie stellen fest, dass Ihr Code eine andere Abstraktionsebene benötigt, damit Sie ihn besser verstehen können (KISS).

3) Sie stellen fest, dass Teile der Funktionalität von Ihren Domain-Experten als Teil einer anderen Komponente verstanden werden (Ubiquitous Language).

Du SOLLST Verantwortung aus einer Klasse heraus nicht umgestalten, wenn:

1) Es gibt keine doppelte Funktionalität.

2) Die Funktionalität ist außerhalb des Kontextes Ihrer Klasse nicht sinnvoll. Anders ausgedrückt, Ihre Klasse bietet einen Kontext, in dem die Funktionalität leichter zu verstehen ist.

3) Ihre Domain-Experten haben keine Vorstellung von dieser Verantwortung.

Mir fällt ein, dass wir, wenn SRP allgemein angewendet wird, eine Art von Komplexität (bei dem Versuch, Kopf oder Zahl einer Klasse mit viel zu viel Inhalt zu machen) mit einer anderen Art (bei dem Versuch, alle Mitarbeiter / Ebenen von zusammenzuhalten) austauschen Abstraktion gerade, um herauszufinden, was diese Klassen tatsächlich tun).

Wenn Sie Zweifel haben, lassen Sie es aus! Sie können später jederzeit umgestalten, wenn ein klarer Grund dafür vorliegt.

Was denkst du?

Brandon
quelle
Obwohl diese Richtlinien hilfreich sein können oder auch nicht, hat sie doch nichts mit der in SOLID definierten SRP zu tun, oder?
Sara
Dankeschön. Ich kann nicht glauben, wie wahnsinnig die sogenannten SOLID-Prinzipien sind, bei denen sehr einfacher Code ohne guten Grund hundertmal komplexer ist . Die von Ihnen angegebenen Punkte beschreiben die tatsächlichen Gründe für die Implementierung von SRP. Ich denke, Prinzipien, die Sie oben angegeben haben, sollten ihr eigenes Akronym werden und "SOLID" rauswerfen, es schadet mehr als es nützt. "Architektur-Astronauten" in der Tat, wie Sie in dem Thread betont haben , der mich hierher führte.
Nicholas Petersen
4

Ich weiß nicht, ob es eine objektive Skala gibt, aber was dies verraten würde, wären die Methoden - nicht so sehr die Anzahl von ihnen, sondern die Vielfalt ihrer Funktion. Ich bin damit einverstanden, dass Sie die Zersetzung zu weit treiben können, aber ich würde keine strengen Regeln befolgen.

Jeremy
quelle
4

Die Antwort liegt in der Definition

Was Sie als Verantwortung definieren , gibt Ihnen letztendlich die Grenze.

Beispiel:

Ich habe eine Komponente, die für die Anzeige von Rechnungen verantwortlich ist -> in diesem Fall verstoße ich gegen das Prinzip, wenn ich anfange, mehr hinzuzufügen.

Wenn ich auf der anderen Seite die Verantwortung für den Umgang mit Rechnungen sagte -> Hinzufügen mehrerer kleinerer Funktionen (z. B. Drucken von Rechnungen , Aktualisieren von Rechnungen ), liegen alle innerhalb dieser Grenzen.

Allerdings , wenn das Modul zu handhaben begonnen jede Funktionalität außerhalb von Rechnungen , dann wäre es außerhalb dieser Grenze liegen.

Dunkle Nacht
quelle
Ich denke, das Hauptproblem ist hier das Wort "Handhabung". Es ist zu allgemein, um eine einzelne Verantwortung zu definieren. Ich denke, es wäre besser, eine Komponente für die Druckrechnung und eine andere für die Aktualisierungsrechnung zu haben, als eine einzelne Komponente (Drucken, Aktualisieren und - warum nicht? - unabhängig von der Rechnung anzeigen .
Machado
1
Das OP fragt im Wesentlichen: "Wie definieren Sie eine Verantwortung?" Wenn Sie also sagen, dass die Verantwortung das ist, was Sie definieren, scheint es, als würde die Frage nur wiederholt.
Despertar
2

Ich betrachte es immer auf zwei Ebenen:

  • Ich stelle sicher, dass meine Methoden nur eines tun und es gut machen
  • Ich sehe eine Klasse als eine logische (OO) Gruppierung dieser Methoden, die eine Sache gut repräsentiert

Also so etwas wie ein Domain-Objekt namens Dog:

Dogist meine Klasse, aber Hunde können viele Dinge! Ich könnte Methoden wie walk(), run()und bite(DotNetDeveloper spawnOfBill)(sorry konnte nicht widerstehen ; p).

Wenn es Dogzu unhandlich wird, würde ich darüber nachdenken, wie Gruppen dieser Methoden in einer anderen Klasse, z. B. einer MovementKlasse, die meine walk()und run()Methoden enthalten könnte, zusammen modelliert werden können .

Es gibt keine feste Regel, Ihr OO-Design wird sich im Laufe der Zeit weiterentwickeln. Ich versuche, eine klare Schnittstelle / öffentliche API sowie einfache Methoden zu verwenden, die eine Sache und eine Sache gut machen.

Martijn Verburg
quelle
Bite sollte wirklich eine Instanz von Object enthalten, und ein DotNetDeveloper sollte eine Unterklasse von Person sein (normalerweise jedenfalls!)
Alan Pearce
@ Alan - Dort - das für Sie behoben :-)
Martijn Verburg
1

Ich sehe es mehr nach dem Vorbild einer Klasse sollte nur repräsentieren eine Sache. Zur Gewährleistung eines angemessenen @ Karianna des Beispiels habe ich meine DogKlasse, die Methoden für das hat walk(), run()und bark(). Ich werde hinzuzufügen Methoden nicht für meaow(), squeak(), slither()oder fly()weil die sind nicht Dinge , die Hunde. Sie sind Dinge, die andere Tiere tun, und diese anderen Tiere hätten ihre eigenen Klassen, um sie darzustellen.

(Übrigens, wenn Ihr Hund nicht fliegen, dann sollten Sie vielleicht aufhören , ihn aus dem Fenster zu werfen).

JohnL
quelle
+1 für "Wenn Ihr Hund fliegt, sollten Sie wahrscheinlich aufhören, ihn aus dem Fenster zu werfen". :)
Bobby Tables
Abgesehen von der Frage, was die Klasse darstellen soll, was repräsentiert eine Instanz ? Betrachtet man SeesFoodals Eigenschaft DogEyes, Barkals etwas von einem getan DogVoice, und Eatals etwas durch eine getan DogMouth, dann wie Logik if (dog.SeesFood) dog.Eat(); else dog.Bark();wird worden if (eyes.SeesFood) mouth.Eat(); else voice.Bark();, jeden Sinn für Identität zu verlieren , die Augen, den Mund und Stimme alle mit einem einzigen entitity werden.
Supercat
@supercat es ist ein fairer Punkt, obwohl der Kontext wichtig ist. Wenn sich der von Ihnen erwähnte Code in der DogKlasse befindet, hängt er wahrscheinlich damit zusammen Dog. Wenn nicht, dann würden Sie wahrscheinlich myDog.Eyes.SeesFoodeher mit so etwas als nur enden eyes.SeesFood. Eine andere Möglichkeit besteht darin, dass Dogdie ISeeSchnittstelle verfügbar gemacht wird , die die Dog.EyesEigenschaft und die SeesFoodMethode erfordert .
JohnL
@JohnL: Wenn die eigentliche Sehmechanik von den Augen eines Hundes gehandhabt wird, im Wesentlichen wie von Katzen oder Zebras, ist es möglicherweise sinnvoll, die Mechanik von einer EyeKlasse handhaben zu lassen , aber ein Hund sollte "sehen". mit den Augen, anstatt nur Augen zu haben, die sehen können. Ein Hund ist kein Auge, aber auch kein einfacher Augenhalter. Es ist ein "Ding, das sehen kann [zumindest versucht, es zu sehen]" und sollte über die Schnittstelle als solches beschrieben werden. Selbst ein blinder Hund kann gefragt werden, ob er Futter sieht. es wird nicht sehr nützlich sein, da der Hund immer "nein" sagt, aber es schadet nicht zu fragen.
Supercat
Dann verwenden Sie die ISee-Oberfläche, wie ich sie in meinem Kommentar beschreibe.
JohnL
1

Eine Klasse sollte eine Sache tun, wenn sie auf ihrer eigenen Abstraktionsebene betrachtet wird. Es wird zweifellos viele Dinge auf einer weniger abstrakten Ebene tun. So arbeiten Klassen, um Programme wartungsfreundlicher zu machen: Sie verbergen Implementierungsdetails, wenn Sie sie nicht genau untersuchen müssen.

Ich benutze Klassennamen als Test dafür. Wenn ich einer Klasse keinen ziemlich kurzen beschreibenden Namen geben kann oder wenn dieser Name ein Wort wie "Und" enthält, verstoße ich wahrscheinlich gegen das Prinzip der Einzelverantwortung.

Nach meiner Erfahrung ist es einfacher, dieses Prinzip auf den unteren Ebenen zu halten, wo die Dinge konkreter sind.

David Thornley
quelle
0

Es geht darum, eine einzige Rolle zu haben .

Jede Klasse sollte mit einem Rollennamen fortgesetzt werden. Eine Rolle ist in der Tat eine (Menge von) Verb (en), die einem Kontext zugeordnet sind.

Beispielsweise :

Datei bietet Zugriff auf eine Datei. FileManager verwaltet Dateiobjekte.

Ressourcenhaltedaten für eine Ressource aus einer Datei. ResourceManager halten und alle Ressourcen bereitstellen.

Hier können Sie sehen, dass einige Verben wie "verwalten" eine Reihe anderer Verben implizieren. Verben allein sind die meiste Zeit besser als Funktionen gedacht als Klassen. Wenn das Verb zu viele Aktionen impliziert, die einen eigenen gemeinsamen Kontext haben, sollte es eine Klasse für sich sein.

Die Idee besteht also nur darin, Ihnen eine einfache Vorstellung davon zu geben, was die Klasse macht, indem Sie eine eindeutige Rolle definieren, die das Zusammenwirken mehrerer Unterrollen sein kann (die von Mitgliedsobjekten oder anderen Objekten ausgeführt werden).

Ich erstelle oft Manager-Klassen, die mehrere andere Klassen enthalten. Wie eine Fabrik, eine Kanzlei usw. Sehen Sie eine Managerklasse wie eine Art Gruppenleiter, einen Orchesterkapitän, der andere Völker dazu anleitet, zusammenzuarbeiten, um eine hochrangige Idee zu erreichen. Er hat eine Rolle, impliziert aber die Arbeit mit anderen einzigartigen Rollen. Sie können es auch so sehen, wie ein Unternehmen organisiert ist: Ein CEO ist nicht produktiv auf der Ebene der reinen Produktivität, aber wenn er nicht da ist, kann nichts richtig zusammenarbeiten. Das ist seine Rolle.

Identifizieren Sie beim Entwerfen eindeutige Rollen. Überprüfen Sie für jede Rolle erneut, ob sie nicht in mehrere andere Rollen unterteilt werden kann. Wenn Sie auf diese Weise die Art und Weise ändern möchten, in der Ihr Manager Objekte erstellt, ändern Sie einfach die Factory, und achten Sie auf Ruhe.

Klaim
quelle
-1

Bei der SRP geht es nicht nur um die Aufteilung von Klassen, sondern auch um die Delegierung von Funktionen.

Verwenden Sie in dem oben verwendeten Hundebeispiel SRP nicht als Begründung für die Verwendung von drei separaten Klassen wie DogBarker, DogWalker usw. (geringe Kohäsion). Schauen Sie sich stattdessen die Implementierung der Methoden einer Klasse an und entscheiden Sie, ob sie "zu viel wissen". Sie können dog.walk () weiterhin verwenden, aber wahrscheinlich sollte die walk () -Methode die Details zum Ausführen des Gehens an eine andere Klasse delegieren.

Tatsächlich erlauben wir der Hundeklasse einen Grund, sich zu ändern: weil sich Hunde ändern. Wenn Sie dies natürlich mit anderen SOLID-Prinzipien kombinieren, erweitern Sie Dog für neue Funktionen, anstatt Dog zu ändern (offen / geschlossen). Und Sie würden Ihre Abhängigkeiten wie IMove und IEat injizieren. Und natürlich würden Sie diese separaten Schnittstellen erstellen (Schnittstellentrennung). Dog würde sich nur ändern, wenn wir einen Bug finden würden oder wenn Dogs sich grundlegend ändern würden (Liskov Sub, Verhalten nicht erweitern und dann entfernen).

Der Nettoeffekt von SOLID besteht darin, dass wir öfter neuen Code schreiben als bestehenden Code ändern müssen, und das ist ein großer Gewinn.

user1488779
quelle
-1

Es hängt alles von der Definition der Verantwortung ab und davon, wie sich diese Definition auf die Wartung Ihres Codes auswirkt. Alles läuft auf eine Sache hinaus, und so hilft Ihnen Ihr Design bei der Pflege Ihres Codes.

Und wie jemand sagte: "Auf dem Wasser spazieren zu gehen und Software nach vorgegebenen Spezifikationen zu entwerfen ist einfach, vorausgesetzt, beide sind eingefroren."

Wenn wir also die Verantwortung konkreter definieren, müssen wir sie nicht ändern.

Manchmal sind die Verantwortlichkeiten offensichtlich, aber manchmal sind sie subtil und wir müssen uns mit Bedacht entscheiden.

Angenommen, wir fügen der Dog-Klasse catchThief () eine weitere Verantwortung hinzu. Jetzt könnte es zu einer zusätzlichen anderen Verantwortung führen. Morgen, wenn die Art und Weise, wie der Hund Dieb fängt, von der Polizei geändert werden muss, muss die Hundeklasse geändert werden. In diesem Fall ist es besser, eine weitere Unterklasse zu erstellen und diese als ThiefCathcerDog zu bezeichnen. Wenn wir uns jedoch sicher sind, dass sich dies unter keinen Umständen ändern wird oder die Art und Weise, wie catchThief implementiert wurde, von externen Parametern abhängt, ist es völlig in Ordnung, diese Verantwortung zu übernehmen. Wenn die Verantwortlichkeiten nicht sonderbar sind, müssen wir sie auf der Grundlage des Anwendungsfalls mit Bedacht entscheiden.

AKS
quelle
-1

"Ein Grund für eine Änderung" hängt davon ab, wer das System verwendet. Stellen Sie sicher, dass Sie Anwendungsfälle für jeden Akteur haben, und erstellen Sie eine Liste der wahrscheinlichsten Änderungen. Stellen Sie für jede mögliche Änderung des Anwendungsfalls sicher, dass nur eine Klasse von dieser Änderung betroffen ist. Wenn Sie einen völlig neuen Anwendungsfall hinzufügen, stellen Sie sicher, dass Sie dazu nur eine Klasse erweitern müssen.

kiwicomb123
quelle
1
Ein Grund für eine Änderung hat nichts mit der Anzahl der Anwendungsfälle, der Akteure oder der Wahrscheinlichkeit einer Änderung zu tun. Sie sollten keine Liste der wahrscheinlichen Änderungen erstellen. Es ist richtig, dass eine Änderung nur eine Klasse betreffen sollte. In der Lage zu sein, eine Klasse zu erweitern, um dieser Veränderung Rechnung zu tragen, ist gut, aber das ist das Open-Closed-Prinzip, nicht das SRP.
candied_orange
Warum sollten wir keine Liste der wahrscheinlichsten Änderungen erstellen? Spekulatives Design?
Kiwicomb123
weil es nicht hilft, nicht vollständig ist und es effektivere Möglichkeiten gibt, mit Veränderungen umzugehen, als zu versuchen, sie vorherzusagen. Erwarten Sie es einfach. Isolieren Sie Entscheidungen und die Auswirkungen sind minimal.
candied_orange
Okay, ich verstehe, es verstößt gegen das Prinzip "Du wirst es nicht brauchen".
Kiwicomb123
Eigentlich kann Yagni auch zu weit gedrängt werden. Es soll Sie wirklich davon abhalten, spekulative Use Cases zu implementieren. Dies soll Sie nicht davon abhalten, einen implementierten Anwendungsfall zu isolieren.
candied_orange