Stellen Sie sicher, dass jede Klasse nur eine Verantwortung hat. Warum?

37

Laut Microsoft-Dokumentation, dem Wikipedia SOLID-Hauptartikel oder den meisten IT-Architekten müssen wir sicherstellen, dass jede Klasse nur eine Verantwortung hat. Ich würde gerne wissen warum, denn wenn alle mit dieser Regel einverstanden zu sein scheinen, scheint sich niemand über die Gründe dieser Regel einig zu sein.

Einige geben eine bessere Wartung an, andere sagen, dass dies einfache Tests ermöglicht oder die Klasse robuster oder sicherer macht. Was ist richtig und was bedeutet es eigentlich? Warum macht es die Wartung besser, das Testen einfacher oder den Code robuster?

Bastien Vandamme
quelle
1
Ich hatte eine Frage, die Sie vielleicht auch im Zusammenhang finden: Was ist die eigentliche Verantwortung einer Klasse?
Pierre Arlaud
3
Können nicht alle Gründe für eine einzelne Verantwortung richtig sein? Dies erleichtert die Wartung. Dies erleichtert das Testen. Dies macht die Klasse (im Allgemeinen) robuster.
Martin York
2
Aus dem gleichen Grund haben Sie Klassen überhaupt.
Davor Ždralo
2
Ich hatte immer ein Problem mit dieser Aussage. Es ist sehr schwierig zu definieren, was eine "einzelne Verantwortung" ist. Eine einzelne Verantwortung kann von "Verifizieren Sie, dass 1 + 1 = 2 ist" bis zu "Führen Sie ein genaues Protokoll aller Einzahlungen auf und Auszahlungen von Firmenbankkonten" reichen.
Dave Nay

Antworten:

57

Modularität. Jede anständige Sprache gibt Ihnen die Möglichkeit, Teile des Codes zusammenzufügen, aber es gibt keine allgemeine Möglichkeit, einen großen Teil des Codes zu lösen, ohne dass der Programmierer eine Operation an der Quelle vornimmt. Indem Sie viele Aufgaben in einem Codekonstrukt zusammenfassen, nehmen Sie sich und anderen die Möglichkeit, die Teile auf andere Weise zu kombinieren, und führen unnötige Abhängigkeiten ein, die dazu führen können, dass sich Änderungen an einem Teil auf die anderen auswirken.

SRP ist für Funktionen genauso anwendbar wie für Klassen, aber die gängigen OOP-Sprachen können Funktionen nur relativ schlecht zusammenfügen.

Doval
quelle
12
+1 für die Erwähnung der funktionalen Programmierung als sicherer Weg zur Erstellung von Abstraktionen.
Logc
> aber Mainstream-OOP-Sprachen sind relativ schlecht darin, Funktionen zusammenzukleben. <Vielleicht, weil sich OOP-Sprachen nicht mit Funktionen befassen, sondern mit Nachrichten.
Jeff Hubbard
@ JeffHubbard Ich denke du meinst es liegt daran, dass sie nach C / C ++ greifen. Es gibt nichts an Objekten, was sie mit Funktionen gegenseitig ausschließt. Hölle, ein Objekt / eine Schnittstelle ist nur eine Aufzeichnung / Struktur von Funktionen. Vorzugeben, dass Funktionen nicht wichtig sind, ist sowohl in der Theorie als auch in der Praxis nicht gerechtfertigt. Sie können sowieso nicht vermieden werden - am Ende müssen Sie "Runnables", "Factories", "Providers" und "Actions" herumwerfen oder die sogenannten "Patterns" der Strategie- und Template-Methode verwenden und haben dann das Ziel ein Haufen unnötiger Kochplatten, um den gleichen Job zu erledigen.
Doval
@Doval Nein, ich meine wirklich, dass OOP sich mit Nachrichten beschäftigt. Das heißt nicht, dass eine bestimmte Sprache beim Zusammenkleben von Funktionen besser oder schlechter ist als eine funktionale Programmiersprache - nur, dass dies nicht das Hauptanliegen der Sprache ist .
Jeff Hubbard
Wenn es in Ihrer Sprache darum geht, OOP einfacher / besser / schneller / stärker zu machen, ist es im Grunde nicht das, worauf Sie sich konzentrieren, ob Funktionen gut zusammenpassen oder nicht.
Jeff Hubbard
30

