Wie spezifisch sollte das Muster der Einzelverantwortung für Klassen sein?

14

Angenommen, Sie haben ein Konsolenspielprogramm, das alle Arten von Eingabe- / Ausgabemethoden für und von der Konsole bietet. Wäre es klug sein , sie alle in einem einzigen zu halten inputOutputKlasse oder sie brechen , um spezifischere Klassen nach unten wie startMenuIO, inGameIO, playerIO, gameBoardIOetc. , so dass jede Klasse hat etwa 1-5 Methoden?

Und im gleichen Sinne, wenn es besser ist, sie zu zerlegen, wäre es klug, sie in einem IONamespace zu platzieren und sie dadurch etwas ausführlicher zu nennen, zB: IO.inGameetc.?

Shinzou
quelle
Verwandte: Ich habe noch nie einen guten Grund gesehen, Input und Output zu kombinieren. Und wenn ich sie absichtlich trenne, wird mein Code viel sauberer.
Mooing Duck
1
In welcher Sprache benennen Sie überhaupt Klassen in lowerCamelCase?
user253751

Antworten:

7

Update (Rückblick)

Da ich eine ziemlich ausführliche Antwort geschrieben habe, läuft alles auf Folgendes hinaus:

  • Namespaces sind gut, verwenden Sie sie, wann immer es Sinn macht
  • Verwendung inGameIOund playerIOKlassen würden wahrscheinlich einen Verstoß gegen die SRP darstellen. Dies bedeutet wahrscheinlich, dass Sie die Art und Weise, wie Sie mit E / A umgehen, mit der Anwendungslogik koppeln.
  • Haben Sie ein paar generische E / A-Klassen, die von Handlerklassen verwendet (oder manchmal gemeinsam genutzt) werden. Diese Handlerklassen würden dann die rohen Eingaben in ein Format übersetzen, aus dem Ihre Anwendungslogik einen Sinn machen kann.
  • Gleiches gilt für die Ausgabe: Dies kann von relativ generischen Klassen durchgeführt werden, aber der Spielstatus wird über ein Handler- / Mapper-Objekt übergeben, das den internen Spielstatus in etwas übersetzt, das die generischen E / A-Klassen verarbeiten können.

Ich denke, Sie sehen das falsch. Sie trennen die E / A in Abhängigkeit von den Komponenten der Anwendung, während es für mich sinnvoller ist, getrennte E / A-Klassen basierend auf der Quelle und dem "Typ" der E / A zu haben .

Wenn Sie einige Basis- / generische KeyboardIOKlassen haben MouseIO, mit denen Sie beginnen sollen, und die dann darauf basieren, wann und wo Sie sie benötigen, verfügen Sie über Unterklassen, die diese E / A unterschiedlich behandeln.
Zum Beispiel ist die Texteingabe etwas, mit dem Sie wahrscheinlich anders umgehen möchten als mit spielinternen Steuerelementen. Sie werden feststellen, dass Sie bestimmte Schlüssel je nach Anwendungsfall unterschiedlich zuordnen möchten, aber diese Zuordnung ist nicht Teil der E / A selbst, sondern die Art und Weise, wie Sie mit der E / A umgehen.

Wenn ich mich an die SRP halte, hätte ich ein paar Klassen, die ich für die Tastatur-E / A verwenden kann. Abhängig von der Situation möchte ich wahrscheinlich auf unterschiedliche Weise mit diesen Klassen interagieren, aber ihre einzige Aufgabe ist es, mir mitzuteilen, was der Benutzer tut.

Ich würde diese Objekte dann in ein Handler-Objekt einfügen, das entweder die rohe E / A auf etwas abbildet, mit dem meine Anwendungslogik arbeiten kann (z. B .: Benutzer drückt "w" , der Handler ordnet das zu MOVE_FORWARD).

Diese Handler werden wiederum verwendet, um die Zeichen in Bewegung zu versetzen und den Bildschirm entsprechend zu zeichnen. Eine grobe Vereinfachung, aber der Kern davon ist diese Art von Struktur:

[ IO.Keyboard.InGame ] // generic, if SoC and SRP are strongly adhered to, changing this component should be fairly easy to do
   ||
   ==> [ Controls.Keyboard.InGameMapper ]

[ Game.Engine ] <- Controls.Keyboard.InGameMapper
                <- IO.Screen
                <- ... all sorts of stuff here
    InGameMapper.move() //returns MOVE_FORWARD or something
      ||
      ==> 1. Game.updateStuff();//do all the things you need to do to move the character in the given direction
          2. Game.Screen.SetState(GameState); //translate the game state (inverse handler)
          3. IO.Screen.draw();//generate actual output

Was wir jetzt haben, ist eine Klasse, die für die Tastatur-E / A in ihrer Rohform verantwortlich ist. Eine andere Klasse, die diese Daten in etwas übersetzt, woraus die Spiel-Engine tatsächlich Sinn machen kann, verwendet diese Daten dann, um den Status aller beteiligten Komponenten zu aktualisieren, und schließlich kümmert sich eine separate Klasse um die Ausgabe auf dem Bildschirm.

Jede einzelne Klasse hat einen einzelnen Job: Die Bearbeitung der Tastatureingaben erfolgt durch eine Klasse, die nicht weiß / sich darum kümmert / wissen muss, was die Eingabe bedeutet, die sie verarbeitet. Es muss nur wissen, wie die Eingabe abgerufen wird (gepuffert, ungepuffert, ...).

Der Handler übersetzt dies in eine interne Darstellung für den Rest der Anwendung, um diese Informationen zu verstehen.

