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?
quelle
Antworten:
Mir gefällt die Art und Weise, wie Robert C. Martin (Onkel Bob) das Prinzip der Einzelverantwortung (verknüpft mit PDF) neu formuliert :
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:
quelle
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?
quelle
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.
quelle
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.
quelle
Ich betrachte es immer auf zwei Ebenen:
Also so etwas wie ein Domain-Objekt namens Dog:
Dog
ist meine Klasse, aber Hunde können viele Dinge! Ich könnte Methoden wiewalk()
,run()
undbite(DotNetDeveloper spawnOfBill)
(sorry konnte nicht widerstehen ; p).Wenn es
Dog
zu unhandlich wird, würde ich darüber nachdenken, wie Gruppen dieser Methoden in einer anderen Klasse, z. B. einerMovement
Klasse, die meinewalk()
undrun()
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.
quelle
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
Dog
Klasse, die Methoden für das hatwalk()
,run()
undbark()
. Ich werde hinzuzufügen Methoden nicht fürmeaow()
,squeak()
,slither()
oderfly()
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).
quelle
SeesFood
als EigenschaftDogEyes
,Bark
als etwas von einem getanDogVoice
, undEat
als etwas durch eine getanDogMouth
, dann wie Logikif (dog.SeesFood) dog.Eat(); else dog.Bark();
wird wordenif (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.Dog
Klasse befindet, hängt er wahrscheinlich damit zusammenDog
. Wenn nicht, dann würden Sie wahrscheinlichmyDog.Eyes.SeesFood
eher mit so etwas als nur endeneyes.SeesFood
. Eine andere Möglichkeit besteht darin, dassDog
dieISee
Schnittstelle verfügbar gemacht wird , die dieDog.Eyes
Eigenschaft und dieSeesFood
Methode erfordert .Eye
Klasse 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.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.
quelle
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.
quelle
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.
quelle
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.
quelle
"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.
quelle