Bessere Wartung, einfache Tests und schnellere Fehlerbehebung sind nur (sehr angenehme) Ergebnisse der Anwendung von SRP. Der Hauptgrund (wie Robert C. Matin es ausdrückt) ist:

Eine Klasse sollte nur einen Grund haben, sich zu ändern.

Mit anderen Worten, SRP erhöht die Änderungslokalität .

SRP fördert auch DRY-Code. Solange wir Klassen haben, die nur eine Verantwortung haben, können wir sie verwenden, wo immer wir wollen. Wenn wir eine Klasse haben, die zwei Verantwortlichkeiten hat, aber nur eine von ihnen benötigt und die zweite stört, haben wir zwei Möglichkeiten:

  1. Kopieren Sie die Klasse in eine andere und erstellen Sie möglicherweise sogar eine weitere Mutante mit mehreren Verantwortlichkeiten (erhöhen Sie die technische Verschuldung ein wenig).
  2. Teilen Sie die Klasse auf und machen Sie sie so, wie es eigentlich sein sollte, was aufgrund der umfangreichen Nutzung der ursprünglichen Klasse teuer sein kann.
Maciej Chałapuk
quelle
1
+1 Für die Verknüpfung von SRP mit DRY.
Mahdi
21

Ist einfach, Code zu erstellen, um ein bestimmtes Problem zu beheben. Es ist komplizierter, Code zu erstellen, der das Problem behebt, während spätere Änderungen sicher vorgenommen werden können. SOLID bietet eine Reihe von Methoden, die den Code verbessern.

Welches ist richtig? Alle drei. Sie alle haben den Vorteil, dass Sie nur eine Verantwortung tragen und den Grund, warum Sie sie verwenden sollten.

Was sie bedeuten:

  • Bessere Wartung bedeutet, dass das einfacher zu ändern ist und sich nicht so oft ändert. Da es weniger Code gibt und dieser Code sich auf etwas Bestimmtes konzentriert, muss die Klasse nicht geändert werden, wenn Sie etwas ändern müssen, das nicht mit der Klasse zusammenhängt. Wenn Sie die Klasse ändern müssen, müssen Sie sich nur um diese Klasse und um nichts anderes kümmern, solange Sie die öffentliche Schnittstelle nicht ändern müssen.
  • Einfaches Testen bedeutet weniger Tests, weniger Setup. Die Klasse enthält nicht so viele bewegliche Teile, sodass die Anzahl möglicher Fehler in der Klasse geringer ist und Sie daher weniger Fälle testen müssen. Es müssen weniger private Bereiche / Mitglieder eingerichtet werden.
  • Aufgrund der beiden oben genannten Faktoren erhalten Sie eine Klasse, die sich weniger ändert und weniger fehlschlägt und daher robuster ist.

Versuchen Sie, Code für eine Weile nach dem Prinzip zu erstellen, und wiederholen Sie diesen Code später, um einige Änderungen vorzunehmen. Sie werden den enormen Vorteil sehen, den es bietet.

Sie tun dasselbe für jede Klasse und erhalten am Ende mehr Klassen, die alle der SRP entsprechen. Dies macht die Struktur unter dem Gesichtspunkt der Verbindungen komplexer, aber die Einfachheit jeder Klasse rechtfertigt dies.

Miyamoto Akira
quelle
Ich denke nicht, dass die hier gemachten Punkte gerechtfertigt sind. Wie vermeidet man insbesondere Nullsummenspiele, bei denen die Vereinfachung einer Klasse dazu führt, dass andere Klassen komplizierter werden, ohne dass sich dies netto auf die Codebasis insgesamt auswirkt? Ich glaube, die Antwort von @ Doval behebt dieses Problem.
2
Sie tun dasselbe für alle Klassen. Sie beenden mit mehr Klassen, die alle der SRP entsprechen. Dies macht die Struktur unter dem Gesichtspunkt der Verbindungen komplexer. Aber die Einfachheit in jeder Klasse rechtfertigt es. Ich mag die Antwort von @ Doval.
Miyamoto Akira
Ich denke, dass Sie das zu Ihrer Antwort hinzufügen sollten.
4

