Was ist die wahre Verantwortung einer Klasse?

42

Ich frage mich immer wieder, ob es legitim ist, Verben zu verwenden, die auf Substantiven in OOP basieren.
Ich bin auf diesen brillanten Artikel gestoßen , obwohl ich immer noch nicht der Meinung bin, worauf es ankommt.

Um das Problem etwas näher zu erläutern, heißt es in dem Artikel, dass es beispielsweise keine FileWriterKlasse geben sollte, aber da Schreiben eine Aktion ist , sollte es eine Methode der Klasse sein File. Sie werden feststellen, dass es oft sprachabhängig ist, da ein Ruby-Programmierer wahrscheinlich gegen die Verwendung einer FileWriterKlasse ist (Ruby verwendet eine Methode, File.openum auf eine Datei zuzugreifen), wohingegen ein Java-Programmierer dies nicht tut.

Mein persönlicher (und ja, sehr bescheidener) Standpunkt ist, dass dies gegen das Prinzip der einheitlichen Verantwortung verstoßen würde. Wenn ich in PHP programmiert habe (weil PHP offensichtlich die beste Sprache für OOP ist, oder?), Habe ich oft diese Art von Framework verwendet:

<?php

// This is just an example that I just made on the fly, may contain errors

class User extends Record {

    protected $name;

    public function __construct($name) {
        $this->name = $name;
    }

}

class UserDataHandler extends DataHandler /* knows the pdo object */ {

    public function find($id) {
         $query = $this->db->prepare('SELECT ' . $this->getFields . ' FROM users WHERE id = :id');
         $query->bindParam(':id', $id, PDO::PARAM_INT);
         $query->setFetchMode( PDO::FETCH_CLASS, 'user');
         $query->execute();
         return $query->fetch( PDO::FETCH_CLASS );
    }


}

?>

Nach meinem Verständnis fügt das Suffix DataHandler nichts Relevantes hinzu . Der Punkt ist jedoch, dass das Prinzip der Einzelverantwortung uns vorschreibt, dass ein Objekt, das als Modell mit Daten verwendet wird (möglicherweise als Datensatz bezeichnet), nicht auch die Verantwortung für die Ausführung von SQL-Abfragen und den Datenbankzugriff haben sollte. Dadurch wird das ActionRecord-Muster, das beispielsweise von Ruby on Rails verwendet wird, ungültig.

Ich bin neulich auf diesen C # -Code gestoßen (yay, vierte in diesem Beitrag verwendete Objektsprache):

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

Und ich muss sagen, dass es für mich wenig sinnvoll ist, wenn eine Encodingoder CharsetKlasse tatsächlich Zeichenfolgen codiert . Es sollte lediglich eine Darstellung dessen sein, was eine Kodierung wirklich ist.

Daher würde ich eher denken, dass:

  • Es liegt nicht in der FileVerantwortung der Klasse, Dateien zu öffnen, zu lesen oder zu speichern.
  • Es liegt nicht in der XmlVerantwortung der Klasse, sich selbst zu serialisieren.
  • Es ist keine UserKlassenverantwortung, eine Datenbank abzufragen.
  • usw.

Wenn wir diese Ideen jedoch extrapolieren, warum sollte Objectes dann eine toStringKlasse geben? Es liegt nicht in der Verantwortung eines Autos oder eines Hundes, sich in eine Schnur umzuwandeln, oder?

Ich verstehe, dass es aus pragmatischer Sicht toStringkeine akzeptable Option ist, die Methode für die Schönheit der Befolgung einer strengen SOLID-Form loszuwerden, die den Code wartbarer macht, indem er unbrauchbar gemacht wird.

Ich verstehe auch, dass es möglicherweise keine genaue Antwort gibt (die eher ein Aufsatz als eine ernsthafte Antwort wäre), oder dass es sich um eine meinungsbasierte Antwort handelt. Trotzdem würde ich gerne wissen, ob mein Ansatz tatsächlich dem Prinzip der Einzelverantwortung folgt.

