Wie kann eine Klasse mehrere Methoden haben, ohne das Prinzip der einzelnen Verantwortung zu brechen?

64

Das Prinzip der Einzelverantwortung ist in Wikipedia definiert als

Das Prinzip der Einzelverantwortung ist ein Computerprogrammierungsprinzip, das besagt, dass jedes Modul, jede Klasse oder Funktion die Verantwortung über einen einzelnen Teil der von der Software bereitgestellten Funktionalität haben sollte und dass die Verantwortung vollständig von der Klasse eingekapselt werden sollte

Wenn eine Klasse nur eine einzige Verantwortung haben soll, wie kann sie dann mehr als eine Methode haben? Hätte nicht jede Methode eine andere Verantwortung, was dann bedeuten würde, dass die Klasse mehr als eine Verantwortung hätte.

Jedes Beispiel, das ich gesehen habe, um das Prinzip der Einzelverantwortung zu demonstrieren, verwendet eine Beispielklasse, die nur eine Methode hat. Es kann hilfreich sein, ein Beispiel zu sehen oder eine Erklärung für eine Klasse mit mehreren Methoden zu haben, von denen immer noch angenommen werden kann, dass sie eine Verantwortung tragen.

Gans
quelle
11
Warum eine Gegenstimme? Scheint eine ideale Frage für SE.SE; Die Person recherchierte das Thema und bemühte sich, die Frage sehr klar zu stellen. Stattdessen verdient es Gegenstimmen.
Arseni Mourzenko
19
Die Ablehnung war wahrscheinlich darauf zurückzuführen, dass diese Frage bereits mehrmals gestellt und beantwortet wurde, siehe beispielsweise softwareengineering.stackexchange.com/questions/345018/… . Meiner Meinung nach werden keine wesentlichen neuen Aspekte hinzugefügt.
Hans-Martin Mosner
9
Das ist einfach reductio ad absurdum. Wenn jeder Klasse buchstäblich nur eine Methode erlaubt wäre, dann wäre kein Programm in der Lage, mehr als eine Sache zu tun.
Darrel Hoffman
6
@ DarrelHoffman Das stimmt nicht. Wenn jede Klasse ein Funktor mit nur einer "call ()" - Methode wäre, dann hätten Sie im Grunde nur eine einfache prozedurale Programmierung mit objektorientierter Programmierung emuliert. Sie können immer noch alles tun, was Sie sonst hätten tun können, da die call () -Methode einer Klasse die call () -Methode vieler anderer Klassen aufrufen kann.
Vaelus

Antworten:

29

Die Einzelverantwortung ist möglicherweise nicht etwas, das eine einzelne Funktion erfüllen kann.

 class Location { 
     public int getX() { 
         return x;
     } 
     public int getY() { 
         return y; 
     } 
 }

Diese Klasse kann gegen das Prinzip der alleinigen Verantwortung verstoßen. Nicht weil es zwei Funktionen hat, sondern wenn der Code für getX()und getY()unterschiedliche Stakeholder befriedigen muss, die Änderungen erfordern könnten. Wenn Vizepräsident X ein Memo herumschickt, dass alle Zahlen als Gleitkommazahlen ausgedrückt werden sollen, und die Buchhaltungsdirektorin Y darauf besteht, dass alle Zahlen, die ihre Abteilungsüberprüfungen ergeben, ganze Zahlen bleiben, unabhängig davon, was Herr X für richtig hält, sollte diese Klasse dies besser haben eine einzige Vorstellung davon, wem es verantwortlich ist, weil die Dinge verwirrend werden.

Wenn SRP befolgt worden wäre, wäre klar, ob die Standortklasse zu Dingen beiträgt, denen Herr X und seine Gruppe ausgesetzt sind. Stellen Sie klar, wofür die Klasse verantwortlich ist, und Sie wissen, welche Anweisung diese Klasse beeinflusst. Wenn sich beide auf diese Klasse auswirken, wurde sie schlecht konzipiert, um die Auswirkungen von Änderungen zu minimieren. "Eine Klasse sollte nur einen Grund haben, sich zu ändern" bedeutet nicht, dass die gesamte Klasse immer nur eine Kleinigkeit tun kann. Es bedeutet, dass ich nicht in der Lage sein sollte, die Klasse anzusehen und zu sagen, dass sowohl Herr X als auch Frau Y ein Interesse an dieser Klasse haben.