Die folgenden Argumente stützen meines Erachtens die Behauptung, dass das Prinzip der einfachen Verantwortung eine gute Praxis ist. Ich biete auch Links zu weiterer Literatur, in der Sie noch detailliertere Begründungen lesen können - und die beredter sind als meine:

  • Bessere Wartung : Im Idealfall muss immer dann, wenn sich eine Funktionalität des Systems ändern muss, nur eine Klasse geändert werden. Eine klare Zuordnung zwischen Klassen und Verantwortlichkeiten bedeutet, dass jeder am Projekt beteiligte Entwickler identifizieren kann, um welche Klasse es sich handelt. (Wie @ MaciejChałapuk bemerkt hat, siehe Robert C. Martin, "Clean Code" .)

  • Einfacheres Testen : Im Idealfall sollten Klassen eine möglichst minimale öffentliche Schnittstelle haben, und Tests sollten nur diese öffentliche Schnittstelle behandeln. Wenn Sie nicht klar testen können, weil viele Teile Ihrer Klasse privat sind, ist dies ein klares Zeichen dafür, dass Ihre Klasse zu viele Verantwortlichkeiten hat und dass Sie sie in kleinere Unterklassen aufteilen sollten. Bitte beachten Sie, dass dies auch für Sprachen gilt, in denen es keine "öffentlichen" oder "privaten" Klassenmitglieder gibt. Eine kleine öffentliche Schnittstelle bedeutet, dass für den Client-Code sehr klar ist, welche Teile der Klasse verwendet werden sollen. ( Weitere Informationen finden Sie in Kent Beck, "Test-Driven Development" .)

  • Robuster Code : Ihr Code schlägt nicht mehr oder weniger häufig fehl, weil er gut geschrieben ist. Wie bei jedem Code ist es jedoch das ultimative Ziel, nicht mit der Maschine , sondern mit anderen Entwicklern zu kommunizieren (siehe Kent Beck, "Implementierungsmuster" , Kapitel 1.). Eine eindeutige Codebasis lässt sich leichter überlegen, sodass weniger Fehler auftreten und es vergeht weniger Zeit zwischen dem Erkennen und Beheben eines Fehlers.

logc
quelle
Wenn sich aus geschäftlichen Gründen etwas ändern muss, glauben Sie mir, dass Sie mit SRP mehr als eine Klasse ändern müssen. Zu sagen, dass ein Klassenwechsel nur aus einem Grund nicht dasselbe ist wie ein Klassenwechsel.
Bastien Vandamme
@ B413: Ich wollte nicht, dass eine Änderung eine einzelne Änderung einer einzelnen Klasse impliziert. Viele Teile des Systems müssen möglicherweise geändert werden, um einer einzelnen Geschäftsänderung zu entsprechen. Aber vielleicht haben Sie einen Punkt und ich hätte schreiben sollen, "wann immer sich eine Funktionalität des Systems ändern muss".
Logc
3

Es gibt eine Reihe von Gründen, aber der eine, den ich mag, ist der Ansatz, den viele der frühen UNIX-Programme verwenden: Mach eine Sache gut. Es ist schon schwer genug, das mit einer Sache zu tun, und es wird immer schwieriger, je mehr Sie versuchen, es zu tun.

Ein weiterer Grund ist die Begrenzung und Kontrolle von Nebenwirkungen. Ich liebte meine Kombination Kaffeemaschine Türöffner. Leider lief der Kaffee normalerweise über, wenn ich Besucher hatte. Ich habe vergessen, die Tür zu schließen, nachdem ich neulich Kaffee gemacht hatte und jemand hat ihn gestohlen.

