Nullverhaltensobjekte in OOP - mein Designdilemma

94

Die Grundidee hinter OOP ist, dass Daten und Verhalten (auf diesen Daten) untrennbar sind und sie durch die Idee eines Objekts einer Klasse gekoppelt sind. Objekte haben Daten und Methoden, die damit arbeiten (und andere Daten). Offensichtlich werden nach den Prinzipien von OOP Objekte, die nur Daten sind (wie C-Strukturen), als Anti-Muster betrachtet.

So weit, ist es gut.

Das Problem ist, dass ich bemerkt habe, dass mein Code in letzter Zeit immer mehr in Richtung dieses Anti-Patterns zu gehen scheint. Mir scheint, je mehr ich versuche, Informationen zwischen Klassen und lose gekoppelten Entwürfen zu verbergen, desto mehr werden meine Klassen eine Mischung aus reinen Datenklassen ohne Verhalten und allen Verhaltensklassen ohne Daten.

Im Allgemeinen gestalte ich Klassen so, dass sie sich der Existenz anderer Klassen möglichst wenig bewusst werden und die Schnittstellen anderer Klassen möglichst wenig kennen. Ich setze dies besonders von oben nach unten durch, Klassen mit niedrigerem Level wissen nichts über Klassen mit höherem Level. Z.B:

Angenommen, Sie haben eine allgemeine Kartenspiel-API. Du hast eine Klasse Card. Jetzt muss diese CardKlasse die Sichtbarkeit für die Spieler bestimmen.

Eine Möglichkeit ist, boolean isVisible(Player p)am CardUnterricht teilzunehmen.

Ein weiterer Grund ist zu haben , boolean isVisible(Card c)auf PlayerKlasse.

Ich mag den ersten Ansatz nicht besonders, da er Wissen über eine höhere PlayerKlasse einer niedrigeren CardKlasse verleiht .

Stattdessen habe ich mich für die dritte Option entschieden, bei der wir eine ViewportKlasse haben, die Playeranhand von a und einer Liste von Karten bestimmt, welche Karten sichtbar sind.

Allerdings raubt dieser Ansatz sowohl Cardund PlayerKlassen einer möglichen Memberfunktion. Sobald Sie dies für andere Sachen als die Sichtbarkeit von Karten zu tun, werden Sie mit links Cardund PlayerKlassen , die rein Daten enthalten , wie alle Funktionen in anderen Klassen implementiert, die ohne Daten meist Klassen sind nur Methoden, wie die Viewportoben.

Dies ist eindeutig gegen die Grundidee von OOP.

Welches ist der richtige Weg? Wie soll ich vorgehen, um die Klassenabhängigkeiten zu minimieren und das angenommene Wissen und die Kopplung zu minimieren, ohne jedoch mit dem seltsamen Design aufzuhören, bei dem alle Klassen auf niedriger Ebene nur Daten enthalten und Klassen auf hoher Ebene alle Methoden enthalten? Hat jemand eine dritte Lösung oder Perspektive für das Klassendesign, die das ganze Problem vermeidet?

PS Hier ist ein weiteres Beispiel:

Angenommen, Sie haben DocumentIdeine unveränderliche Klasse, die nur ein einziges BigDecimal idMitglied und einen Getter für dieses Mitglied hat. Jetzt müssen Sie irgendwo eine Methode haben, die eine DocumentIdRückgabe Documentfür diese ID aus einer Datenbank liefert .

Machst du:

  • Fügen Sie Document getDocument(SqlSession)der DocumentIdKlasse eine Methode hinzu , die plötzlich Kenntnisse über Ihre Persistenz ( "we're using a database and this query is used to retrieve document by id"), die API für den Zugriff auf die Datenbank und dergleichen vermittelt. Auch diese Klasse benötigt nun eine Persistenz-JAR-Datei, um kompiliert zu werden.
  • Fügen Sie eine andere Klasse mit der Methode hinzu Document getDocument(DocumentId id)und lassen Sie die DocumentIdKlasse als tot, ohne Verhalten, strukturähnliche Klasse.
RokL
quelle
21
Einige Ihrer Prämissen hier sind völlig falsch, was die Beantwortung der zugrunde liegenden Frage sehr schwierig machen wird. Halten Sie Ihre Fragen so präzise und meinungsfrei wie möglich und Sie erhalten bessere Antworten.
pdr
31
"Dies ist eindeutig gegen die Grundidee von OOP" - nein, es ist nicht, aber es ist ein verbreiteter Irrtum.
Doc Brown
5
Ich denke, das Problem liegt in der Tatsache, dass es in der Vergangenheit verschiedene Schulen für "Objektorientierung" gegeben hat - so wie es ursprünglich von Leuten wie Alan Kay gemeint war (siehe geekswithblogs.net/theArchitectsNapkin/archive/2013/09/08/ … ) Und der Weg wurde im Kontext von OOA / OOD von den Leuten von Rational gelehrt ( en.wikipedia.org/wiki/Object-oriented_analysis_and_design ).
Doc Brown
21
Dies ist eine sehr gute Frage und gut gepostet - im Gegensatz zu einigen anderen Kommentaren, würde ich sagen. Es zeigt deutlich, wie naiv oder unvollständig die meisten Ratschläge zur Programmstrukturierung sind - und wie schwierig es ist, dies zu tun, und wie unerreichbar ein korrektes Design in vielen Situationen ist, unabhängig davon, wie sehr man sich bemüht, es richtig zu machen. Und obwohl eine offensichtliche Antwort auf die spezifische Frage Multimethoden sind, bleibt das zugrunde liegende Problem des Entwurfs bestehen.
Thiago Silva
5
Wer sagt, dass Klassen ohne Verhalten ein Anti-Muster sind?
James Anderson