Davon abgesehen. Nein, mehrere Methoden sind in Ordnung. Geben Sie ihm einen Namen, der klar macht, welche Methoden in die Klasse gehören und welche nicht.

In Onkel Bobs SRP geht es mehr um Conways Gesetz als um Curlys Gesetz . Onkel Bob befürwortet die Anwendung von Curlys Gesetz (mache eine Sache) auf Funktionen, nicht auf Klassen. SRP warnt vor Mischungsgründen, die sich gemeinsam ändern. Das Gesetz von Conway besagt, dass das System dem Informationsfluss einer Organisation folgt. Das führt dazu, dass Sie SRP folgen, weil es Ihnen egal ist, wovon Sie nie etwas hören.

"Ein Modul sollte einem und nur einem Akteur verantwortlich sein"

Robert C Martin - Saubere Architektur

Die Leute wollen immer wieder, dass SRP jeder Grund ist, den Umfang einzuschränken. Es gibt mehr Gründe, den Umfang einzuschränken als SRP. Ich schränke den Umfang weiter ein, indem ich darauf bestehe, dass die Klasse eine Abstraktion ist, die einen Namen annehmen kann, der sicherstellt , dass ein Blick ins Innere Sie nicht überrascht .

Sie können Curly's Law auf Klassen anwenden. Sie sind außerhalb dessen, worüber Onkel Bob spricht, aber Sie können es tun. Wenn Sie anfangen zu denken, dass dies eine Funktion bedeutet, können Sie etwas falsch machen. Das ist wie der Gedanke, eine Familie sollte nur ein Kind haben. Mehr als ein Kind zu haben, hindert es nicht daran, eine Familie zu sein.

Wenn Sie Curlys Gesetz auf eine Klasse anwenden, sollte sich alles in der Klasse um eine einzige einheitliche Idee handeln. Diese Idee kann weit gefasst sein. Die Idee könnte Beharrlichkeit sein. Wenn einige Funktionen des Protokollierungsdienstprogramms vorhanden sind, sind sie eindeutig nicht vorhanden. Es spielt keine Rolle, ob Herr X der einzige ist, der sich um diesen Code kümmert.

Das klassische Prinzip, das hier anzuwenden ist, heißt Trennung von Bedenken . Wenn Sie alle Ihre Anliegen trennen, könnte man argumentieren, dass es sich bei dem, was an einem Ort übrig bleibt, um ein Anliegen handelt. So nannten wir diese Idee, bevor uns die City Slickers 1991 mit der Figur Curly bekannt machten.

Das ist okay. Es ist nur so, dass das, was Onkel Bob eine Verantwortung nennt, kein Problem ist. Eine Verantwortung für ihn ist nichts, worauf du dich konzentrierst. Es ist etwas, das dich zwingen kann, dich zu verändern. Sie können sich auf ein Anliegen konzentrieren und dennoch Code erstellen, der für verschiedene Personengruppen mit unterschiedlichen Agenden verantwortlich ist.

Vielleicht interessiert dich das nicht. Fein. Der Gedanke, dass das Halten, um "eine Sache zu tun", all Ihre Designprobleme lösen wird, zeigt einen Mangel an Vorstellungskraft darüber, was "eine Sache" am Ende sein kann. Ein weiterer Grund, den Umfang einzuschränken, ist die Organisation. Sie können viele "eine Sache" in andere "eine Sache" schachteln, bis Sie eine Müllschublade voller Dinge haben. Ich habe darüber gesprochen , bevor

