it-swarm.dev

Gibt es einen Grund, "einfache alte Daten" -Klassen zu verwenden?

Im Legacy-Code sehe ich gelegentlich Klassen, die nichts anderes als Wrapper für Daten sind. 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 sind nd die Methoden, mit diesen Daten zu arbeiten. Dies scheint Objekte dieses Typs auszuschließen. Für mich sind sie nichts mehr als structs und irgendwie den Zweck von OO zu besiegen. Ich denke nicht, dass es notwendigerweise böse ist, obwohl es ein Code-Geruch sein kann.

Gibt es einen Fall, in dem solche Objekte notwendig wären? Wenn dies häufig verwendet wird, macht es das Design verdächtig?

44
Michael K

Auf jeden Fall nicht böse und kein Code-Geruch in meinem Kopf. Datencontainer sind gültige OO Bürger. Manchmal möchten Sie verwandte Informationen zusammenfassen. Es ist viel besser, eine Methode wie diese zu haben

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

als

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

Durch die Verwendung einer Klasse können Sie Bottle auch einen zusätzlichen Parameter hinzufügen, ohne jeden Aufrufer von DoStuffWithBottle zu ändern. Außerdem können Sie Bottle in Unterklassen unterteilen und bei Bedarf die Lesbarkeit und Organisation Ihres Codes weiter verbessern.

Es gibt auch einfache Datenobjekte, die beispielsweise als Ergebnis einer Datenbankabfrage zurückgegeben werden können. Ich glaube, der Begriff für sie ist in diesem Fall "Datenübertragungsobjekt".

In einigen Sprachen gibt es auch andere Überlegungen. In C # verhalten sich Klassen und Strukturen beispielsweise unterschiedlich, da Strukturen ein Werttyp und Klassen Referenztypen sind.

68
Adam Lear

Datenklassen sind in einigen Fällen gültig. DTOs sind ein gutes Beispiel, das von Anna Lear erwähnt wird. Im Allgemeinen sollten Sie sie jedoch als Keim einer Klasse betrachten, deren Methoden noch nicht entstanden sind. Und wenn Sie im alten 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 OO) programmiert haben und ein Zeichen für prozedurale Programmierung sind. Sie verlassen sich ständig auf Getter und Setter (oder noch schlimmer auf) Direkter Zugriff von nicht privaten Mitgliedern) kann Sie in Schwierigkeiten bringen, bevor Sie es wissen. Betrachten Sie ein Beispiel für eine externe Methode, die Informationen von Bottle benötigt.

Hier ist Bottle eine Datenklasse):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Hier haben wir Bottle ein Verhalten gegeben):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Die erste Methode verstößt gegen das Tell-Don't-Ask-Prinzip, und indem wir Bottle dumm halten, lassen wir implizites Wissen über Flaschen, wie z. B. was ein Fragment (das Cap) macht, hineinschlüpfen Logik, die außerhalb der Klasse Bottle liegt. Sie müssen auf Trab 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 es für seine Arbeit benötigt, und lässt Bottle zu entscheiden, ob es zerbrechlich oder größer als eine bestimmte Größe ist. Das Ergebnis ist eine viel lockerere Kopplung zwischen der Methode und der Implementierung von Bottle. Ein angenehmer Nebeneffekt ist, dass die Methode sauberer und ausdrucksvoller ist.

Sie werden selten so viele Felder eines Objekts verwenden, ohne eine Logik zu schreiben, die sich in der Klasse mit diesen Feldern befinden sollte. Finden Sie heraus, was diese Logik ist, und verschieben Sie sie dann dorthin, wo sie hingehört.

25
Mike E

Wenn Sie so etwas brauchen, ist das in Ordnung, aber bitte, bitte, bitte machen Sie es so

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

statt so etwas

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

Bitte.

7
compman

Wie @Anna sagte, definitiv nicht böse. Sicher können Sie Operationen (Methoden) in Klassen einfügen, aber nur, wenn Sie wollen to. Sie haben nicht haben zu.