Antworten:

42

Was Sie beschreiben, wird als anämisches Domänenmodell bezeichnet . Wie bei vielen OOP-Konstruktionsprinzipien (wie Law of Demeter usw.) lohnt es sich nicht, sich nach hinten zu bücken, nur um eine Regel zu erfüllen.

Es ist nichts Falsches daran, Tüten voller Werte zu haben, solange sie nicht die gesamte Landschaft durcheinander bringen und sich nicht auf andere Objekte verlassen, um die Hausarbeit zu erledigen, die sie für sich selbst erledigen könnten .

Es wäre sicherlich ein Codegeruch, wenn Sie eine separate Klasse nur zum Ändern der Eigenschaften von Card- hätten, wenn vernünftigerweise zu erwarten wäre, dass sie sich von selbst darum kümmert.

Aber ist es wirklich eine Aufgabe Cardzu wissen, für wen Playeres sichtbar ist?

Und warum implementieren Card.isVisibleTo(Player p), aber nicht Player.isVisibleTo(Card c)? Oder umgekehrt?

Ja, Sie können versuchen, eine Art Regel dafür zu finden, wie Sie es getan haben - Playerals höheres Niveau als ein Card(?) -, aber es ist nicht so einfach zu erraten, und ich muss an mehr als einer Stelle nachsehen um die Methode zu finden.

Im Laufe der Zeit kann es zu einem schlechten Designkompromiss bei der Implementierung isVisibleToauf beiden Card und auf der PlayerKlasse kommen, was meiner Meinung nach ein Nein-Nein ist. Warum so? Denn ich stelle mir schon den beschämenden Tag vor, an dem player1.isVisibleTo(card1)ein anderer Wert zurückkommt als card1.isVisibleTo(player1).ich denke - es ist subjektiv - dies sollte durch das Design unmöglich gemacht werden .

Gegenseitige Sichtbarkeit von Karten und Spieler sollten besser durch eine Art von einem Kontextobjekt geregelt werden - sei es Viewport, Dealoder Game.

Es ist nicht gleichbedeutend mit globalen Funktionen. Schließlich kann es viele gleichzeitige Spiele geben. Beachten Sie, dass dieselbe Karte für viele Tabellen gleichzeitig verwendet werden kann. Sollen wir Cardfür jedes Pik-As viele Instanzen erstellen ?

Ich könnte implementieren immer noch isVisibleToauf Card, aber ein Kontextobjekt , um es passieren und machen Carddelegieren die Abfrage. Programm zur Schnittstelle, um eine hohe Kopplung zu vermeiden.

Wie für Ihr zweites Beispiel - wenn die Dokument-ID nur aus a besteht BigDecimal, warum überhaupt eine Wrapper-Klasse dafür erstellen?

Ich würde sagen, Sie brauchen nur eine DocumentRepository.getDocument(BigDecimal documentID);

Übrigens gibt es structin C # zwar kein Java, aber s.

Sehen

als Referenz. Es ist eine sehr objektorientierte Sprache, aber niemand macht eine große Sache daraus.