Was ist die Verantwortung einer Klasse?

Pierre Arlaud
quelle
Objekte toString ist vor allem praktisch, und toString wird im Allgemeinen verwendet, um den internen Status während des Debuggens schnell anzuzeigen
Ratschenfreak
3
@ratchetfreak Ich stimme zu, aber es war ein Beispiel (fast ein Witz), um zu zeigen, dass die einzelne Verantwortung sehr oft in der von mir beschriebenen Weise gebrochen ist.
Pierre Arlaud
8
Während das Prinzip der Einzelverantwortung seine leidenschaftlichen Befürworter hat, ist es meiner Meinung nach bestenfalls ein Leitfaden und ein sehr lockerer Leitfaden für die überwiegende Mehrheit der Entwurfsentscheidungen. Das Problem mit SRP ist, dass Sie am Ende Hunderte von Klassen mit seltsamen Namen haben. Dadurch ist es sehr schwierig, das zu finden, was Sie benötigen (oder herauszufinden, ob das, was Sie benötigen, bereits vorhanden ist), und das Gesamtbild zu erfassen. Ich bevorzuge es, weniger gut benannte Klassen zu haben, die mir sofort sagen, was von ihnen zu erwarten ist, selbst wenn sie eine ganze Reihe von Aufgaben haben. Wenn sich die Dinge in Zukunft ändern, teile ich die Klasse auf.
Dunk

Antworten:

24

Angesichts einiger Unterschiede zwischen den Sprachen kann dies ein heikles Thema sein. Daher formuliere ich die folgenden Kommentare so, dass versucht wird, so umfassend wie möglich im Bereich von OO zu sein.

Zum einen ist das sogenannte "Single Responsibility Principle" ein ausdrücklich erklärter Reflex des Begriffes Kohäsion . Beim Lesen der damaligen Literatur (um 1970) hatten (und haben) die Menschen Mühe, zu definieren, was ein Modul ist und wie man sie so konstruiert, dass schöne Eigenschaften erhalten bleiben. Sie würden also sagen: "Hier gibt es eine Reihe von Strukturen und Prozeduren, ich werde ein Modul daraus machen", aber ohne Kriterien, warum diese willkürlichen Dinge zusammengestellt sind, ergibt die Organisation möglicherweise wenig Sinn - wenig "Zusammenhalt". Daher kam es zu Diskussionen über Kriterien.

Als Erstes ist hier festzuhalten, dass es bislang um die Organisation und die damit verbundenen Auswirkungen auf die Wartung und Verständlichkeit geht (für einen Computer ist es unerheblich, ob ein Modul "sinnvoll" ist).

Dann kam jemand anderes (Herr Martin) herein und wandte dasselbe Denken auf die Einheit einer Klasse als Kriterium an, um darüber nachzudenken, was dazu gehören sollte oder nicht, und machte dieses Kriterium zu einem besprochenen Prinzip Hier. Der Punkt, den er machte, war, dass "eine Klasse nur einen Grund haben sollte, sich zu ändern" .

Nun, wir wissen aus Erfahrung, dass viele Objekte (und viele Klassen), die scheinbar "viele Dinge" tun, einen sehr guten Grund dafür haben. Der unerwünschte Fall wären die Klassen, die mit Funktionalität bis zu dem Punkt überfüllt sind, dass sie für Wartung usw. undurchdringlich sind. Und letztere zu verstehen, bedeutet zu sehen, wo mr. Martin zielte darauf ab, als er auf das Thema näher einging.