Aus psychologischer Sicht können Sie immer nur wenige Dinge auf einmal im Auge behalten. Allgemeine Schätzungen sind sieben plus oder minus zwei. Wenn eine Klasse mehrere Aufgaben ausführt, müssen Sie alle gleichzeitig im Auge behalten. Dadurch können Sie nicht mehr nachverfolgen, was Sie gerade tun. Wenn eine Klasse drei Aufgaben ausführt und Sie nur eine davon ausführen möchten, können Sie möglicherweise die Übersicht verlieren, bevor Sie tatsächlich etwas mit der Klasse anfangen.

Das Durchführen mehrerer Aufgaben erhöht die Codekomplexität. Über den einfachsten Code hinaus erhöht eine zunehmende Komplexität die Wahrscheinlichkeit von Fehlern. Von diesem Standpunkt aus möchten Sie, dass Klassen so einfach wie möglich sind.

Das Testen einer Klasse, die eines tut, ist viel einfacher. Sie müssen nicht bei jedem Test überprüfen, ob das Zweite, was die Klasse tut oder nicht, passiert ist. Sie müssen die fehlerhaften Bedingungen auch nicht beheben und erneut testen, wenn einer dieser Tests fehlschlägt.

BillThor
quelle
2

Weil Software organisch ist. Die Anforderungen ändern sich ständig, sodass Sie Komponenten mit möglichst wenig Kopfschmerzen bearbeiten müssen. Wenn Sie nicht den SOLID-Grundsätzen folgen, erhalten Sie möglicherweise eine konkrete Codebasis.

Stellen Sie sich ein Haus mit einer tragenden Betonwand vor. Was passiert, wenn Sie diese Mauer ohne Unterstützung entfernen? Das Haus wird wahrscheinlich zusammenbrechen. In Software wollen wir das nicht, deshalb strukturieren wir Anwendungen so, dass Sie Komponenten leicht verschieben, austauschen oder modifizieren können, ohne viel Schaden zu verursachen.

CodeART
quelle
1

Ich folge dem Gedanken: 1 Class = 1 Job.

Unter Verwendung einer Physiologie-Analogie: Motor (neuronales System), Atmung (Lunge), Verdauung (Magen), Riechstoff (Beobachtung) usw. Jede von diesen hat eine Untergruppe von Kontrollpersonen, aber jede hat nur eine Verantwortung, ob sie zu verwalten ist die Art und Weise, wie die jeweiligen Subsysteme funktionieren, oder ob sie ein Endpunkt-Subsystem sind und nur eine Aufgabe ausführen, z. B. einen Finger heben oder einen Haarfollikel wachsen lassen.

Verwechseln Sie nicht die Tatsache, dass es sich um einen Manager handelt und nicht um einen Arbeiter. Einige Mitarbeiter werden schließlich zum Manager befördert, wenn die von ihnen ausgeführte Arbeit zu kompliziert geworden ist, als dass ein Prozess für sich allein ausreichen würde.

Der komplizierteste Teil meiner Erfahrungen besteht darin, zu wissen, wann eine Klasse als Operator, Supervisor oder Manager-Prozess zu bestimmen ist. Unabhängig davon müssen Sie die Funktionen für 1 Verantwortung (Bediener, Vorgesetzter oder Manager) beobachten und kennzeichnen.

Wenn eine Klasse / ein Objekt mehr als eine dieser Rollen ausführt, treten im gesamten Prozess Leistungsprobleme oder Prozessengpässe auf.