Natürlich besteht der klassische OOP-Grund für die Einschränkung des Gültigkeitsbereichs darin, dass die Klasse private Felder enthält und stattdessen Getter verwendet, um diese Daten gemeinsam zu nutzen. Wir setzen jede Methode, die diese Daten benötigt, in die Klasse ein, in der sie die Daten privat verwenden können. Viele finden dies zu restriktiv, um es als Bereichsbegrenzer zu verwenden, da nicht jede zusammengehörige Methode genau dieselben Felder verwendet. Ich möchte sicherstellen, dass jede Idee, die die Daten zusammengebracht hat, dieselbe Idee ist, die die Methoden zusammengebracht hat.

Die funktionale Art und Weise, dies zu betrachten ist , dass a.f(x)und a.g(x)ist einfach f ein (x) und g a (x). Nicht zwei Funktionen, sondern ein Kontinuum von Funktionspaaren, die sich gemeinsam unterscheiden. Sie amüssen nicht einmal Daten enthalten. Es könnte einfach sein, wie Sie wissen, welche fund welche gImplementierung Sie verwenden werden. Funktionen, die sich gemeinsam ändern, gehören zusammen. Das ist guter alter Polymorphismus.

SRP ist nur einer von vielen Gründen, den Umfang einzuschränken. Das ist ein guter. Aber nicht der einzige.

kandierte_orange
quelle
25
Ich denke, diese Antwort ist verwirrend für jemanden, der versucht, die SRP herauszufinden. Der Kampf zwischen Herrn Präsident und Frau Direktor wird nicht mit technischen Mitteln gelöst, und es ist unsinnig, ihn zur Rechtfertigung einer technischen Entscheidung zu verwenden. Conways Gesetz in Aktion.
Whatsisname
8
@whatsisname Im Gegenteil. Das SRP sollte ausdrücklich für Interessengruppen gelten. Es hat nichts mit technischem Design zu tun. Sie können mit diesem Ansatz nicht einverstanden sein, aber so wurde SRP ursprünglich von Onkel Bob definiert, und er musste es immer wieder wiederholen, weil die Leute aus irgendeinem Grund nicht in der Lage zu sein scheinen, diesen einfachen Begriff zu verstehen (egal, ob es ist eigentlich nützlich, ist eine völlig orthogonale Frage).
Luaan
Das von Tim Ottinger beschriebene Gesetz von Curly betont, dass eine Variable durchweg eine Sache bedeuten sollte . Für mich ist SRP ein bisschen stärker als das; Eine Klasse kann konzeptionell "eine Sache" darstellen, jedoch die SRP verletzen, wenn zwei externe Treiber der Veränderung einen Aspekt dieser "einen Sache" auf unterschiedliche Weise behandeln oder sich um zwei unterschiedliche Aspekte sorgen. Das Problem ist eines der Modellierung; Sie haben sich dafür entschieden, etwas als einzelne Klasse zu modellieren, aber die Domäne hat etwas, das diese Auswahl problematisch macht (mit der Weiterentwicklung der Codebasis beginnen sich die Dinge in Ihren Weg zu stellen).
Filip Milovanović
2
@ FilipMilovanović Die Ähnlichkeit, die ich zwischen Conways Gesetz und SRP sehe, wie Onkel Bob SRP in seinem Clean Architecture-Buch erklärte, beruht auf der Annahme, dass die Organisation ein sauberes azyklisches Organigramm hat. Das ist eine alte Idee. Sogar die Bibel hat hier ein Zitat: "Niemand kann zwei Herren dienen".
candied_orange
1
@TKK Ich beziehe es (nicht gleichzusetzen) mit Conways Gesetz, nicht mit Curlys Gesetz. Ich widerlege die Idee, dass SRP das Gesetz von Curly ist, vor allem, weil Onkel Bob dies selbst in seinem Buch „Saubere Architektur“ gesagt hat.
candied_orange
48

Der Schlüssel hier ist der Umfang oder, wenn Sie es vorziehen, die Granularität . Ein Teil der durch eine Klasse dargestellten Funktionalität kann weiter in Teile der Funktionalität unterteilt werden, wobei jeder Teil eine Methode ist.