Natürlich nach dem Lesen, was mr. Martin schrieb, es sollte klar sein, dass dies Kriterien für Richtung und Design sind, um problematische Szenarien zu vermeiden , und nicht, um irgendeine Art von Compliance zu verfolgen, geschweige denn, wenn "Verantwortung" schlecht definiert ist (und Fragen wie "tut dies") gegen den Grundsatz verstößt? "sind perfekte Beispiele für die weit verbreitete Verwirrung). Daher finde ich es bedauerlich, dass es ein Prinzip genannt wird, irreführende Menschen zu versuchen, es zu den letzten Konsequenzen zu bringen, wo es nichts nützen würde. Herr Martin selbst diskutierte Entwürfe, die "mehr als eine Sache tun", die wahrscheinlich so gehalten werden sollten, da das Trennen schlechtere Ergebnisse bringen würde. Es gibt auch viele bekannte Herausforderungen in Bezug auf Modularität (und dieses Thema ist ein Fall davon), und wir sind nicht in der Lage, auch für einige einfache Fragen eine gute Antwort zu finden.

Wenn wir diese Ideen jedoch extrapolieren, warum sollte Object eine toString-Klasse haben? Es liegt nicht in der Verantwortung eines Autos oder eines Hundes, sich in eine Schnur umzuwandeln, oder?

Lassen Sie mich jetzt innehalten, um etwas darüber zu sagen toString: Es gibt eine grundlegende Sache, die häufig vernachlässigt wird, wenn man diesen Gedankenübergang von Modulen zu Klassen durchführt und darüber nachdenkt, welche Methoden zu einer Klasse gehören sollten. Und die Sache ist dynamischer Versand (aka, späte Bindung, "Polymorphismus").