Konrad Morawski
quelle
1
Nur eine Anmerkung zu Strukturen in C #: Sie sind keine typischen Strukturen, wie Sie sie in C kennen. Tatsächlich unterstützen sie OOP auch mit Vererbung, Kapselung und Polymorphismus. Neben einigen Besonderheiten besteht der Hauptunterschied darin, wie die Laufzeit Instanzen behandelt, wenn sie an andere Objekte übergeben werden: Strukturen sind Werttypen und Klassen sind Referenztypen!
Aschratt
3
@Aschratt: Strukturen unterstützen keine Vererbung. Strukturen können Schnittstellen implementieren, Strukturen, die Schnittstellen implementieren, verhalten sich jedoch anders als Klassenobjekte, die dies ebenfalls tun. Während es möglich ist, Strukturen so zu gestalten, dass sie sich wie Objekte verhalten, ist dies der beste Anwendungsfall für Strukturen, wenn man etwas möchte, das sich wie eine C-Struktur verhält, und die Dinge, die man einkapselt, entweder Primitive oder unveränderliche Klassentypen sind.
Supercat
1
+1 für den Grund, warum "es nicht gleichbedeutend ist, globale Funktionen zu haben." Dies wurde von anderen nicht viel angesprochen. (Auch wenn Sie über mehrere Decks verfügen, gibt eine globale Funktion immer noch unterschiedliche Werte für verschiedene Instanzen derselben Karte zurück.)
Alexis
@supercat Dies verdient eine separate Frage oder eine Chat-Sitzung, aber ich bin derzeit auch nicht daran interessiert :-( Sie sagen (in C #) "Strukturen, die Interfaces implementieren, verhalten sich anders als Klassenobjekte, die dasselbe tun". Ich stimme dem zu es gibt auch andere Verhaltensunterschiede zu berücksichtigen, aber AFAIK in folgenden Code Interface iObj = (Interface)obj;, der das Verhalten iObjder nicht betroffen ist structoder classStatus obj(außer dass es eine Box sein Exemplar bei dieser Zuordnung , wenn es sich um ein struct).
Mark Hurd
150

Die Grundidee hinter OOP ist, dass Daten und Verhalten (auf diesen Daten) untrennbar sind und sie durch die Idee eines Objekts einer Klasse gekoppelt sind.

Sie machen den allgemeinen Fehler anzunehmen, dass Klassen ein grundlegendes Konzept in OOP sind. Klassen sind nur ein besonders beliebter Weg, um eine Kapselung zu erreichen. Aber wir können zulassen, dass das abrutscht.

Angenommen, Sie haben eine allgemeine Kartenspiel-API. Sie haben eine Klassenkarte. Jetzt muss diese Kartenklasse die Sichtbarkeit für die Spieler bestimmen.

GUTE HIMMEL NR. Wenn du Bridge spielst, fragst du die Sieben der Herzen, wann es Zeit ist, die Hand des Dummys von einem Geheimnis, das nur dem Dummy bekannt ist, zu einem Geheimnis, das allen bekannt ist, zu ändern? Natürlich nicht. Das ist überhaupt kein Problem der Karte.

Eine Möglichkeit besteht darin, den Booleschen Wert isVisible (Player p) in der Kartenklasse zu haben. Eine andere Möglichkeit ist, in der Spielerklasse den Booleschen Wert isVisible (Karte c) zu haben.

Beide sind schrecklich; Tu beides nicht. Weder der Spieler noch die Karte sind für die Umsetzung der Bridge-Regeln verantwortlich!

Stattdessen habe ich mich für die dritte Option entschieden, bei der wir eine Viewport-Klasse haben, die bei einem bestimmten Spieler und einer Liste von Karten bestimmt, welche Karten sichtbar sind.

Ich habe noch nie Karten mit einem "Ansichtsfenster" gespielt, daher habe ich keine Ahnung, was diese Klasse verkapseln soll. Ich habe Karten mit ein paar Kartenspielen, einigen Spielern, einem Tisch und einer Kopie von Hoyle gespielt. Welches dieser Dinge repräsentiert Viewport?

Dieser Ansatz beraubt jedoch sowohl die Karten- als auch die Spielerklasse einer möglichen Mitgliedsfunktion.

Gut!

Wenn Sie dies für andere Dinge als die Sichtbarkeit von Karten tun, bleiben Ihnen Karten- und Player-Klassen, die reine Daten enthalten, übrig, da alle Funktionen in anderen Klassen implementiert sind, bei denen es sich zumeist um Klassen ohne Daten handelt, sondern nur um Methoden, wie im obigen Ansichtsfenster. Dies ist eindeutig gegen die Grundidee von OOP.

Nein; Die Grundidee von OOP ist, dass Objekte ihre Anliegen zusammenfassen . In Ihrem System kümmert sich eine Karte nicht viel. Weder ist ein Spieler. Dies liegt daran, dass Sie die Welt genau modellieren . In der realen Welt sind die Eigenschaften von Karten, die für ein Spiel relevant sind, außerordentlich einfach. Wir könnten die Bilder auf den Karten durch die Zahlen von 1 bis 52 ersetzen, ohne das Spiel zu verändern. Wir könnten die vier Leute durch Schaufensterpuppen mit den Bezeichnungen Nord, Süd, Ost und West ersetzen, ohne das Spiel zu verändern. Spieler und Karten sind die einfachsten Dinge in der Welt der Kartenspiele. Die Regeln sind kompliziert, und in der Klasse, die die Regeln darstellt, sollte die Komplikation liegen.

Wenn einer Ihrer Spieler eine KI ist, kann sein interner Zustand extrem kompliziert sein. Aber diese KI bestimmt nicht, ob sie eine Karte sehen kann. Die Regeln bestimmen das .

So würde ich Ihr System entwerfen.

Erstens sind Karten überraschend kompliziert, wenn es Spiele mit mehr als einem Deck gibt. Sie müssen sich die Frage stellen: Können die Spieler zwei Karten mit demselben Rang unterscheiden? Wenn Spieler 1 eines der sieben Herzen spielt und dann etwas passiert und Spieler 2 eines der sieben Herzen spielt, kann Spieler 3 dann feststellen, dass es die gleichen sieben Herzen waren? Betrachten Sie dies sorgfältig. Abgesehen von dieser Sorge sollten die Karten sehr einfach sein. Sie sind nur Daten.

Was ist nun die Natur eines Spielers? Ein Spieler verbraucht eine Folge sichtbarer Aktionen und führt eine Aktion aus .

Das Objekt rules ist das, was all dies koordiniert. Die Regeln erzeugen eine Abfolge von sichtbaren Aktionen und informieren die Spieler:

  • Spieler eins, die Zehn der Herzen wurde dir von Spieler drei ausgehändigt.
  • Spieler zwei, Spieler drei hat eine Karte an Spieler eins ausgehändigt.

Und bittet dann den Spieler um eine Aktion.

  • Spieler eins, was willst du machen?
  • Spieler eins sagt: verdreifache das fromp.
  • Spieler eins, das ist eine illegale Aktion, weil ein verdreifachter Fromp ein nicht zu rechtfertigendes Gambit erzeugt.
  • Spieler eins, was willst du machen?
  • Spieler eins sagt: Wirf die Pik Dame ab.
  • Spieler zwei, Spieler eins hat die Pik Dame abgeworfen.

Und so weiter.

Trennen Sie Ihre Mechanismen von Ihren Richtlinien . Die Richtlinien des Spiels sollten in einem Richtlinienobjekt und nicht in den Karten enthalten sein . Die Karten sind nur ein Mechanismus.

Eric Lippert
quelle
41
@gnat: Eine gegensätzliche Meinung von Alan Kay ist "Eigentlich habe ich den Begriff" objektorientiert " erfunden , und ich kann Ihnen sagen, dass ich C ++ nicht im Sinn hatte." Es gibt OO-Sprachen ohne Klassen; JavaScript fällt mir ein.
Eric Lippert
19
@gnat: Ich würde zustimmen, dass JS, wie es heute ist, kein großartiges Beispiel für eine OOP-Sprache ist, aber es zeigt, dass man eine OO-Sprache ohne Klassen ziemlich leicht aufbauen kann. Ich stimme zu, dass die grundlegende Einheit von OO-ness in Eiffel und C ++ beide die Klasse ist; Wo ich nicht einverstanden bin, ist die Vorstellung, dass Klassen die unabdingbare Voraussetzung für OO sind. Die unabdingbare Voraussetzung für OO sind Objekte, die Verhalten einschließen und über eine genau definierte öffentliche Schnittstelle miteinander kommunizieren.
Eric Lippert
16
Ich stimme mit EricLippert überein, Klassen sind weder für OO grundlegend noch für die Vererbung, was auch immer der Mainstream sagen mag. Die Kapselung von Daten, Verhalten und Verantwortung ist jedoch erreicht. Es gibt prototypbasierte Sprachen jenseits von Javascript, die OO, aber klassenlos sind. Es ist besonders ein Fehler, sich auf die Vererbung dieser Konzepte zu konzentrieren. Das heißt, Klassen sind sehr nützliche Mittel, um die Einkapselung von Verhalten zu organisieren. Dass Sie Klassen als Objekte behandeln können (und umgekehrt in Prototypensprachen), lässt die Linie verschwimmen.
Schwern
6
Betrachten Sie es so: Welches Verhalten zeigt eine tatsächliche Karte in der realen Welt für sich? Ich denke die Antwort ist "keine". Andere Dinge wirken sich auf die Karte aus. Die Karte selbst, in der realen Welt, buchstäblich ist nur Informationen (4 von Vereinen), ohne intrinsisches Verhalten jeglicher Art. Wie diese Informationen (auch "eine Karte" genannt) verwendet werden, hängt zu 100% von etwas / jemand anderem ab, den "Regeln" und den "Spielern". Dieselben Karten können für eine unendliche (naja, vielleicht nicht ganz) Vielzahl verschiedener Spiele von einer beliebigen Anzahl verschiedener Spieler verwendet werden. Eine Karte ist nur eine Karte, und alles, was sie hat, sind Eigenschaften.
Craig
5
@Montagist: Lass mich dann die Dinge ein bisschen klarstellen. Betrachten Sie C. Ich denke, Sie stimmen zu, dass C keine Klassen hat. Sie könnten jedoch sagen, dass Strukturen "Klassen" sind, Sie könnten Felder vom Typ Funktionszeiger erstellen, Sie könnten vtables erstellen, Sie könnten Methoden namens "Konstruktoren" erstellen, die die vtables so einrichten, dass einige Strukturen voneinander "geerbt" werden. und so weiter. Sie können die klassenbasierte Vererbung in C emulieren und Sie können sie in JS emulieren . Dies bedeutet jedoch, dass Sie etwas auf der Sprache aufbauen, die noch nicht vorhanden ist.
Eric Lippert
29

Sie haben Recht, dass die Kopplung von Daten und Verhalten die zentrale Idee von OOP ist, aber es steckt noch mehr dahinter. Beispiel: Kapselung : OOP / modulare Programmierung ermöglicht es uns, eine öffentliche Schnittstelle von Implementierungsdetails zu trennen. In OOP bedeutet dies, dass die Daten niemals öffentlich zugänglich sein sollten und nur über Accessoren verwendet werden sollten. Nach dieser Definition ist ein Objekt ohne Methoden in der Tat unbrauchbar.

Eine Klasse, die keine anderen Methoden als Accessoren bietet, ist im Wesentlichen eine überkomplizierte Struktur. Dies ist jedoch nicht schlecht, da OOP Ihnen die Flexibilität gibt, interne Details zu ändern, was eine Struktur nicht tut. Anstatt beispielsweise einen Wert in einem Mitgliedsfeld zu speichern, könnte er jedes Mal neu berechnet werden. Oder ein Backing-Algorithmus wird geändert und damit der Zustand, der verfolgt werden muss.

Während OOP einige klare Vorteile hat (insbesondere gegenüber der einfachen prozeduralen Programmierung), ist es naiv, nach "reinem" OOP zu streben. Einige Probleme lassen sich nicht gut auf einen objektorientierten Ansatz abbilden und lassen sich leichter durch andere Paradigmen lösen. Wenn Sie auf ein solches Problem stoßen, bestehen Sie nicht auf einem minderwertigen Ansatz.

  • Erwägen Sie, die Fibonacci-Sequenz objektorientiert zu berechnen . Ich kann mir keinen vernünftigen Weg vorstellen, das zu tun. einfache strukturierte Programmierung bietet die beste Lösung für dieses Problem.

  • Ihre isVisibleBeziehung gehört zu beiden Klassen oder zu keiner oder tatsächlich: zum Kontext . Verhaltenslose Datensätze sind typisch für einen funktionalen oder prozeduralen Programmieransatz, der am besten zu Ihrem Problem zu passen scheint. Es ist nichts falsch mit

    static boolean isVisible(Card c, Player p);
    

    und es ist nichts falsch daran Card, keine Methoden jenseits rankund suitAccessoren zu haben.

amon
quelle
11
@UMad ja, das ist genau mein Punkt, und daran ist nichts auszusetzen. Verwenden Sie die richtige <del> Sprache </ del> Paradigma für den Job bei der Hand. (Die meisten Sprachen außer Smalltalk sind übrigens nicht rein objektorientiert. Beispielsweise unterstützen Java, C # und C ++ die imperative, strukturierte, prozedurale, modulare, funktionale und objektorientierte Programmierung. All diese Nicht-OO-Paradigmen sind aus einem bestimmten Grund verfügbar : damit du sie benutzen kannst)
amon
1
Es gibt eine sinnvolle OO-Methode, um Fibonacci auszuführen. Rufen Sie die fibonacciMethode für eine Instanz von auf integer. Ich möchte Ihren Punkt betonen, dass es bei OO um die Verkapselung geht, auch an scheinbar kleinen Orten. Lassen Sie die Ganzzahl herausfinden, wie die Arbeit erledigt wird. Später können Sie die Implementierung verbessern und Caching hinzufügen, um die Leistung zu verbessern. Im Gegensatz zu Funktionen folgen Methoden den Daten, sodass alle Aufrufer von einer verbesserten Implementierung profitieren. Möglicherweise werden Ganzzahlen mit beliebiger Genauigkeit später hinzugefügt. Sie können transparent wie normale Ganzzahlen behandelt werden und über eine eigene fibonacciMethode zur Leistungsoptimierung verfügen .
Schwern
2
@Schwern Wenn etwas Fibonaccieine Unterklasse der abstrakten Klasse ist Sequence, wird die Sequenz von einer beliebigen Zahlenmenge verwendet und ist für das Speichern von Startwerten, Status, Caching und einem Iterator verantwortlich.
George Reith
2
Ich hatte nicht erwartet, dass "pure OOP Fibonacci" so effektiv beim Nerd-Sniping ist . Bitte beenden Sie in diesen Kommentaren jegliche Zirkeldiskussion darüber, obwohl sie einen gewissen Unterhaltungswert hatte. Lassen Sie uns zur Abwechslung etwas Konstruktives tun!
Amon
3
Es wäre dumm, Fibonacci zu einer Methode für ganze Zahlen zu machen, nur um zu sagen, dass es OOP ist. Es ist eine Funktion und sollte wie eine Funktion behandelt werden.
immibis
19

Die Grundidee hinter OOP ist, dass Daten und Verhalten (auf diesen Daten) untrennbar sind und sie durch die Idee eines Objekts einer Klasse gekoppelt sind. Objekte haben Daten und Methoden, die damit arbeiten (und andere Daten). Offensichtlich werden nach den Prinzipien von OOP Objekte, die nur Daten sind (wie C-Strukturen), als Anti-Muster betrachtet. (...) Dies ist eindeutig gegen die Grundidee von OOP.

Dies ist eine schwierige Frage, da sie auf einigen fehlerhaften Prämissen beruht:

  1. Die Idee, dass OOP der einzig gültige Weg ist, um Code zu schreiben.
  2. Die Idee, dass OOP ein klar definiertes Konzept ist. Es ist so ein Modewort geworden, dass es schwierig ist, zwei Leute zu finden, die sich darüber einigen können, worum es bei OOP geht.
  3. Bei OOP geht es um die Bündelung von Daten und Verhalten.
  4. Die Idee, dass alles eine Abstraktion ist / sein sollte.

Ich werde nicht viel auf # 1-3 eingehen, da jeder seine eigene Antwort hervorbringen könnte und dies zu vielen meinungsbasierten Diskussionen einlädt. Besonders beunruhigend finde ich jedoch die Idee, dass es bei OOP um die Kopplung von Daten und Verhalten geht. Dies führt nicht nur zu # 4, sondern auch zu der Idee, dass alles eine Methode sein sollte.

Es gibt einen Unterschied zwischen den Operationen, die einen Typ definieren, und den Möglichkeiten, wie Sie diesen Typ verwenden können. Die Fähigkeit, das ith-Element abzurufen , ist für das Konzept eines Arrays von entscheidender Bedeutung, aber das Sortieren ist nur eines der vielen Dinge, die ich mit einem tun kann. Das Sortieren muss nicht mehr eine Methode sein als "ein neues Array erstellen, das nur die geraden Elemente enthält".

Bei OOP geht es um die Verwendung von Objekten. Objekte sind nur ein Weg zur Abstraktion . Abstraktion ist ein Mittel, um unnötige Kopplungen in Ihrem Code zu vermeiden und kein Selbstzweck. Wenn Ihre Vorstellung von einer Karte nur durch den Wert ihrer Suite und ihres Ranges definiert ist, ist es in Ordnung, sie als einfaches Tupel oder Datensatz zu implementieren. Es gibt keine unwesentlichen Details, von denen ein anderer Teil des Codes eine Abhängigkeit bilden könnte. Manchmal hast du einfach nichts zu verbergen.

Sie würden keine isVisibleMethode dieser CardArt entwickeln, da es für Ihre Vorstellung von einer Karte wahrscheinlich nicht wesentlich ist, sichtbar zu sein (es sei denn, Sie haben sehr spezielle Karten, die durchscheinend oder undurchsichtig werden können ...). Sollte es eine Methode des PlayerTyps sein? Nun, das ist wahrscheinlich auch keine definierende Eigenschaft der Spieler. Sollte es ein Teil irgendeiner ViewportArt sein? Dies hängt wiederum davon ab, wie Sie ein Ansichtsfenster definieren und ob die Prüfung der Sichtbarkeit von Karten ein wesentlicher Bestandteil der Definition eines Ansichtsfensters ist.

Es ist sehr viel möglich, isVisiblesollte nur eine freie Funktion sein.

Doval
quelle
1
+1 für gesunden Menschenverstand statt sinnloses Dröhnen.
Rightfold
Aus den Zeilen, die ich gelesen habe, sieht der Essay, den Sie verlinkt haben, so aus, als hätte ich ihn eine Weile nicht mehr gelesen.
Arthur Havlicek
@ArthurHavlicek Es ist schwieriger zu folgen, wenn Sie die im Codebeispiel verwendeten Sprachen nicht verstehen, aber ich fand es ziemlich aufschlussreich.
Doval
9

Offensichtlich werden nach den Prinzipien von OOP Objekte, die nur Daten sind (wie C-Strukturen), als Anti-Muster betrachtet.

Nein, das sind sie nicht. Plain-Old-Data-Objekte sind ein vollkommen gültiges Muster, und ich würde sie in jedem Programm erwarten, das sich mit Daten befasst, die zwischen bestimmten Bereichen Ihres Programms persistent sein oder kommuniziert werden müssen.

Während Ihre Datenebene beim Einlesen aus der Tabelle möglicherweise eine vollständige PlayerKlasse Playersspoolt, kann es sich stattdessen nur um eine allgemeine Datenbibliothek handeln, die einen POD mit den Feldern aus der Tabelle zurückgibt, der an einen anderen Bereich Ihres Programms übergeben wird, der konvertiert ein Spieler POD zu Ihrer konkreten PlayerKlasse.

Die Verwendung von Datenobjekten, entweder typisiert oder nicht typisiert, ist in Ihrem Programm möglicherweise nicht sinnvoll, macht sie jedoch nicht zu einem Anti-Pattern. Wenn sie sinnvoll sind, verwenden Sie sie, und wenn nicht, tun Sie es nicht.

DougM
quelle
5
Widersprechen Sie nichts, was Sie gesagt haben, aber dies beantwortet die Frage überhaupt nicht. Das heißt, ich beschuldige die Frage mehr als die Antwort.
pdr
2
Genau genommen sind Karten und Dokumente auch in der realen Welt nur Informationsbehälter, und jedes "Muster", das damit nicht umgehen kann, muss ignoriert werden.
JeffO
1
Plain-Old-Data objects are a perfectly valid pattern Ich habe nicht gesagt, dass dies nicht der Fall ist. Ich sage, es ist falsch, wenn sie die gesamte untere Hälfte der Anwendung ausfüllen.
RokL
8

Persönlich denke ich, dass Domain Driven Design dazu beiträgt, Klarheit in dieses Problem zu bringen. Die Frage, die ich stelle, ist, wie beschreibe ich das Kartenspiel den Menschen? Mit anderen Worten, was modelliere ich? Wenn das, was ich modelliere, wirklich das Wort "Ansichtsfenster" und ein Konzept enthält, das seinem Verhalten entspricht, dann würde ich das Ansichtsfensterobjekt erstellen und es tun lassen, was es logisch sollte.

Wenn ich jedoch das Konzept des Ansichtsfensters nicht in meinem Spiel habe, und es ist etwas, von dem ich denke, dass ich es brauche, weil sich der Code sonst "falsch anfühlt". Ich denke zweimal darüber nach, mein Domain-Modell hinzuzufügen.

Das Wort Modell bedeutet, dass Sie eine Darstellung von etwas erstellen. Ich warne davor, eine Klasse einzufügen, die etwas Abstraktes darstellt, das über das hinausgeht, was Sie darstellen.

Ich werde hinzufügen, dass es möglich ist, dass Sie das Konzept eines Ansichtsfensters in einem anderen Teil Ihres Codes benötigen, wenn Sie eine Schnittstelle mit einer Anzeige benötigen. In DDD-Begriffen wäre dies jedoch ein Infrastrukturproblem und würde außerhalb des Domänenmodells bestehen.

RibaldEddie
quelle
Die Antwort von Lippert oben ist ein besseres Beispiel für dieses Konzept.
RibaldEddie
5

Normalerweise mache ich keine Eigenwerbung, aber Tatsache ist, dass ich viel über OOP-Designprobleme in meinem Blog geschrieben habe . Um mehrere Seiten zusammenzufassen: Sie sollten nicht mit Klassen beginnen. Beginnend mit Interfaces oder APIs und Shape-Code von dort haben Sie höhere Chancen, aussagekräftige Abstraktionen bereitzustellen, Spezifikationen anzupassen und zu vermeiden, dass konkrete Klassen mit nicht wiederverwendbarem Code überladen werden.

Wie dies anzuwenden Card- PlayerProblem: Erstellen einer ViewPortAbstraktion Sinn macht , wenn Sie denken Cardund Playerals zwei unabhängige Bibliotheken zu sein (was bedeuten würde , Playermanchmal ohne verwendet wird Card). Ich bin jedoch geneigt zu denken , eine Playerhält Cardsund bieten sollte Collection<Card> getVisibleCards ()Accessor zu ihnen. Diese beiden Lösungen ( ViewPortund meine) sind besser als das Bereitstellen isVisibleeiner Methode Cardoder Playerim Hinblick auf das Erstellen verständlicher Codebeziehungen.

Eine Lösung von außerhalb der Klasse ist viel, viel besser für die DocumentId. Es gibt wenig Motivation, eine komplexe Datenbankbibliothek (im Grunde genommen eine Ganzzahl) abhängig zu machen.

Arthur Havlicek
quelle
Ich mag deinen Blog.
RokL
3

Ich bin nicht sicher, ob die vorliegende Frage auf der richtigen Ebene beantwortet wird. Ich hatte den Weisen im Forum dringend gebeten, hier aktiv über den Kern der Frage nachzudenken.

U Mad führt eine Situation an, in der er glaubt, dass die Programmierung nach seinem Verständnis von OOP im Allgemeinen dazu führen würde, dass viele Blattknoten Daten enthalten, während seine API der oberen Ebene den größten Teil des Verhaltens ausmacht.

Ich denke, das Thema ging ein wenig tangential dahin, ob isVisible für Card vs Player definiert wird. es war nur ein illustriertes Beispiel, wenn auch naiv.

Ich hatte die hier erfahrene drängen, um das vorliegende Problem zu betrachten. Ich denke, es gibt eine gute Frage, auf die U Mad gedrängt hat. Ich verstehe, dass Sie die Regeln und die betreffende Logik auf ein eigenes Objekt übertragen würden. aber wie ich verstehe ist die frage

  1. Ist es in Ordnung, einfache Datenbehälterkonstrukte (Klassen / Strukturen; es ist mir egal, wie sie für diese Frage modelliert sind) zu haben, die nicht wirklich viel Funktionalität bieten?
  2. Wenn ja, wie lassen sie sich am besten oder am besten modellieren?
  3. Wenn nein, wie integrieren wir diese Datengegenstücke in höhere API-Klassen (einschließlich Verhalten)?

Meine Sicht:

Ich denke, Sie stellen eine Frage der Granularität, die in der objektorientierten Programmierung nur schwer richtig zu stellen ist. Nach meiner kleinen Erfahrung würde ich keine Entität in mein Modell aufnehmen, die selbst kein Verhalten enthält. Wenn ich müsste, hätte ich wahrscheinlich eine Struktur verwendet, die so konzipiert ist, dass sie eine solche Abstraktion enthält, im Gegensatz zu einer Klasse, die die Idee hat, Daten und Verhalten zu kapseln.

Harsha
quelle
3
Das Problem ist, dass es bei der Frage (und Ihrer Antwort) darum geht, wie man Dinge "allgemein" macht. Tatsache ist, wir machen niemals Dinge "im Allgemeinen". Wir machen immer bestimmte Dinge. Es ist notwendig, unsere spezifischen Dinge zu untersuchen und sie an unseren Anforderungen zu messen, um festzustellen, ob unsere spezifischen Dinge die richtigen Dinge für die Situation sind.
John Saunders
@JohnSaunders Ich nehme Ihre Weisheit hier wahr und stimme in gewissem Umfang zu, aber es ist auch nur ein konzeptioneller Ansatz erforderlich, bevor Sie ein Problem angehen. Immerhin ist die Frage hier nicht so offen, wie es scheint. Ich denke, es ist eine gültige OOD-Frage, mit der jeder OO-Designer in den anfänglichen Verwendungen von OOP konfrontiert ist. Was ist deine Meinung? Wenn eine Konkretisierung hilft, können wir besprechen, ein Beispiel Ihrer Wahl zu bauen.
Harsha
Ich bin jetzt seit über 35 Jahren nicht mehr in der Schule. In der realen Welt finde ich sehr wenig Wert in "konzeptuellen Ansätzen". In diesem Fall empfinde ich Erfahrung als einen besseren Lehrer als Meyers.
John Saunders
Ich verstehe die Klasse für Daten und die Klasse für Verhaltensunterscheidung nicht wirklich. Wenn Sie Ihre Objekte richtig abstrahieren, gibt es keinen Unterschied. Stellen Sie sich eine Pointmit getX()Funktion vor. Sie könnten sich vorstellen, dass es eines seiner Attribute bekommt, aber es könnte auch von der Festplatte oder aus dem Internet gelesen werden. Erhalten und Setzen ist Verhalten, und Klassen zu haben, die genau das tun, ist völlig in Ordnung. Datenbanken erhalten und setzen nur Daten fwiw
Arthur Havlicek
@ArthurHavlicek: Zu wissen, was eine Klasse nicht tun wird, ist oft genauso nützlich wie zu wissen, was sie tun wird. Es ist nützlich, wenn in seinem Vertrag festgelegt ist, dass er sich nur als gemeinsam nutzbarer, unveränderlicher Dateninhaber oder als nicht gemeinsam nutzbarer, veränderlicher Dateninhaber verhält.
Supercat
2

Eine häufige Ursache für Verwirrung in OOP ist die Tatsache, dass viele Objekte zwei Aspekte des Zustands einschließen: die Dinge, die sie kennen, und die Dinge, die sie kennen. Diskussionen über den Objektzustand ignorieren häufig den letzteren Aspekt, da es in Frameworks, in denen Objektreferenzen promiskuitiv sind, keine allgemeine Möglichkeit gibt, zu bestimmen, was die Dinge über ein Objekt wissen könnten, dessen Referenz jemals der Außenwelt ausgesetzt war.

Ich würde vorschlagen, dass es wahrscheinlich hilfreich wäre, ein CardEntityObjekt zu haben, das diese Aspekte der Karte in separaten Komponenten kapselt. Eine Komponente würde sich auf die Markierungen auf der Karte beziehen (z. B. "Diamantenkönig" oder "Lavaschlag; Spieler haben die AC-3-Chance auszuweichen oder 2D6-Schaden zu erleiden"). Man könnte sich auf einen einzigartigen Aspekt des Zustands beziehen, wie z. B. die Position (z. B. im Deck oder in Joes Hand oder auf dem Tisch vor Larry). Ein dritter könnte sich darauf beziehen, kann es sehen (vielleicht niemand, vielleicht ein Spieler oder vielleicht viele Spieler). Um sicherzustellen, dass alles synchron bleibt, werden Orte, an denen sich eine Karte möglicherweise befindet, nicht als einfache Felder, sondern als CardSpaceObjekte eingekapselt . um eine Karte auf ein Feld zu verschieben, würde man ihr einen Verweis auf das richtige gebenCardSpaceObjekt; es würde sich dann aus dem alten Raum entfernen und sich in den neuen Raum versetzen).

Die explizite Verkapselung von "Wer weiß über X Bescheid?" Es ist manchmal Vorsicht geboten, um Speicherlecks zu vermeiden, insbesondere bei vielen Assoziationen (z. B. wenn neue Karten entstehen und alte Karten verschwinden). Man muss sicherstellen, dass Karten, die aufgegeben werden sollten, nicht dauerhaft an langlebigen Objekten hängen bleiben ) aber wenn das Vorhandensein von Referenzen auf ein Objekt einen relevanten Teil seines Zustands ausmacht, ist es völlig richtig, dass das Objekt selbst solche Informationen explizit kapselt (selbst wenn es die eigentliche Verwaltung an eine andere Klasse delegiert).

Superkatze
quelle
0

Dieser Ansatz beraubt jedoch sowohl die Karten- als auch die Spielerklasse einer möglichen Mitgliedsfunktion.

Und wie ist das schlecht / schlecht beraten?

Um eine ähnliche Analogie zu Ihrem Kartenbeispiel zu verwenden, betrachten Sie a Car, a Driverund Sie müssen feststellen, ob das DriverLaufwerk das kann Car.

OK, also hast du entschieden, dass du nicht Carwissen willst , ob das DriverAuto den richtigen Autoschlüssel hat oder nicht, und aus einem unbekannten Grund hast du auch beschlossen, dass du nicht Driverüber die CarKlasse Bescheid wissen willst (du bist nicht ganz Fleisch geworden) dies auch in Ihrer ursprünglichen Frage). Daher haben Sie eine Zwischenklasse, die der UtilsKlasse ähnelt und die Methode mit Geschäftsregeln enthält , um einen booleanWert für die obige Frage zurückzugeben.

Ich denke das ist in Ordnung. Die Zwischenklasse muss jetzt möglicherweise nur noch nach Autoschlüsseln suchen, kann jedoch überarbeitet werden, um zu prüfen, ob der Fahrer einen gültigen Führerschein besitzt, unter Alkoholeinfluss steht oder in einer dystopischen Zukunft auf DNA-Biometrie prüft. Durch die Verkapselung gibt es wirklich keine großen Probleme, wenn diese drei Klassen zusammen existieren.

hjk
quelle