Gestatten Sie mir einen kleinen Kritikpunkt über die Idee, dass Sie Operationen in Klassen einfügen müssen, zusammen mit der Idee, dass Klassen Abstraktionen sind. In der Praxis ermutigt dies Programmierer dazu

  1. Machen Sie mehr Klassen als nötig (redundante Datenstruktur). Wenn eine Datenstruktur mehr Komponenten enthält als minimal erforderlich, 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.

  2. Lösen Sie Problem 1, indem Sie Benachrichtigung Methoden eingeben. Wenn Teil A geändert wird, versucht es, die erforderlichen Änderungen an Teil B und C weiterzugeben. Dies ist der Hauptgrund, warum Get-and empfohlen wird -set Zugriffsmethoden. Da dies empfohlen wird, scheint es Problem 1 zu entschuldigen, was mehr von Problem 1 und mehr von Lösung 2 verursacht. Dies führt nicht nur zu Fehlern aufgrund der unvollständigen Implementierung der Benachrichtigungen, sondern auch zu einem leistungseinschränkenden Problem von 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 Millionen-Monster-Apps arbeiten mussten, die mit diesen Problemen durchsetzt sind.

Folgendes versuche ich zu tun:

  1. Halten Sie die Daten so normal wie möglich, damit Änderungen an den Daten an möglichst wenigen Codepunkten vorgenommen werden, um die Wahrscheinlichkeit des Eintritts in einen inkonsistenten Zustand zu minimieren.

  2. Wenn Daten nicht normalisiert werden müssen und Redundanz unvermeidbar ist, verwenden Sie keine Benachrichtigungen, um die Konsistenz zu gewährleisten. Tolerieren Sie vielmehr 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 effizient ist.

6
Mike Dunlavey

Beim Spieldesign kann es sich aufgrund des Overheads von Tausenden von Funktionsaufrufen und Ereignis-Listenern manchmal lohnen, Klassen zu haben, die nur Daten speichern, und andere Klassen, die alle Nur-Daten-Klassen durchlaufen, um die Logik auszuführen.

3
AttackingHobo

Stimmen Sie mit der Anna Lear überein,

Auf jeden Fall nicht böse und kein Code-Geruch in meinem Kopf. Datencontainer sind gültige OO Bürger. Manchmal möchten Sie verwandte Informationen zusammenfassen. Es ist viel besser, eine Methode wie ...

Manchmal vergessen die Leute, die 1999 Java Coding Conventions) zu lesen, die sehr deutlich machen, dass diese Art der Programmierung vollkommen in Ordnung ist. Wenn Sie dies vermeiden, riecht Ihr Code tatsächlich! (Zu viele Getter/Setter)

From 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 (wenn Java unterstützte Struktur)), sollten Sie die Instanzvariablen der Klasse öffentlich machen. - http://www.Oracle.com/technetwork/Java/javase/documentation/codeconventions-137265.html#177

Bei korrekter Verwendung sind PODs (einfache alte Datenstrukturen) besser als POJOs, genau wie POJOs oft besser als EJBs.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures

3
Richard Faber

Diese Art von Klassen ist aus folgenden Gründen sehr nützlich, wenn Sie mit mittelgroßen/großen Anwendungen arbeiten:

  • es ist recht einfach, einige Testfälle zu erstellen und sicherzustellen, dass die Daten konsistent sind.
  • es enthält alle Arten von Verhaltensweisen, die diese Informationen beinhalten, sodass die Zeit für die Verfolgung von Datenfehlern verkürzt wird
  • Wenn Sie sie verwenden, sollten die Methodenargumente leicht bleiben.
  • Bei Verwendung von ORMs bietet diese Klasse Flexibilität und Konsistenz. Durch Hinzufügen eines komplexen Attributs, das auf einfachen Informationen basiert, die bereits in der Klasse enthalten sind, wird eine einfache Methode neu geschrieben. Dies ist wesentlich agiler und produktiver, als wenn Sie Ihre Datenbank überprüfen und sicherstellen müssen, dass alle Datenbanken mit neuen Änderungen gepatcht werden.

Zusammenfassend sind sie meiner Erfahrung nach normalerweise eher nützlich als nervig.

3
guiman

Strukturen haben ihren Platz auch in Java. Sie sollten sie nur verwenden, wenn die folgenden zwei Dinge zutreffen:

  • Sie müssen nur Daten aggregieren, die kein Verhalten aufweisen, z. als Parameter übergeben
  • Es spielt keine Rolle, welche Art von Werten aggregierte Daten haben

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 sie keine Eigenschaften wie eine nützliche Sprache haben. Da Ihr strukturähnliches Objekt sowieso keine Methoden haben sollte, sind öffentliche Felder am sinnvollsten.