In einer Welt ohne "übergeordnete Methoden" hängt die Wahl zwischen "obj.toString ()" und "toString (obj)" allein von der bevorzugten Syntax ab. In einer Welt, in der Programmierer das Verhalten eines Programms ändern können, indem sie eine Unterklasse mit eindeutiger Implementierung einer vorhandenen / überschriebenen Methode hinzufügen, ist diese Wahl nicht mehr von Belang. und dasselbe gilt möglicherweise nicht für "freie Verfahren" (Sprachen, die mehrere Methoden unterstützen, haben einen Ausweg aus dieser Zweiteilung). Folglich geht es nicht mehr nur um die Organisation, sondern auch um die Semantik. Schließlich wird die Frage, an welche Klasse die Methode gebunden ist, auch zu einer wichtigen Entscheidung (und in vielen Fällen haben wir bisher nur Richtlinien, die uns bei der Entscheidung helfen, wo die Dinge hingehören.

Schließlich sehen wir uns mit Sprachen konfrontiert, die schreckliche Designentscheidungen enthalten, zum Beispiel, die einen dazu zwingen, für jede Kleinigkeit eine Klasse zu erstellen. Was war also einst der kanonische Grund und das Hauptkriterium für das Vorhandensein von Objekten (und somit von Klassen im Klassenland), nämlich diese "Objekte", die eine Art "Verhalten, das sich auch wie Daten verhält" sind? , aber ihre konkrete Darstellung (falls vorhanden) um jeden Preis vor direkter Manipulation zu schützen (und das ist der Haupttipp für die Schnittstelle eines Objekts aus Sicht seiner Kunden), wird unscharf und verwirrt.

Thiago Silva
quelle
31

[Anmerkung: Ich werde hier über Objekte sprechen. Bei objektorientierter Programmierung geht es ja nicht um Klassen, sondern um Objekte.]

Die Verantwortung für ein Objekt hängt hauptsächlich von Ihrem Domain-Modell ab. Normalerweise gibt es viele Möglichkeiten, dieselbe Domäne zu modellieren, und Sie werden die eine oder andere Möglichkeit auswählen, je nachdem, wie das System verwendet werden soll.

Wie wir alle wissen, ist die "Verb / Nomen" -Methode, die oft in Einführungskursen gelehrt wird, lächerlich, weil sie so sehr davon abhängt, wie Sie einen Satz formulieren. Sie können fast alles als Substantiv oder Verb ausdrücken, entweder aktiv oder passiv. Wenn Ihr Domain-Modell davon abhängt, dass dies falsch ist, sollte es eine bewusste Designentscheidung sein, ob etwas ein Objekt oder eine Methode ist, nicht nur eine zufällige Folge davon, wie Sie die Anforderungen formulieren.

Es zeigt jedoch , dass Sie, genau wie Sie viele Möglichkeiten haben, dasselbe auf Englisch auszudrücken, viele Möglichkeiten haben, dasselbe in Ihrem Domain-Modell auszudrücken… und keine dieser Möglichkeiten ist von Natur aus korrekter als die anderen. Es kommt auf den Kontext an.

Hier ist ein Beispiel, das auch in Einführungskursen sehr beliebt ist: ein Bankkonto. Was in der Regel als Objekt mit einem balanceFeld und einer transferMethode modelliert wird. Mit anderen Worten: Der Kontostand besteht aus Daten und eine Überweisung ist ein Vorgang . Welches ist ein vernünftiger Weg, um ein Bankkonto zu modellieren.

Abgesehen davon werden Bankkonten in der realen Bankensoftware nicht so modelliert (und tatsächlich funktionieren Banken auch nicht so in der realen Welt). Stattdessen haben Sie einen Transaktionsbeleg, und der Kontostand wird berechnet, indem alle Transaktionsbelege für ein Konto addiert (und subtrahiert) werden. Mit anderen Worten: Die Übertragung ist Daten und der Saldo ist eine Operation ! (Interessanterweise ist Ihr System dadurch auch rein funktionsfähig, da Sie den Kontostand nie ändern müssen. Sowohl Ihre Kontoobjekte als auch die Transaktionsobjekte müssen sich nie ändern, sie sind unveränderlich.)

Bezüglich Ihrer spezifischen Frage zu toStringstimme ich zu. Ich bevorzuge sehr die Haskell-Lösung einer Showable Typenklasse . (Scala lehrt uns, dass Typenklassen wunderbar zu OO passen.) Gleiches gilt für Gleichheit. Gleichheit ist sehr oft keine Eigenschaft eines Objekts, sondern des Kontexts, in dem das Objekt verwendet wird. Denken Sie nur an Gleitkommazahlen: Was soll das Epsilon sein? Auch hier hat Haskell die EqTypenklasse.

Jörg W. Mittag
quelle
Ich mag deine Hash-Vergleiche, das macht die Antwort ein bisschen… frischer. Was würden Sie dann über Entwürfe sagen, die es einem ermöglichen ActionRecord, sowohl ein Modell als auch ein Datenbankanforderer zu sein? Unabhängig vom Kontext scheint es das Prinzip der Einzelverantwortung zu brechen.
Pierre Arlaud
5
"... weil es so sehr darauf ankommt, wie du einen Satz formulierst." Yay! Oh und ich liebe dein Bankbeispiel. Ich wusste nicht, dass mir schon in den achtziger Jahren funktionale Programmierung beigebracht wurde :) (datenzentriertes Design und zeitunabhängige Speicherung, bei der Waagen usw. berechenbar sind und nicht gespeichert werden sollten, und die Adresse von jemandem sollte nicht geändert, sondern als gespeichert werden eine neue Tatsache) ...
Marjan Venema
1
Ich würde nicht sagen, dass die Verb / Nomen-Methode "lächerlich" ist. Ich würde sagen, dass simplistische Entwürfe scheitern und es sehr schwer ist , die richtigen zu finden. Ist eine RESTful-API beispielsweise keine Verb / Noun-Methode? Wo, für einen zusätzlichen Schwierigkeitsgrad, sind die Verben extrem begrenzt?
user949300
@ user949300: Die Tatsache, dass die Menge der Verben festgelegt ist, vermeidet genau das Problem, das ich angesprochen habe, nämlich dass Sie Sätze auf verschiedene Arten formulieren können, wodurch das, was ein Substantiv und was ein Verb ist, vom Schreibstil und nicht von der Domänenanalyse abhängig gemacht wird .
Jörg W Mittag
8