Hier ist ein Beispiel. Stellen Sie sich vor, Sie müssen eine CSV aus einer Sequenz erstellen. Wenn Sie mit RFC 4180 kompatibel sein möchten, würde es einige Zeit dauern, den Algorithmus zu implementieren und alle Edge-Fälle zu behandeln.

Wenn Sie es in einer einzelnen Methode ausführen, wird der Code nicht besonders lesbar sein, und insbesondere führt die Methode mehrere Aktionen gleichzeitig aus. Daher werden Sie es in mehrere Methoden aufteilen; Beispielsweise kann einer von ihnen für die Erzeugung des Headers, dh der allerersten Zeile der CSV, zuständig sein, während eine andere Methode einen Wert eines beliebigen Typs in seine für das CSV-Format geeignete Zeichenfolgendarstellung umwandelt, während eine andere bestimmt, ob a value muss in doppelte Anführungszeichen eingeschlossen werden.

Diese Methoden haben ihre eigene Verantwortung. Die Methode, die prüft, ob Anführungszeichen hinzugefügt werden müssen, hat eine eigene und die Methode, die den Header generiert, eine eigene. Dies ist SRP, das auf Methoden angewendet wird .

Nun haben alle diese Methoden ein Ziel gemeinsam, nämlich eine Sequenz zu erstellen und die CSV zu generieren. Dies liegt in der alleinigen Verantwortung der Klasse .


Pablo H kommentierte:

Nettes Beispiel, aber ich glaube, es antwortet immer noch nicht, warum SRP einer Klasse erlaubt, mehr als eine öffentliche Methode zu haben.

Tatsächlich. Das von mir angegebene CSV-Beispiel hat idealerweise eine öffentliche Methode, und alle anderen Methoden sind privat. Ein besseres Beispiel wäre eine Warteschlange, die von einer QueueKlasse implementiert wird . Diese Klasse würde grundsätzlich zwei Methoden enthalten: push(auch genannt enqueue) und pop(auch genannt dequeue).

  • Es liegt in der Verantwortung von Queue.push, ein Objekt zum Ende der Warteschlange hinzuzufügen.

  • Die Verantwortung von Queue.popbesteht darin, ein Objekt aus dem Kopf der Warteschlange zu entfernen und den Fall zu behandeln, in dem die Warteschlange leer ist.

  • Die Verantwortung der QueueKlasse besteht darin, eine Warteschlangenlogik bereitzustellen.

Arseni Mourzenko
quelle
1
Nettes Beispiel, aber ich glaube, es antwortet immer noch nicht, warum SRP einer Klasse erlaubt, mehr als eine öffentliche Methode zu haben.
Pablo H
1
@PabloH: fair. Ich habe ein weiteres Beispiel hinzugefügt, in dem eine Klasse zwei Methoden hat.
Arseni Mourzenko
30

Eine Funktion ist eine Funktion.

Eine Verantwortung ist eine Verantwortung.

Ein Mechaniker hat die Verantwortung, Autos zu reparieren, was Diagnose, einige einfache Wartungsaufgaben, einige tatsächliche Reparaturarbeiten, einige Delegation von Aufgaben an andere usw. beinhaltet.

Eine Containerklasse (Liste, Array, Wörterbuch, Karte usw.) ist dafür verantwortlich, Objekte zu speichern. Dazu gehört das Speichern, das Einfügen, das Bereitstellen des Zugriffs, eine Art von Bestellung usw.

Eine einzelne Verantwortung bedeutet nicht, dass es nur sehr wenig Code / Funktionalität gibt, sondern dass alle Funktionen, die vorhanden sind, unter derselben Verantwortung "zusammengehören".