Die Game-Engine verwendet die übersetzten Daten, um alle relevanten Komponenten über das Geschehen zu informieren. Jede dieser Komponenten macht nur eine Sache, egal ob es sich um Kollisionsprüfungen oder Änderungen der Charakteranimation handelt, es spielt keine Rolle, das hängt von jedem einzelnen Objekt ab.

Diese Objekte geben dann ihren Zustand zurück, und diese Daten werden an Game.Screeneinen inversen E / A-Handler übergeben. Es ordnet die interne Darstellung einem Element zu, mit dem die IO.ScreenKomponente die eigentliche Ausgabe generieren kann.

Elias Van Ootegem
quelle
Da es sich um eine Konsolenanwendung handelt, gibt es keine Maus, und das Drucken der Nachrichten oder des Boards ist eng mit der Eingabe verknüpft. Sind in Ihrem Beispiel die Namespaces IOund gameoder Klassen mit Unterklassen?
Shinzou
@ kuhaku: Das sind Namespaces. Das Wesentliche ist, dass Sie, wenn Sie Unterklassen basierend auf dem Teil der Anwendung erstellen, in dem Sie sich befinden, die grundlegenden E / A-Funktionen effektiv eng mit Ihrer Anwendungslogik verknüpfen. Sie erhalten Klassen, die für die E / A in Abhängigkeit von der Anwendung verantwortlich sind . Das klingt für mich nach einer Verletzung des SRP. In
Bezug
Ich habe meine Antwort aktualisiert, um meine Antwort zusammenzufassen
Elias Van Ootegem
Ja, das ist tatsächlich ein anderes Problem, das ich habe (E / A mit der Logik koppeln), also empfehlen Sie tatsächlich, die Eingabe von der Ausgabe zu trennen?
Shinzou
@kuhaku: Das hängt wirklich davon ab, was Sie tun und wie komplex die Eingabe / Ausgabe-Aufgaben sind. Wenn sich die Handler (Spielstatus und unformatierte Eingabe / Ausgabe) zu stark unterscheiden, sollten Sie wahrscheinlich die Eingabe- und Ausgabeklassen trennen.
Andernfalls
23

Das Prinzip der Einzelverantwortung kann schwierig zu verstehen sein. Was ich als nützlich empfunden habe, ist, sich vorzustellen, wie man Sätze schreibt. Sie versuchen nicht, viele Ideen in einen einzigen Satz zu packen. Jeder Satz sollte eine Idee klar wiedergeben und die Details aufschieben. Wenn Sie beispielsweise ein Auto definieren möchten, würden Sie sagen:

Ein Straßenfahrzeug, typischerweise mit vier Rädern, das von einem Verbrennungsmotor angetrieben wird.

Dann würden Sie Dinge wie "Fahrzeug", "Straße", "Räder" usw. separat definieren. Sie würden nicht versuchen zu sagen:

Ein Fahrzeug zum Transportieren von Personen auf einer Durchgangsstraße, Route oder einem Landweg zwischen zwei Stellen, die gepflastert oder auf andere Weise verbessert wurden, um eine Fahrt mit vier kreisförmigen Objekten zu ermöglichen, die sich auf einer unter dem Fahrzeug befestigten Achse drehen und von einem Motor angetrieben werden, der einen Motor erzeugt Antriebskraft durch Verbrennen von Benzin, Öl oder anderem Kraftstoff mit Luft.

Ebenso sollten Sie versuchen, Ihre Klassen, Methoden usw. so einfach wie möglich zu gestalten, das zentrale Konzept anzugeben und die Details auf andere Methoden und Klassen zu verschieben. Genau wie beim Schreiben von Sätzen gibt es keine feste Regel, wie groß sie sein sollten.

Vaughn Cato
quelle
So wie beim Definieren des Autos mit ein paar statt vielen Wörtern, ist das Definieren kleiner spezifischer, aber ähnlicher Klassen in Ordnung?
Shinzou
2
@ kuhaku: Klein ist gut. Spezifisch ist gut. Ähnlich ist es in Ordnung, solange Sie es trocken halten. Sie versuchen, Ihr Design einfach zu ändern. Wenn Sie die Dinge klein und spezifisch halten, wissen Sie, wo Sie Änderungen vornehmen müssen. Wenn Sie es trocken halten, müssen Sie nicht viele Stellen wechseln.
Vaughn Cato
6
Dein zweiter Satz sieht aus wie "Verdammt, Lehrer sagte, das müssten 4 Seiten sein ..." erzeugt Triebkraft "das ist es !!"
CorsiKa
2

Ich würde sagen, dass der beste Weg ist, sie in getrennten Klassen zu halten. Kleine Klassen sind nicht schlecht, in der Tat sind sie die meiste Zeit eine gute Idee.

In Bezug auf Ihren speziellen Fall denke ich, dass die Trennung Ihnen dabei helfen kann, die Logik eines dieser speziellen Handler zu ändern, ohne die anderen zu beeinflussen, und es wäre für Sie, falls erforderlich, einfacher, eine neue Eingabe- / Ausgabemethode hinzuzufügen, wenn es dazu käme.

Zalomon
quelle
0

Das Prinzip der Einzelverantwortung besagt, dass eine Klasse nur einen Grund haben sollte, sich zu ändern. Wenn Ihre Klasse mehrere Änderungsgründe hat, können Sie sie in andere Klassen aufteilen und die Komposition verwenden, um dieses Problem zu beheben.

Um Ihre Frage zu beantworten, muss ich Ihnen eine Frage stellen: Hat Ihre Klasse nur einen Grund, sich zu ändern? Wenn nicht, dann haben Sie keine Angst davor, weiter spezialisierte Klassen hinzuzufügen, bis jeder nur noch einen Grund zum Ändern hat.

Greg Burghardt
quelle