Nur allzu oft wird das Prinzip der Einzelverantwortung zum Prinzip der Nullverantwortung, und am Ende haben Sie anämische Klassen , die nichts tun (außer Setter und Getter), was zu einer Katastrophe im Königreich der Nomen führt .

Sie werden es nie perfekt machen, aber es ist besser für eine Klasse, etwas zu viel als zu wenig zu tun.

In Ihrem Beispiel mit Encoding, IMO, sollte es definitiv in der Lage sein, zu codieren. Was soll es stattdessen tun? Nur einen Namen "utf" zu haben, ist keine Verantwortung. Vielleicht sollte der Name jetzt Encoder sein. Aber, wie Konrad sagte, gehören Daten (welche Kodierung) und Verhalten (tun es) zusammen .

user949300
quelle
7

Eine Instanz einer Klasse ist ein Abschluss. Das ist es. Wenn Sie so denken, sieht jede gut gestaltete Software, die Sie betrachten, richtig aus, und jede schlecht durchdachte Software nicht. Lass mich erweitern.

Wenn Sie etwas schreiben möchten, um in eine Datei zu schreiben, denken Sie an alle Dinge, die Sie angeben müssen (zum Betriebssystem): Dateiname, Zugriffsmethode (Lesen, Schreiben, Anhängen), die Zeichenfolge, die Sie schreiben möchten.

Sie erstellen also ein File-Objekt mit dem Dateinamen (und der Zugriffsmethode). Das File-Objekt wird jetzt über dem Dateinamen geschlossen (in diesem Fall wird es wahrscheinlich als readonly / const-Wert aufgenommen). Die File-Instanz kann jetzt Aufrufe der in der Klasse definierten Methode "write" entgegennehmen. Dies erfordert nur ein String-Argument, aber im Hauptteil der Write-Methode, der Implementierung, gibt es auch Zugriff auf den Dateinamen (oder das Datei-Handle, das daraus erstellt wurde).

Eine Instanz der File-Klasse fasst daher einige Daten zu einem zusammengesetzten Blob zusammen, sodass die spätere Verwendung dieser Instanz einfacher ist. Wenn Sie ein File-Objekt haben, müssen Sie sich keine Gedanken darüber machen, wie der Dateiname lautet, sondern es geht Ihnen nur darum, welche Zeichenfolge Sie als Argument in den Schreibaufruf eingeben. Zur Wiederholung: Das File-Objekt kapselt alles, was Sie nicht wissen müssen, wo die Zeichenfolge, die Sie schreiben möchten, sich tatsächlich befindet.

Die meisten Probleme, die Sie lösen möchten, werden auf diese Weise überlagert - Dinge, die im Vorfeld erstellt wurden, Dinge, die zu Beginn jeder Iteration einer Prozessschleife erstellt wurden, dann verschiedene Dinge, die in den beiden Hälften eines if else deklariert wurden, dann Dinge, die bei erstellt wurden Der Start einer Subschleife, dann die Deklaration von Dingen als Teil des Algorithmus in dieser Schleife usw. Wenn Sie dem Stapel immer mehr Funktionsaufrufe hinzufügen, wird der Code immer feiner, aber jedes Mal, wenn Sie ihn haben Die Daten in den unteren Ebenen des Stapels wurden in eine nette Abstraktion verpackt, die den Zugriff erleichtert. Sehen Sie, was Sie tun, ist die Verwendung von "OO" als eine Art Closure-Management-Framework für einen funktionalen Programmieransatz.

Die Abkürzung für einige Probleme beinhaltet den veränderlichen Zustand von Objekten, der nicht so funktionell ist - Sie manipulieren die Verschlüsse von außen. Sie machen einige der Dinge in Ihrer Klasse "global", zumindest im obigen Umfang. Versuche jedes Mal, wenn du einen Setter schreibst, diese zu vermeiden. Ich würde behaupten, hier liegt die Verantwortung für Ihre Klasse oder für die anderen Klassen, in denen sie tätig ist, falsch. Aber machen Sie sich nicht zu viele Sorgen - es ist manchmal pragmatisch, sich in einem veränderlichen Zustand zu bewegen, um bestimmte Probleme schnell und ohne zu große kognitive Belastung zu lösen und tatsächlich etwas zu erledigen.