GoldBishop
quelle
Selbst wenn die Umsetzung der Verarbeitung von Luftsauerstoff zu Hämoglobin eine LungKlasse sein sollte, würde ich davon ausgehen, dass eine Instanz von Persondennoch Breatheund somit die PersonKlasse genügend Logik enthalten sollte, um zumindest die mit dieser Methode verbundenen Verantwortlichkeiten zu delegieren. Ich würde außerdem vorschlagen, dass eine Gesamtstruktur von miteinander verbundenen Entitäten, auf die nur ein gemeinsamer Eigentümer zugreifen kann, häufig einfacher zu beurteilen ist als eine Gesamtstruktur mit mehreren unabhängigen Zugriffspunkten.
Supercat
Ja, in diesem Fall ist die Lunge Manager, obwohl der Villi ein Supervisor wäre und der Prozess der Atmung die Arbeiterklasse wäre, um die Luftpartikel in den Blutstrom zu übertragen.
GoldBishop
@GoldBishop: Vielleicht wäre es die richtige Ansicht, zu sagen, dass eine PersonKlasse die Delegation aller Funktionen hat, die mit einer ungeheuer überkomplizierten "realen menschlichen Wesen" -Entität verbunden sind, einschließlich der Assoziation ihrer verschiedenen Teile . Von einer Entität 500 Dinge tun zu lassen ist hässlich, aber wenn das zu modellierende reale System so funktioniert, ist es möglicherweise besser, eine Klasse zu haben, die 500 Funktionen delegiert und dabei eine Identität beibehält, als alles mit disjunkten Teilen zu tun.
Supercat
@supercat Oder du könntest einfach das Ganze aufteilen und 1 Klasse mit dem nötigen Supervisor über die Unterprozesse haben. Auf diese Weise könnten Sie (theoretisch) jeden Prozess von der Basisklasse Persontrennen und trotzdem über Erfolg / Misserfolg berichten, aber nicht unbedingt den anderen beeinflussen. 500 Funktionen (obwohl als Beispiel gesetzt, wäre überbordend und nicht unterstützbar) Ich versuche, diese Art von Funktionalität abgeleitet und modular zu halten.
GoldBishop
@GoldBishop: Ich wünschte, es gäbe eine Möglichkeit, einen "kurzlebigen" Typ zu deklarieren, sodass externer Code Member aufrufen oder ihn als Parameter an die eigenen Methoden des Typs übergeben könnte, aber sonst nichts. Von einer Code Organisation Perspektive machen Klassen unterteilt Sinn und sogar außerhalb Code invoke Methoden einer Ebene nach unten mit (zB Fred.vision.lookAt(George)sinnvoll ist, aber so dass Code zu sagen someEyes = Fred.vision; someTubes = Fred.digestionscheinen eklig , da sie die Beziehung zwischen verschleiern someEyesund someTubes.
supercat
1

Gerade bei so wichtigen Prinzipien wie der Einzelverantwortung würde ich persönlich erwarten, dass es viele Gründe gibt, warum Menschen dieses Prinzip anwenden.

Einige dieser Gründe könnten sein:

  • Wartung - SRP stellt sicher, dass sich ein Verantwortungswechsel in einer Klasse nicht auf andere Verantwortungsbereiche auswirkt, wodurch die Wartung vereinfacht wird. Das liegt daran, dass, wenn jede Klasse nur eine Verantwortung hat, die an einer Verantwortung vorgenommenen Änderungen von anderen Verantwortlichkeiten isoliert sind.
  • Testen - Wenn eine Klasse eine Verantwortung hat, ist es viel einfacher, herauszufinden, wie diese Verantwortung getestet werden kann. Wenn eine Klasse mehrere Verantwortlichkeiten hat, müssen Sie sicherstellen, dass Sie die richtige testen und dass der Test nicht von anderen Verantwortlichkeiten der Klasse betroffen ist.

Beachten Sie auch, dass SRP mit einem Paket von SOLID-Prinzipien geliefert wird. Sich an SRP zu halten und den Rest zu ignorieren, ist genauso schlimm wie SRP überhaupt nicht zu tun. Sie sollten SRP also nicht alleine bewerten, sondern im Kontext aller SOLID-Prinzipien.

Euphorisch
quelle
... test is not affected by other responsibilities the class hasKönnten Sie dies bitte näher erläutern?
Mahdi
1

Der beste Weg, um die Wichtigkeit dieser Prinzipien zu verstehen, ist, die Notwendigkeit zu haben.

Als ich ein Anfänger in der Programmierung war, habe ich nicht viel über Design nachgedacht. Tatsächlich wusste ich nicht einmal, dass Designmuster existieren. Als meine Programme wuchsen, bedeutete das Ändern einer Sache das Ändern vieler anderer Dinge. Es war schwer, Fehler aufzuspüren, der Code war riesig und wiederholte sich. Es gab nicht viel von einer Objekthierarchie, die Dinge waren überall. Wenn Sie etwas Neues hinzufügen oder etwas Altes entfernen, treten in den anderen Programmteilen Fehler auf. Stelle dir das vor.

Bei kleinen Projekten spielt es vielleicht keine Rolle, aber bei großen Projekten können die Dinge sehr albtraumhaft sein. Als ich später auf die Konzepte von Designmustern stieß, sagte ich mir: "Oh ja, das hätte die Sache dann viel einfacher gemacht."

Sie können die Bedeutung von Entwurfsmustern erst dann wirklich verstehen, wenn die Notwendigkeit entsteht. Ich respektiere die Muster, weil ich aus Erfahrung sagen kann, dass sie die Code-Pflege einfach und den Code robust machen.

Wie Sie bin ich mir jedoch immer noch nicht sicher, ob es sich um ein "einfaches Testen" handelt, da ich noch nicht in das Testen von Einheiten einsteigen musste.

harsimranb
quelle
Muster sind irgendwie mit SRP verbunden, aber SRP ist beim Anwenden von Entwurfsmustern nicht erforderlich. Es ist möglich, Muster zu verwenden und SRP vollständig zu ignorieren. Wir verwenden Muster, um nichtfunktionalen Anforderungen gerecht zu werden, aber die Verwendung von Mustern ist in keinem Programm erforderlich. Prinzipien sind wichtig, um die Entwicklung weniger schmerzhaft zu machen, und (IMO) sollten zur Gewohnheit werden.
Maciej Chałapuk
Ich stimme völlig mit Ihnen. In gewissem Maße bereinigt SRP das Design und die Hierarchie von Objekten. Es ist eine gute Mentalität für einen Entwickler, sich darauf einzulassen, da der Entwickler anfängt, in Bezug auf Objekte zu denken und wie sie sein sollten. Persönlich hat sich das auch sehr positiv auf meine Entwicklung ausgewirkt.
Harsimranb
1

Die Antwort ist, wie andere betont haben, dass sie alle richtig sind und sich gegenseitig beeinflussen. Einfacher zu testen macht die Wartung einfacher macht den Code robuster macht die Wartung einfacher und so weiter ...

All dies läuft darauf hinaus, dass ein wichtiger Prinzipalcode so klein und so wenig wie nötig ist, um die Arbeit zu erledigen. Dies gilt für eine Anwendung, eine Datei oder eine Klasse genauso wie für eine Funktion. Je mehr Dinge ein Code tut, desto schwieriger ist es zu verstehen, zu warten, zu erweitern oder zu testen.

Ich denke, es kann in einem Wort zusammengefasst werden: Umfang. Achten Sie genau auf den Umfang der Artefakte. Je weniger Dinge an einem bestimmten Punkt in einer Anwendung im Umfang sind, desto besser.

Erweiterter Geltungsbereich = mehr Komplexität = mehr Möglichkeiten, Fehler zu beheben.

jmoreno
quelle
1

Wenn eine Klasse zu groß ist, ist es schwierig, sie zu pflegen, zu testen und zu verstehen. Andere Antworten haben diesen Willen abgedeckt.

Es ist möglich, dass eine Klasse mehr als eine Verantwortung ohne Probleme hat, aber Sie stoßen bei zu komplexen Klassen schnell auf Probleme.

Eine einfache Regel von „nur einer Verantwortung“ macht es jedoch einfacher zu wissen, wann Sie eine neue Klasse benötigen .

Die Definition von „Verantwortung“ ist jedoch schwierig. Dies bedeutet nicht, dass Sie „alles tun, was in der Anwendungsspezifikation angegeben ist“. Die eigentliche Fertigkeit besteht darin, das Problem in kleine Einheiten von „Verantwortung“ aufzuteilen.

Ian
quelle