In altem Code sehe ich gelegentlich Klassen, die nichts als Wrapper für Daten sind. so etwas wie:
class Bottle {
int height;
int diameter;
Cap capType;
getters/setters, maybe a constructor
}
Mein Verständnis von OO ist, dass Klassen Strukturen für Daten und die Methoden zur Verarbeitung dieser Daten sind. Dies scheint Objekte dieses Typs auszuschließen. Für mich sind sie nichts anderes als eine structs
Art Niederlage des Zwecks von OO. Ich denke nicht, dass es notwendigerweise böse ist, obwohl es ein Codegeruch sein kann.
Gibt es einen Fall, in dem solche Objekte notwendig wären? Wenn dies häufig verwendet wird, lässt es das Design vermuten?
java
code-quality
object-oriented
code-smell
Michael K
quelle
quelle
Antworten:
Auf keinen Fall böse und kein Codegeruch in meinem Kopf. Datencontainer sind gültige OO-Bürger. Manchmal möchten Sie zusammengehörige Informationen zusammenfassen. Es ist viel besser, eine Methode wie diese zu haben
als
Mit einer Klasse können Sie auch einen zusätzlichen Parameter zu Bottle hinzufügen, ohne jeden Aufrufer von DoStuffWithBottle zu ändern. Außerdem können Sie Bottle in Unterklassen unterteilen und die Lesbarkeit und Organisation Ihres Codes bei Bedarf weiter verbessern.
Es gibt auch einfache Datenobjekte, die beispielsweise als Ergebnis einer Datenbankabfrage zurückgegeben werden können. Ich glaube, dass der Begriff für sie in diesem Fall "Datenübertragungsobjekt" ist.
In einigen Sprachen gibt es auch andere Überlegungen. Beispielsweise verhalten sich in C # Klassen und Strukturen unterschiedlich, da Strukturen ein Werttyp und Klassen Referenztypen sind.
quelle
DoStuffWith
sollte ein Verfahren von derBottle
Klasse in OOP (und soll vermutlich unveränderlich sein, auch). Was Sie oben geschrieben haben, ist kein gutes Muster in einer OO-Sprache (es sei denn, Sie arbeiten mit einer älteren API zusammen). Es ist jedoch ein gültiger Entwurf in einer Nicht-OO-Umgebung.In einigen Fällen sind Datenklassen gültig. DTOs sind ein gutes Beispiel, das von Anna Lear erwähnt wird. Im Allgemeinen sollten Sie sie jedoch als den Keim einer Klasse betrachten, deren Methoden noch nicht entstanden sind. Und wenn Sie in altem Code auf viele davon stoßen, behandeln Sie sie als starken Codegeruch. Sie werden häufig von alten C / C ++ - Programmierern verwendet, die noch nie auf die OO-Programmierung umgestiegen sind und ein Zeichen für die prozedurale Programmierung sind. Wenn Sie sich die ganze Zeit auf Getter und Setter verlassen (oder, noch schlimmer, auf den direkten Zugriff nicht privater Mitglieder), können Sie Probleme bekommen, bevor Sie es merken. Stellen Sie sich ein Beispiel für eine externe Methode vor, für die Informationen von benötigt werden
Bottle
.Hier
Bottle
ist eine Datenklasse:Hier haben wir
Bottle
etwas Verhalten gegeben):Das erste Verfahren gegen die Tells-nicht-Frage Prinzip, und indem sie
Bottle
stumm wir haben implizites Wissen über Flaschen lassen, wie zum Beispiel , was macht einen Fragle (dieCap
), Schlicker in einer Logik , die außerhalb dem istBottle
Klasse. Sie müssen auf der Hut sein, um diese Art von "Leckage" zu verhindern, wenn Sie sich gewöhnlich auf Getter verlassen.Die zweite Methode fragt
Bottle
nur nach dem, was sie für ihre Arbeit benötigt, undBottle
entscheidet, ob sie zerbrechlich oder größer als eine bestimmte Größe ist. Das Ergebnis ist eine viel lockere Kopplung zwischen der Methode und der Implementierung von Bottle. Ein angenehmer Nebeneffekt ist, dass die Methode sauberer und ausdrucksvoller ist.Sie werden selten von so vielen Feldern eines Objekts Gebrauch machen, ohne eine Logik zu schreiben, die sich in der Klasse mit diesen Feldern befinden sollte. Finde heraus, was diese Logik ist und verschiebe sie dahin, wo sie hingehört.
quelle
Wenn Sie so etwas brauchen, ist das in Ordnung, aber bitte, bitte, bitte machen Sie es so
anstelle von so etwas wie
Bitte.
quelle
Wie @Anna sagte, definitiv nicht böse. Sicher können Sie Operationen (Methoden) in Klassen einfügen, aber nur, wenn Sie möchten . Sie müssen nicht haben zu.
Erlauben Sie mir einen kleinen Kritikpunkt über die Idee, dass Sie Operationen in Klassen einordnen müssen, zusammen mit der Idee, dass Klassen Abstraktionen sind. In der Praxis ermutigt dies Programmierer dazu
Machen Sie mehr Klassen als nötig (redundante Datenstruktur). Wenn eine Datenstruktur mehr Komponenten als nur minimal erforderlich enthält, ist sie nicht normalisiert und enthält daher inkonsistente Zustände. Mit anderen Worten, wenn es geändert wird, muss es an mehr als einer Stelle geändert werden, um konsistent zu bleiben. Wenn nicht alle koordinierten Änderungen durchgeführt werden, ist dies inkonsistent und ein Fehler.
Beheben Sie Problem 1, indem Sie Benachrichtigungsmethoden einfügen. Wenn Teil A geändert wird, werden die erforderlichen Änderungen an Teil B und C weitergegeben. Dies ist der Hauptgrund, weshalb die Verwendung von get-and-set-Zugriffsmethoden empfohlen wird. Da dies eine empfohlene Vorgehensweise ist, scheint sie Problem 1 zu entschuldigen, was mehr zu Problem 1 und mehr zu Lösung 2 führt. Dies führt nicht nur zu Fehlern aufgrund der unvollständigen Implementierung der Benachrichtigungen, sondern auch zu einem leistungsbeeinträchtigenden Problem bei außer Kontrolle geratenen Benachrichtigungen. Dies sind keine unendlichen Berechnungen, sondern nur sehr lange.
Diese Konzepte werden als gute Dinge gelehrt, im Allgemeinen von Lehrern, die nicht in Monster-Apps mit Millionen von Zeilen arbeiten mussten, die sich mit diesen Problemen befassen.
Folgendes versuche ich zu tun:
Halten Sie die Daten so normal wie möglich, damit Änderungen an den Daten an möglichst wenigen Codepunkten vorgenommen werden, um die Wahrscheinlichkeit zu minimieren, in einen inkonsistenten Zustand zu wechseln.
Verwenden Sie Benachrichtigungen nicht, wenn Daten nicht normalisiert werden müssen und Redundanz unvermeidbar ist, um die Konsistenz zu gewährleisten. Dulden Sie eher vorübergehende Inkonsistenzen. Beheben Sie Inkonsistenzen mit periodischen Durchläufen der Daten durch einen Prozess, der nur dies tut. Dies zentralisiert die Verantwortung für die Aufrechterhaltung der Konsistenz und vermeidet gleichzeitig die Leistungs- und Korrektheitsprobleme, für die Benachrichtigungen anfällig sind. Dies führt zu Code, der viel kleiner, fehlerfrei und effizienter ist.
quelle
Diese Art von Klassen ist aus folgenden Gründen sehr nützlich, wenn Sie sich mit mittelgroßen / großen Anwendungen befassen:
Zusammenfassend sind sie meiner Erfahrung nach in der Regel nützlicher als ärgerlich.
quelle
Beim Spieledesign lohnt es sich manchmal, Klassen zu haben, die nur Daten speichern, und andere Klassen, die alle Nur-Daten-Klassen durchlaufen, um die Logik auszuführen.
quelle
Stimme dem Anna Lear zu,
Manchmal vergessen die Leute, die Java-Kodierungskonventionen von 1999 zu lesen, was sehr deutlich macht, dass diese Art der Programmierung vollkommen in Ordnung ist. In der Tat, wenn Sie es vermeiden, dann wird Ihr Code riechen! (zu viele Getter / Setter)
Aus Java Code Conventions 1999: Ein Beispiel für geeignete öffentliche Instanzvariablen ist der Fall, in dem die Klasse im Wesentlichen eine Datenstruktur ohne Verhalten ist. Mit anderen Worten, wenn Sie eine Struktur anstelle einer Klasse verwendet hätten (sofern Java diese unterstützt), ist es angebracht, die Instanzvariablen der Klasse öffentlich zu machen. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177
Bei richtiger Verwendung sind PODs (einfache alte Datenstrukturen) besser als POJOs, genauso wie POJOs oft besser als EJBs sind.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures
quelle
Strukturen haben auch in Java ihren Platz. Sie sollten sie nur verwenden, wenn die folgenden beiden Dinge zutreffen:
Wenn dies der Fall ist, sollten Sie die Felder öffentlich machen und die Getter / Setter überspringen. Getter und Setter sind sowieso klobig, und Java ist albern, weil es keine Eigenschaften wie eine nützliche Sprache hat. Da Ihr strukturähnliches Objekt ohnehin keine Methoden haben sollte, sind öffentliche Felder am sinnvollsten.
Wenn jedoch eine dieser Bedingungen nicht zutrifft, haben Sie es mit einer echten Klasse zu tun. Das bedeutet, dass alle Felder privat sein sollten. (Wenn Sie unbedingt ein Feld mit einem besser zugänglichen Bereich benötigen, verwenden Sie einen Getter / Setter.)
Um zu überprüfen, ob sich Ihre mutmaßliche Struktur verhält, prüfen Sie, wann die Felder verwendet werden. Wenn es gegen Tell zu verstoßen scheint, fragen Sie nicht , dann müssen Sie dieses Verhalten in Ihre Klasse verschieben.
Wenn sich einige Ihrer Daten nicht ändern sollten, müssen Sie alle Felder endgültig ausfüllen. Sie könnten erwägen, Ihre Klasse unveränderlich zu machen . Wenn Sie Ihre Daten validieren müssen, geben Sie die Validierung in den Setters und Konstruktoren an. (Ein nützlicher Trick besteht darin, einen privaten Setter zu definieren und Ihr Feld in Ihrer Klasse nur mit diesem Setter zu ändern.)
Ihr Bottle-Beispiel würde höchstwahrscheinlich beide Tests nicht bestehen. Sie könnten (erfundenen) Code haben, der so aussieht:
Stattdessen sollte es sein
Wenn Sie die Höhe und den Durchmesser ändern würden, wäre es die gleiche Flasche? Wahrscheinlich nicht. Die sollten endgültig sein. Ist ein negativer Wert für den Durchmesser in Ordnung? Muss Ihre Flasche größer sein als breit? Kann das Cap null sein? Nein? Wie validieren Sie das? Angenommen, der Kunde ist entweder dumm oder böse. ( Es ist unmöglich, den Unterschied zu erkennen. ) Sie müssen diese Werte überprüfen.
So könnte Ihre neue Flaschenklasse aussehen:
quelle
IMHO gibt es oft nicht genug Klassen wie diese in stark objektorientierten Systemen. Ich muss das sorgfältig abschätzen.
Wenn die Datenfelder einen großen Umfang und eine große Sichtbarkeit haben, kann dies natürlich äußerst unerwünscht sein, wenn in Ihrer Codebasis Hunderte oder Tausende Stellen vorhanden sind, an denen solche Daten manipuliert werden. Das verlangt nach Schwierigkeiten und Schwierigkeiten, Invarianten aufrechtzuerhalten. Gleichzeitig bedeutet dies jedoch nicht, dass jede einzelne Klasse in der gesamten Codebasis vom Verbergen von Informationen profitiert.
Es gibt jedoch viele Fälle, in denen solche Datenfelder einen sehr engen Bereich haben. Ein sehr einfaches Beispiel ist eine private
Node
Klasse einer Datenstruktur. Es kann den Code oftmals erheblich vereinfachen, indem die Anzahl der laufenden Objektinteraktionen verringert wird, wenn dieseNode
einfach aus Rohdaten bestehen könnten. Dies dient als Entkopplungsmechanismus, da die alternative Version möglicherweise eine bidirektionale Kopplung beispielsweise vonTree->Node
undNode->Tree
im Gegensatz zu einfach erfordertTree->Node Data
.Ein komplexeres Beispiel wären Entity-Component-Systeme, wie sie häufig in Game-Engines verwendet werden. In diesen Fällen handelt es sich bei den Komponenten häufig nur um Rohdaten und Klassen wie die, die Sie gezeigt haben. Ihr Umfang / ihre Sichtbarkeit ist jedoch tendenziell begrenzt, da normalerweise nur ein oder zwei Systeme auf diesen bestimmten Komponententyp zugreifen können. Infolgedessen fällt es Ihnen immer noch leicht, Invarianten in diesen Systemen zu verwalten, und darüber hinaus treten bei solchen Systemen nur sehr wenige
object->object
Wechselwirkungen auf, sodass Sie leicht nachvollziehen können, was auf der Augenhöhe des Vogels vor sich geht.In solchen Fällen können Sie in Bezug auf Interaktionen zu etwas Ähnlichem kommen (dieses Diagramm zeigt Interaktionen an, nicht die Kopplung, da ein Kopplungsdiagramm möglicherweise abstrakte Schnittstellen für das zweite Bild unten enthält):
... im Gegensatz dazu:
... und der frühere Systemtyp ist in der Regel viel einfacher zu warten und in Bezug auf die Richtigkeit zu begründen, obwohl die Abhängigkeiten tatsächlich in Richtung Daten fließen. Sie erhalten in erster Linie eine viel geringere Kopplung, da viele Dinge in Rohdaten umgewandelt werden können, anstatt dass Objekte miteinander interagieren und ein sehr komplexes Wechselwirkungsdiagramm bilden.
quelle
Es gibt sogar viele seltsame Kreaturen in der OOP-Welt. Ich habe einmal eine Klasse erstellt, die abstrakt ist und nichts enthält, nur um ein gemeinsames Elternteil von zwei anderen Klassen zu sein ... es ist die Port-Klasse:
SceneMember ist also die Basisklasse und erledigt alle Aufgaben, die für die Darstellung in der Szene erforderlich sind: Hinzufügen, Löschen, Festlegen der ID usw. Die Nachricht ist die Verbindung zwischen den Ports. Sie hat ihr eigenes Leben. InputPort und OutputPort enthalten ihre eigenen I / O-spezifischen Funktionen.
Port ist leer. Es wird nur verwendet, um InputPort und OutputPort beispielsweise für eine Portliste zu gruppieren. Es ist leer, da SceneMember alle Elemente enthält, die als Member fungieren sollen, und InputPort und OutputPort die vom Port angegebenen Aufgaben enthalten. InputPort und OutputPort sind so unterschiedlich, dass sie keine gemeinsamen Funktionen (außer SceneMember) haben. Port ist also leer. Aber es ist legal.
Vielleicht ist es ein Gegenmuster , aber ich mag es.
(Es ist wie das Wort "Kumpel", das sowohl für "Ehefrau" als auch für "Ehemann" verwendet wird. Sie verwenden das Wort "Kumpel" niemals für eine konkrete Person, da Sie das Geschlecht der Person kennen und es keine Rolle spielt ob er verheiratet ist oder nicht, so verwenden Sie stattdessen "jemanden", der jedoch immer noch existiert und in seltenen, abstrakten Situationen verwendet wird, z. B. in einem Gesetzestext.)
quelle