Um Ihre Frage zu beantworten: Was ist die eigentliche Verantwortung einer Klasse? Es geht darum, eine Art Plattform zu präsentieren, die auf einigen zugrunde liegenden Daten basiert, um eine detailliertere Funktionsweise zu erreichen. Es geht darum, die Hälfte der Daten zu kapseln, die an einem Vorgang beteiligt sind, der sich weniger häufig ändert als die andere Hälfte der Daten (Think currying ...). ZB Dateiname gegen zu schreibende Zeichenfolge.

Oft sehen diese Kapseln oberflächlich aus wie ein Objekt der realen Welt / ein Objekt in der Problemdomäne, lassen sich aber nicht täuschen. Wenn Sie eine Autoklasse erstellen, handelt es sich nicht wirklich um ein Auto, sondern um eine Sammlung von Daten, die eine Plattform bilden, auf der Sie bestimmte Dinge erreichen können, die Sie möglicherweise mit einem Auto tun möchten. Das Bilden einer Zeichenfolgendarstellung (toString) ist eines dieser Dinge - das Ausspucken all dieser internen Daten. Denken Sie daran, dass eine Autoklasse manchmal nicht die richtige Klasse ist, auch wenn es in der Problemdomäne nur um Autos geht. Im Gegensatz zum Königreich der Substantive sind es die Operationen, die Verben, auf denen eine Klasse basieren sollte.

Benedikt
quelle
4

Ich verstehe auch, dass es möglicherweise keine genaue Antwort gibt (die eher ein Aufsatz als eine ernsthafte Antwort wäre), oder dass es sich um eine meinungsbasierte Antwort handelt. Trotzdem würde ich gerne wissen, ob mein Ansatz tatsächlich dem Prinzip der Einzelverantwortung folgt.

Während dies der Fall ist, ist dies möglicherweise nicht unbedingt ein guter Code. Abgesehen von der einfachen Tatsache, dass jede Art von blind befolgter Regel zu schlechtem Code führt, gehen Sie von einer ungültigen Prämisse aus: (Programmier-) Objekte sind keine (physischen) Objekte.

Objekte sind eine Reihe zusammenhängender Daten- und Betriebsbündel (und manchmal nur eines von beiden). Während diese oft reale Objekte modellieren, erfordert der Unterschied zwischen einem Computermodell von etwas und dem Ding selbst Unterschiede.

Indem Sie diese harte Linie zwischen einem "Substantiv", das eine Sache darstellt, und anderen Dingen, die sie verbrauchen, ziehen Sie einen entscheidenden Vorteil der objektorientierten Programmierung in Kauf (Funktion und Zustand zusammen haben, damit die Funktion Invarianten des Staates schützen kann) . Schlimmer noch, Sie versuchen, die physische Welt im Code darzustellen, was, wie die Geschichte gezeigt hat, nicht gut funktionieren wird.

Telastyn
quelle
4

Ich bin neulich auf diesen C # -Code gestoßen (yay, vierte in diesem Beitrag verwendete Objektsprache):

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

Und ich muss sagen, dass es für mich wenig sinnvoll ist, wenn eine Encodingoder CharsetKlasse tatsächlich Zeichenfolgen codiert . Es sollte lediglich eine Darstellung dessen sein, was eine Kodierung wirklich ist.

Theoretisch ja, aber ich denke, dass C # der Einfachheit halber einen berechtigten Kompromiss eingeht (im Gegensatz zu Javas strengerem Ansatz).

Wenn Encodingnur eine bestimmte Codierung dargestellt (oder identifiziert) wird - beispielsweise UTF-8 -, benötigen Sie natürlich auch eine EncoderKlasse, damit Sie sie implementieren können. GetBytesDann müssen Sie jedoch die Beziehung zwischen Encoders und Encodings verwalten am Ende mit unserem guten alten

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