Peter
quelle
2
Concur. @Aulis Ronkainen - um die beiden Antworten zu verknüpfen. Und für verschachtelte Aufgaben hat eine Werkstatt gemäß Ihrer Mechaniker-Analogie die Verantwortung für die Wartung der Fahrzeuge. Verschiedene Mechaniker in der Garage haben die Verantwortung für verschiedene Teile des Autos, aber jeder dieser Mechaniker arbeitet im Zusammenhalt zusammen
wolfsshield
2
@wolfsshield, einverstanden. Mechaniker, die nur eines tun, sind nutzlos, Mechaniker, die eine einzige Verantwortung tragen, nicht (zumindest nicht unbedingt). Auch wenn reale Analogien nicht immer die besten sind, um abstrakte OOP-Konzepte zu beschreiben, ist es wichtig, diese Unterschiede zu unterscheiden. Ich glaube, der Unterschied nicht zu verstehen, ist das, was die Verwirrung an erster Stelle schafft.
Aulis Ronkainen
3
@AulisRonkainen Während es aussieht, riecht und sich anfühlt wie eine Analogie, wollte ich den Mechanismus verwenden, um die spezifische Bedeutung des Begriffs Verantwortung in SRP hervorzuheben . Ich stimme Ihrer Antwort vollkommen zu.
Peter
20

Einzelverantwortung bedeutet nicht notwendigerweise, dass sie nur eine Sache tut.

Nehmen Sie zum Beispiel eine Benutzerdienstklasse:

class UserService {
    public User Get(int id) { /* ... */ }
    public User[] List() { /* ... */ }

    public bool Create(User u) { /* ... */ }
    public bool Exists(int id) { /* ... */ }
    public bool Update(User u) { /* ... */ }
}

Diese Klasse hat mehrere Methoden, aber die Verantwortung ist klar. Es bietet Zugriff auf die Benutzerdatensätze im Datenspeicher. Die einzigen Abhängigkeiten sind das Benutzermodell und der Datenspeicher. Es ist lose gekoppelt und sehr kohäsiv, was SRP wirklich versucht, Sie zum Nachdenken zu bewegen.

SRP sollte nicht mit dem "Prinzip der Schnittstellentrennung" verwechselt werden (siehe SOLID ). Das Prinzip der Schnittstellentrennung (ISP) besagt, dass kleinere, leichtgewichtige Schnittstellen größeren, allgemeineren Schnittstellen vorzuziehen sind. Go nutzt ISP in seiner gesamten Standardbibliothek intensiv:

// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

SRP und ISP sind sicherlich verwandt, aber das eine impliziert nicht das andere. ISP befindet sich auf der Schnittstellenebene und SRP auf der Klassenebene. Wenn eine Klasse mehrere einfache Schnittstellen implementiert, hat sie möglicherweise nicht mehr nur eine Verantwortung.

Vielen Dank an Luaan, der auf den Unterschied zwischen ISP und SRP hingewiesen hat.

Jesse
quelle
3
Tatsächlich beschreiben Sie das Prinzip der Schnittstellentrennung (das "I" in SOLID). SRP ist ein ganz anderes Tier.
Luaan
Abgesehen davon, welche Kodierungskonvention verwenden Sie hier? Ich würde die erwarten , Objekte UserService und Userwerden Uppercamelcase, aber die Methoden Create , Existsund Updateich würde lowerCamelCase gemacht haben.
KlaymenDK
1
@KlaymenDK Du hast recht, die Verwendung von Go in Großbuchstaben ist nur eine Gewohnheit (Großbuchstaben = exportiert / öffentlich, Kleinbuchstaben = privat)
Jesse
@Luaan Danke für den Hinweis, ich werde meine Antwort klären
Jesse
1
@KlaymenDK Viele Sprachen verwenden PascalCase sowohl für Methoden als auch für Klassen. C # zum Beispiel.
Omegastick
15

In einem Restaurant gibt es einen Koch. Seine einzige Verantwortung ist das Kochen. Dennoch kann er Steaks, Kartoffeln, Brokkoli und hundert andere Dinge kochen. Würden Sie einen Koch pro Gericht auf Ihrer Speisekarte einstellen? Oder ein Koch für jede Komponente eines Gerichts? Oder ein Koch, der seiner alleinigen Verantwortung gerecht werden kann: Kochen?