Wenn jedoch eine davon nicht zutrifft, handelt es sich um eine echte Klasse. Das heißt, alle Felder sollten privat sein. (Wenn Sie unbedingt ein Feld in einem zugänglicheren Bereich benötigen, verwenden Sie einen Getter/Setter.)

Überprüfen Sie, wann die Felder verwendet werden, um zu überprüfen, ob Ihre angebliche Struktur Verhalten aufweist. 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 diese Felder endgültig machen. Sie könnten erwägen, Ihre Klasse unveränderlich zu machen . Wenn Sie Ihre Daten validieren müssen, geben Sie die Validierung in den Setzern und Konstruktoren an. (Ein nützlicher Trick besteht darin, einen privaten Setter zu definieren und Ihr Feld innerhalb Ihrer Klasse nur mit diesem Setter zu ändern.)

Ihr Flaschenbeispiel würde höchstwahrscheinlich beide Tests nicht bestehen. Sie könnten (erfundenen) Code haben, der so aussieht:

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

Stattdessen sollte es sein

double volume = bottle.calculateVolumeAsCylinder();

Wenn Sie die Höhe und den Durchmesser ändern würden, wäre es dieselbe Flasche? Wahrscheinlich nicht. Diese sollten endgültig sein. Ist ein negativer Wert für den Durchmesser in Ordnung? Muss Ihre Flasche größer als breit sein? Kann die Kappe 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:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}
3
Eva

IMHO gibt es oft nicht genug Klassen wie diese in stark objektorientierten Systemen. Ich muss das sorgfältig qualifizieren.

Wenn die Datenfelder einen großen Umfang und eine große Sichtbarkeit haben, kann dies natürlich äußerst unerwünscht sein, wenn Hunderte oder Tausende von Stellen in Ihrer Codebasis solche Daten manipulieren. Das erfordert Ärger und Schwierigkeiten bei der Aufrechterhaltung von Invarianten. Gleichzeitig bedeutet dies jedoch nicht, dass jede einzelne Klasse in der gesamten Codebasis vom Verstecken von Informationen profitiert.

Es gibt jedoch viele Fälle, in denen solche Datenfelder einen sehr engen Umfang haben. Ein sehr einfaches Beispiel ist eine private Node Klasse einer Datenstruktur. Es kann den Code oft erheblich vereinfachen, indem die Anzahl der laufenden Objektinteraktionen verringert wird, wenn Node einfach aus Rohdaten bestehen könnte. Dies dient als Entkopplungsmechanismus, da die alternative Version möglicherweise eine bidirektionale Kopplung von beispielsweise Tree->Node Und Node->Tree Im Gegensatz zu einfach Tree->Node Data Erfordert.

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 von Ihnen gezeigte. Ihr Umfang/ihre Sichtbarkeit sind jedoch tendenziell eingeschränkt, da normalerweise nur ein oder zwei Systeme auf diesen bestimmten Komponententyp zugreifen können. Infolgedessen ist es für Sie immer noch recht einfach, Invarianten in diesen Systemen beizubehalten, und darüber hinaus weisen solche Systeme nur sehr wenige object->object - Interaktionen auf, sodass Sie sehr einfach nachvollziehen können, was auf Vogelperspektive vor sich geht.

In solchen Fällen können Sie in Bezug auf Interaktionen zu etwas Ähnlichem führen (dieses Diagramm zeigt Interaktionen an, nicht Kopplung, da ein Kopplungsdiagramm abstrakte Schnittstellen für das zweite Bild unten enthalten kann):

(enter image description here

... im Gegensatz dazu:

(enter image description here

... und der frühere Systemtyp ist in der Regel viel einfacher zu warten und in Bezug auf die Korrektheit zu begründen, obwohl die Abhängigkeiten tatsächlich in Richtung Daten fließen. Sie erhalten viel weniger Kopplung, vor allem, weil viele Dinge in Rohdaten umgewandelt werden können, anstatt dass Objekte miteinander interagieren und einen sehr komplexen Interaktionsgraphen bilden.

0
user204677