so brillant verspottet in dem Artikel, den du verlinkt hast (übrigens großartig gelesen).

Wenn ich Sie gut verstehe, fragen Sie, wo die Leitung ist.

Nun, auf der anderen Seite des Spektrums befindet sich der prozedurale Ansatz, der mit statischen Klassen in OOP-Sprachen nicht schwer zu implementieren ist:

HelpfulStuff.GetUTF8Bytes(myString)

Das große Problem dabei ist, dass HelpfulStuffich , wenn der Autor von " Least" und ich vor dem Monitor sitzen, nicht weiß, wo GetUTF8Byteses implementiert ist.

Sehe ich es in HelpfulStuff, Utils, StringHelper?

Herr, hilf mir, wenn die Methode tatsächlich in allen drei Fällen unabhängig implementiert wird ... was häufig vorkommt, ist, dass der Mann vor mir nicht wusste, wo er nach dieser Funktion suchen sollte, und glaubte, dass es keine gab doch so haben sie einen anderen herausgeschnappt und jetzt haben wir sie im Überfluss.

Bei richtigem Design geht es mir nicht darum, abstrakte Kriterien zu erfüllen, sondern darum, dass ich es mir leicht mache, weiterzumachen, nachdem ich mich vor Ihren Code gesetzt habe. Wie geht das jetzt?

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

bewerten in diesem Aspekt? Schlecht auch. Das ist keine Antwort, die ich hören möchte, wenn ich meinem Arbeitskollegen schreie: "Wie codiere ich eine Zeichenfolge in eine Byte-Sequenz?" :)

Ich würde also gemäßigt vorgehen und gegenüber der Einzelverantwortung liberal sein. Ich würde es vorziehen, wenn die EncodingKlasse sowohl eine Art der Codierung als auch die eigentliche Codierung identifiziert: Daten, die nicht vom Verhalten getrennt sind.

Ich denke, dass es sich um ein Fassadenmuster handelt, das dabei hilft, die Zahnräder einzufetten. Verstößt dieses legitime Muster gegen SOLID? Eine verwandte Frage: Verstößt das Fassadenmuster gegen SRP?

Beachten Sie, dass auf diese Weise möglicherweise codierte Bytes intern (in .NET-Bibliotheken) implementiert werden. Die Klassen sind einfach nicht öffentlich sichtbar und nur diese "fassadenartige" API ist außerhalb verfügbar. Es ist nicht so schwer zu überprüfen, ob das stimmt, ich bin einfach ein bisschen zu faul, um es jetzt zu tun :) Das ist es wahrscheinlich nicht, aber das macht meinen Standpunkt nicht ungültig.

Sie können sich aus Gründen der Benutzerfreundlichkeit dafür entscheiden, die öffentlich sichtbare Implementierung zu vereinfachen, während Sie intern eine strengere, kanonische Implementierung beibehalten.

Konrad Morawski
quelle
1

In Java ist die zur Darstellung von Dateien verwendete Abstraktion ein Stream, einige Streams sind unidirektional (schreibgeschützt oder schreibgeschützt), andere sind bidirektional. Für Ruby ist die File-Klasse die Abstraktion, die Betriebssystemdateien darstellt. So wird die Frage, was ist seine einzige Verantwortung? In Java ist FileWriter dafür verantwortlich, einen unidirektionalen Strom von Bytes bereitzustellen, der mit einer Betriebssystemdatei verbunden ist. In Ruby ist File dafür verantwortlich, einen bidirektionalen Zugriff auf eine Systemdatei bereitzustellen. Beide erfüllen das SRP, sie haben nur unterschiedliche Verantwortlichkeiten.

Es liegt nicht in der Verantwortung eines Autos oder eines Hundes, sich in eine Schnur umzuwandeln, oder?