Wenn Sie diesen Koch bitten, auch die Gehaltsabrechnung zu übernehmen, verstoßen Sie gegen das SRP.

gnasher729
quelle
4

Gegenbeispiel: Speicherung des veränderlichen Zustands.

Angenommen, Sie hatten die einfachste Klasse aller Zeiten, deren einzige Aufgabe es ist, eine zu speichern int.

public class State {
    private int i;


    public State(int i) { this.i = i; }
}

Wenn Sie auf nur eine Methode beschränkt wären, könnten Sie entweder eine setState()oder eine haben getState(), es sei denn, Sie unterbrechen die Kapselung und machen sie iöffentlich.

  • Ein Setter ist ohne Getter nutzlos (man könnte die Informationen nie lesen)
  • Ein Getter ist ohne Setter nutzlos (Sie können die Informationen niemals mutieren).

Es ist klar, dass für diese einzelne Verantwortung mindestens zwei Methoden für diese Klasse erforderlich sind . QED.

Alexander
quelle
4

Sie interpretieren das Prinzip der Einzelverantwortung falsch.

Einzelne Verantwortung entspricht nicht einer einzelnen Methode. Sie bedeuten verschiedene Dinge. In der Softwareentwicklung sprechen wir über Zusammenhalt . Funktionen (Methoden), die eine hohe Kohäsion aufweisen, "gehören" zusammen und können als eine Verantwortung ausgeführt werden.

Es ist Sache des Entwicklers, das System so zu gestalten, dass das Prinzip der einmaligen Verantwortung erfüllt wird. Man kann dies als Abstraktionstechnik sehen und ist daher manchmal Ansichtssache. Durch die Implementierung des Grundsatzes der Einzelverantwortung ist der Code hauptsächlich einfacher zu testen und seine Architektur und sein Design besser zu verstehen.

Aulis Ronkainen
quelle
2

Es ist oft hilfreich (in jeder Sprache, insbesondere in OO-Sprachen), die Dinge zu betrachten und sie aus der Sicht der Daten und nicht aus der Sicht der Funktionen zu organisieren.

Betrachten Sie die Verantwortung einer Klasse daher als die Aufrechterhaltung der Integrität und als Hilfe bei der korrekten Verwendung der ihr gehörenden Daten. Dies ist natürlich einfacher, wenn sich der gesamte Code in einer Klasse befindet, als auf mehrere Klassen verteilt zu sein. Das Hinzufügen von zwei Punkten ist mit einer Point add(Point p)Methode in der PointKlasse zuverlässiger und die Pflege des Codes einfacher als mit einer anderen Methode .

Insbesondere sollte die Klasse nichts verfügbar machen, was zu inkonsistenten oder falschen Daten führen könnte. Wenn a beispielsweise Pointin einer Ebene von (0,0) bis (127,127) liegen muss, haben der Konstruktor und alle Methoden, die neue PointWerte ändern oder erzeugen , die Verantwortung, die angegebenen Werte zu überprüfen und alle Änderungen abzulehnen, die dies verletzen würden Anforderung. (Oft ist so etwas wie a Pointunveränderlich, und sicherzustellen, dass es keine Möglichkeiten gibt Point, ein Objekt nach seiner Erstellung zu ändern, liegt dann auch in der Verantwortung der Klasse.)

Beachten Sie, dass die Schichtung hier vollkommen akzeptabel ist. Möglicherweise haben Sie eine PointKlasse für den Umgang mit einzelnen Punkten und eine PolygonKlasse für den Umgang mit einer Menge von Points. Diese haben immer noch getrennte Verantwortlichkeiten, da die PolygonDelegierten die gesamte Verantwortung für alles, was ausschließlich mit einem zu tun hat Point(z. B. sicherzustellen, dass ein Punkt sowohl einen xals auch einen yWert hat), an die PointKlasse delegieren .

Curt J. Sampson
quelle