Sicher warum nicht? Ich erwarte, dass die Car-Klasse eine Car-Abstraktion vollständig repräsentiert. Wenn es sinnvoll ist, die Autoabstraktion dort zu verwenden, wo ein String erforderlich ist, erwarte ich, dass die Car-Klasse die Konvertierung in einen String unterstützt.

Um die größere Frage direkt zu beantworten, ist die Verantwortung einer Klasse eine Abstraktion. Dieser Job mag komplex sein, aber das ist der springende Punkt. Eine Abstraktion sollte komplexe Dinge hinter einer benutzerfreundlicheren, weniger komplexen Benutzeroberfläche verbergen. Die SRP ist eine Gestaltungsrichtlinie, sodass Sie sich auf die Frage konzentrieren, was diese Abstraktion darstellt. Wenn mehrere unterschiedliche Verhaltensweisen erforderlich sind, um das Konzept vollständig zu abstrahieren, dann sei es so.

Steinmetalle
quelle
0

Klasse kann mehrere Verantwortlichkeiten haben. Zumindest im klassischen datenzentrierten OOP.

Wenn Sie das Prinzip der Einzelverantwortung in vollem Umfang anwenden, erhalten Sie ein verantwortungsorientiertes Design, bei dem im Grunde jede Nicht-Helfer-Methode zu einer eigenen Klasse gehört.

Ich denke, es ist nichts Falsches daran, ein Stück Komplexität aus einer Basisklasse zu extrahieren, wie im Fall von File und FileWriter. Der Vorteil ist, dass Sie eine klare Trennung von Code erhalten, der für eine bestimmte Operation verantwortlich ist, und dass Sie auch nur diesen Teil des Codes überschreiben können (Unterklasse), anstatt die gesamte Basisklasse (z. B. Datei) zu überschreiben. Dennoch scheint mir eine systematische Anwendung ein Overkill zu sein. Es stehen einfach viel mehr Klassen zur Verfügung, und für jede Operation muss die entsprechende Klasse aufgerufen werden. Flexibilität ist mit Kosten verbunden.

Diese extrahierten Klassen sollten sehr beschreibende Namen basierend auf dem Betrieb die sie enthalten, zum Beispiel <X>Writer, <X>Reader, <X>Builder. Namen wie <X>Manager, <X>Handlerbeschreiben die Operation überhaupt nicht und wenn sie so genannt werden, weil sie mehr als eine Operation enthalten, ist nicht klar, was Sie überhaupt erreicht haben. Sie haben die Funktionalität von ihren Daten getrennt, Sie brechen auch in dieser extrahierten Klasse immer noch das Prinzip der Einzelverantwortung, und wenn Sie nach einer bestimmten Methode suchen, wissen Sie möglicherweise nicht, wo Sie danach suchen sollen (wenn in <X>oder <X>Manager).

Jetzt ist Ihr Fall mit UserDataHandler anders, weil es keine Basisklasse gibt (die Klasse, die tatsächliche Daten enthält). Die Daten für diese Klasse werden in einer externen Ressource (Datenbank) gespeichert, und Sie verwenden eine API, um auf sie zuzugreifen und sie zu bearbeiten. Ihre Benutzerklasse stellt einen Laufzeitbenutzer dar, der etwas anderes ist als ein beständiger Benutzer, und die Aufteilung von Verantwortlichkeiten (Bedenken) kann hier nützlich sein, insbesondere wenn Sie über eine Vielzahl von Logik verfügen, die sich auf Laufzeitbenutzer bezieht.

Sie haben UserDataHandler wahrscheinlich so genannt, weil es in dieser Klasse im Grunde nur Funktionalität gibt und keine echten Daten. Sie können jedoch auch von dieser Tatsache abstrahieren und die Daten in der Datenbank als zur Klasse gehörend betrachten. Mit dieser Abstraktion können Sie die Klasse nach dem benennen, was sie ist und nicht nach dem, was sie tut. Sie können ihm einen Namen wie UserRepository geben, der ziemlich schick ist und auch die Verwendung des Repository-Musters vorschlägt .

Klima
quelle