Vererbung gegen zusätzliche Eigenschaft mit Nullwert

12

Ist es für Klassen mit optionalen Feldern besser, Vererbung oder eine nullfähige Eigenschaft zu verwenden? Betrachten Sie dieses Beispiel:

class Book {
    private String name;
}
class BookWithColor extends Book {
    private String color;
}

oder

class Book {
    private String name;
    private String color; 
    //when this is null then it is "Book" otherwise "BookWithColor"
}

oder

class Book {
    private String name;
    private Optional<String> color;
    //when isPresent() is false it is "Book" otherwise "BookWithColor"
}

Der Code, der von diesen drei Optionen abhängt, wäre:

if (book instanceof BookWithColor) { ((BookWithColor)book).getColor(); }

oder

if (book.getColor() != null) { book.getColor(); }

oder

if (book.getColor().isPresent()) { book.getColor(); }

Der erste Ansatz erscheint mir natürlicher, ist aber möglicherweise weniger lesbar, da Casting erforderlich ist. Gibt es einen anderen Weg, um dieses Verhalten zu erreichen?

Bojan VukasovicTest
quelle
1
Ich fürchte, die Lösung hängt von Ihrer Definition der Geschäftsregeln ab. Ich meine, wenn alle Ihre Bücher eine Farbe haben, aber die Farbe optional ist, dann ist die Vererbung übertrieben und eigentlich unnötig, weil Sie möchten, dass alle Ihre Bücher wissen, dass die Farbeigenschaft existiert. Wenn Sie jedoch sowohl Bücher haben können, die überhaupt nichts über Farbe wissen, als auch Bücher, die eine Farbe haben, ist das Erstellen spezialisierter Klassen nicht schlecht. Selbst dann würde ich wahrscheinlich Komposition über Vererbung anstreben.
Andy
Um es etwas genauer zu machen - es gibt 2 Arten von Büchern - eines mit Farbe und eines ohne. Daher haben möglicherweise nicht alle Bücher eine Farbe.
Bojan VukasovicTest
1
Wenn es wirklich einen Fall gibt, in dem ein Buch überhaupt keine Farbe hat, ist die Vererbung in Ihrem Fall die einfachste Möglichkeit, die Eigenschaften eines bunten Buches zu erweitern. In diesem Fall sieht es nicht so aus, als würden Sie etwas kaputtmachen, indem Sie die Basisbuchklasse erben und ihr eine Farbeigenschaft hinzufügen. Sie können das Prinzip der Liskov-Substitution lesen, um mehr über Fälle zu erfahren, in denen eine Klassenerweiterung zulässig ist und in denen dies nicht der Fall ist.
Andy
4
Können Sie ein Verhalten identifizieren, das Sie von Büchern mit Färbung wollen? Können Sie ein allgemeines Verhalten zwischen Büchern mit und ohne Färbung feststellen, bei dem sich die Bücher mit Färbung etwas anders verhalten sollten? In diesem Fall ist dies ein guter Fall für OOP mit verschiedenen Typen. Die Idee wäre, das Verhalten in die Klassen zu verschieben, anstatt das Vorhandensein und den Wert von Eigenschaften extern abzufragen.
Erik Eidt
1
Wenn die möglichen Buchfarben im Voraus bekannt sind, wie wäre es mit einem Aufzählungsfeld für BookColor mit einer Option für BookColor.NoColor?
Graham

Antworten:

7

Das hängt von den Umständen ab. Das spezifische Beispiel ist unrealistisch, da BookWithColorin einem realen Programm keine Unterklasse aufgerufen werden würde .

Im Allgemeinen sollte eine Eigenschaft, die nur für bestimmte Unterklassen sinnvoll ist, nur in diesen Unterklassen vorhanden sein.

Zum Beispiel, wenn Bookhat PhysicalBookundDigialBook als Abkömmlinge, dann PhysicalBookkönnte eine hat weightEigenschaft und DigitalBookeine sizeInKbEigenschaft. Werde DigitalBookaber nicht haben weightund umgekehrt. Bookwird keine Eigenschaft haben, da eine Klasse nur Eigenschaften haben sollte, die von allen Nachkommen geteilt werden.

Ein besseres Beispiel sind Klassen aus echter Software. Die JSliderKomponente hat das Feld majorTickSpacing. Da nur ein Schieberegler "Häkchen" hat, ist dieses Feld nur für sinnvollJSlider und seine Nachkommen . Es wäre sehr verwirrend, wenn andere Geschwisterkomponenten wie JButtonein majorTickSpacingFeld hätten.

JacquesB
quelle
oder Sie könnten das Gewicht bastract, um beide zu reflektieren, aber das ist wahrscheinlich zu viel.
Walfrat
3
@Walfrat: Es wäre eine wirklich schlechte Idee, dass dasselbe Feld völlig verschiedene Dinge in verschiedenen Unterklassen repräsentiert.
JacquesB
Was ist, wenn ein Buch sowohl physische als auch digitale Ausgaben hat? Sie müssten für jede Permutation, ob Sie ein optionales Feld haben oder nicht, eine andere Klasse erstellen. Ich denke, die beste Lösung wäre, je nach Buch einige Felder wegzulassen oder nicht. Sie haben eine völlig andere Situation angegeben, in der sich die beiden Klassen funktional unterschiedlich verhalten würden. Das impliziert diese Frage nicht. Sie müssen lediglich eine Datenstruktur modellieren.
4castle
@ 4castle: Dann brauchst du separate Klassen pro Edition, da jede Edition möglicherweise auch einen anderen Preis hat. Sie können dies nicht mit optionalen Feldern lösen. Aus dem Beispiel wissen wir jedoch nicht, ob die Operatoren ein anderes Verhalten für Bücher mit Farbe oder "farblose Bücher" wünschen. Daher ist es in diesem Fall unmöglich zu wissen, was die richtige Modellierung ist.
JacquesB
3
stimme mit @JacquesB überein. Sie können keine allgemeingültige Lösung für "Modellierungsbücher" angeben, da dies davon abhängt, welches Problem Sie lösen möchten und was Ihr Unternehmen ist. Ich denke, es ist ziemlich sicher zu sagen, dass das Definieren von Klassenhierarchien, in denen Sie gegen Liskov verstoßen, kategorisch falsch ist (yeah yeah, Ihr Fall ist wahrscheinlich sehr speziell und rechtfertigt dies (obwohl ich es bezweifle))
sara
5

Ein wichtiger Punkt, der anscheinend nicht erwähnt wurde: In den meisten Sprachen können Instanzen einer Klasse nicht ändern, von welcher Klasse sie stammen. Wenn Sie also ein Buch ohne Farbe hatten und eine Farbe hinzufügen möchten, müssen Sie ein neues Objekt erstellen, wenn Sie verschiedene Klassen verwenden. Und dann müssten Sie wahrscheinlich alle Verweise auf das alte Objekt durch Verweise auf das neue Objekt ersetzen.

Wenn "Bücher ohne Farbe" und "Bücher mit Farbe" Instanzen derselben Klasse sind, ist das Hinzufügen der Farbe oder das Entfernen der Farbe weniger problematisch. (Wenn Ihre Benutzeroberfläche eine Liste mit "Büchern mit Farbe" und "Büchern ohne Farbe" anzeigt, müsste sich diese Benutzeroberfläche offensichtlich ändern, aber ich würde davon ausgehen, dass Sie dies sowieso tun müssen, ähnlich einer "Liste mit roten Büchern" books "und" list of green books ").

gnasher729
quelle
Das ist wirklich ein wichtiger Punkt! Es definiert, ob eine Sammlung optionaler Eigenschaften anstelle von Klassenattributen in Betracht gezogen werden soll.
Chromanoid
1

Stellen Sie sich eine JavaBean (wie Ihre Book) als Datensatz in einer Datenbank vor. Optionale Spalten sind, nullwenn sie keinen Wert haben, aber es ist völlig legal. Daher Ihre zweite Option:

class Book {
    private String name;
    private String color; // null when not applicable
}

Ist das vernünftigste. 1

Seien Sie jedoch vorsichtig, wie Sie die OptionalKlasse verwenden. Zum Beispiel ist dies nicht Serializableder Fall, was normalerweise ein Merkmal einer JavaBean ist. Hier einige Tipps von Stephen Colebourne :

  1. Deklarieren Sie keine Instanzvariable vom Typ Optional.
  2. Verwenden null , um optionale Daten im privaten Bereich einer Klasse anzugeben.
  3. Verwenden Sie Optionaldiese Option für Getter, die auf das optionale Feld zugreifen.
  4. Verwende nicht Optional in Setzern oder Konstrukteuren verwenden.
  5. Verwendung Optionalals Rückgabetyp für alle anderen Geschäftslogikmethoden mit optionalem Ergebnis.

Daher innerhalb Ihrer Klasse sollten Sie verwenden , nullum darzustellen , dass das Feld nicht vorhanden ist, aber wenn die color Blätter des Book(als Rückgabetyp) sollte es mit einem eingewickelt werden ,Optional .

return Optional.ofNullable(color); // inside the class

book.getColor().orElse("No color"); // outside the class

Dies sorgt für klares Design und besser lesbaren Code.


1 Wenn Sie beabsichtigen , für eine BookWithColoreine ganze „Klasse“ der Bücher zu verkapseln , die haben Fähigkeiten spezialisiert auf andere Bücher, dann wäre es sinnvoll zu nutzen Erbschaft machen.

4castle
quelle
1
Wer hat etwas über eine Datenbank gesagt? Wenn alle OO-Muster in ein relationales Datenbankparadigma passen müssten, müssten wir viele OO-Muster ändern.
Alexanderbird
Ich würde argumentieren, das Hinzufügen von nicht verwendeten Eigenschaften "nur für den Fall" ist die am wenigsten eindeutige Option. Wie andere gesagt haben, hängt dies vom Anwendungsfall ab, aber die wünschenswerteste Situation wäre, keine Eigenschaften mit sich zu führen, bei denen eine externe Anwendung wissen muss, ob eine vorhandene Eigenschaft tatsächlich verwendet wird oder nicht.
Volker Kueffel
@Volker Lasst uns zustimmen, nicht zuzustimmen, da ich einige Leute anführen kann, die beim Hinzufügen der OptionalKlasse zu Java genau zu diesem Zweck eine Rolle gespielt haben . Es ist vielleicht nicht OO, aber es ist sicherlich eine gültige Meinung.
4castle
@Alexanderbird Ich habe mich speziell mit einer JavaBean befasst, die serialisierbar sein sollte. Wenn ich durch das Aufrufen von JavaBeans vom Thema abkam, war das mein Fehler.
4castle
@Alexanderbird Ich erwarte nicht , alle Muster eine relationale Datenbank zu finden , aber ich erwarte eine Java Bean
4castle
0

Sie sollten entweder eine Schnittstelle (keine Vererbung) oder eine Art Eigenschaftsmechanik (möglicherweise sogar etwas, das wie das Ressourcenbeschreibungsframework funktioniert) verwenden.

Also entweder

interface Colored {
  Color getColor();
}
class ColoredBook extends Book implements Colored {
  ...
}

oder

class PropertiesHolder {
  <T> extends Property<?>> Optional<T> getProperty( Class<T> propertyClass ) { ... }
  <V, T extends Property<V>> Optional<V> getPropertyValue( Class<T> propertyClass ) { ... }
}
interface Property<T> {
  String getName();
  T getValue();
}
class ColorProperty implements Property<Color> {
  public Color getValue() { ... }
  public String getName() { return "color"; }
}
class Book extends PropertiesHolder {
}

Klarstellung (bearbeiten):

Fügen Sie einfach optionale Felder in die Entitätsklasse ein

Vor allem mit dem Optional-Wrapper (edit: siehe 4castle -Antwort) halte ich dies (Hinzufügen von Feldern in der ursprünglichen Entität) für eine praktikable Möglichkeit, neue Eigenschaften in kleinem Maßstab hinzuzufügen. Das größte Problem bei diesem Ansatz ist, dass er möglicherweise gegen eine geringe Kopplung wirkt.

Stellen Sie sich vor, Ihre Buchklasse ist in einem speziellen Projekt für Ihr Domain-Modell definiert. Nun fügen Sie ein weiteres Projekt hinzu, das das Domänenmodell für eine spezielle Aufgabe verwendet. Diese Aufgabe erfordert eine zusätzliche Eigenschaft in der Buchklasse. Entweder haben Sie die Vererbung (siehe unten) oder Sie müssen das allgemeine Domänenmodell ändern, um die neue Aufgabe zu ermöglichen. In letzterem Fall kann es vorkommen, dass Sie eine Reihe von Projekten haben, die alle von ihren eigenen Eigenschaften abhängen, die der Buchklasse hinzugefügt wurden, während die Buchklasse selbst in gewisser Weise von diesen Projekten abhängt, da Sie die Buchklasse ohne die nicht verstehen können zusätzliche Projekte.

Warum ist die Vererbung problematisch, wenn es darum geht, zusätzliche Eigenschaften bereitzustellen?

Wenn ich Ihre Beispielklasse "Buch" sehe, denke ich an ein Domänenobjekt, das häufig viele optionale Felder und Untertypen enthält. Stellen Sie sich vor, Sie möchten eine Eigenschaft für Bücher hinzufügen, die eine CD enthalten. Es gibt jetzt vier Arten von Büchern: Bücher, Bücher mit Farbe, Bücher mit CD, Bücher mit Farbe und CD. Sie können diese Situation nicht mit Vererbung in Java beschreiben.

Mit Schnittstellen umgehen Sie dieses Problem. Sie können die Eigenschaften einer bestimmten Buchklasse einfach über Schnittstellen zusammenstellen. Durch Delegation und Komposition wird es einfach, genau die Klasse zu erhalten, die Sie möchten. Bei der Vererbung erhalten Sie normalerweise einige optionale Eigenschaften in der Klasse, die nur vorhanden sind, weil eine Geschwisterklasse sie benötigt.

Lesen Sie weiter, warum Vererbung oft eine problematische Idee ist:

Warum wird Vererbung von OOP-Befürwortern im Allgemeinen als eine schlechte Sache angesehen?

JavaWorld: Warum sich ausdehnt, ist böse

Das Problem beim Implementieren einer Reihe von Schnittstellen zum Erstellen einer Reihe von Eigenschaften

Wenn Sie Schnittstellen zur Erweiterung verwenden, ist alles in Ordnung, solange Sie nur einen kleinen Satz davon haben. Insbesondere wenn Ihr Objektmodell von anderen Entwicklern verwendet und erweitert wird, z. B. in Ihrem Unternehmen, wird die Anzahl der Schnittstellen zunehmen. Und schließlich erstellen Sie eine neue offizielle "Eigenschaftsschnittstelle", die eine Methode hinzufügt, die Ihre Kollegen bereits in ihrem Projekt für Kunde X für einen völlig unabhängigen Anwendungsfall verwendet haben - Ugh.

edit: Ein weiterer wichtiger Aspekt wird von gnasher729 erwähnt . Oft möchten Sie einem vorhandenen Objekt optionale Eigenschaften dynamisch hinzufügen. Mit Vererbung oder Schnittstellen müssten Sie das gesamte Objekt mit einer anderen Klasse neu erstellen, die alles andere als optional ist.

Wenn Sie Erweiterungen Ihres Objektmodells in einem solchen Ausmaß erwarten, sind Sie besser dran, wenn Sie explizit die Möglichkeit einer dynamischen Erweiterung modellieren . Ich schlage so etwas wie oben vor, wo jede "Erweiterung" (in diesem Fall Eigenschaft) eine eigene Klasse hat. Die Klasse fungiert als Namespace und Bezeichner für die Erweiterung. Wenn Sie die Paketnamenskonventionen entsprechend festlegen, werden auf diese Weise unendlich viele Erweiterungen ermöglicht, ohne dass der Namespace für Methoden in der ursprünglichen Entitätsklasse verschmutzt wird.

In der Spieleentwicklung stoßen Sie häufig auf Situationen, in denen Sie Verhalten und Daten in vielen Variationen zusammenstellen möchten. Aus diesem Grund wurde das Architekturmuster Entity-Component-System in Spielentwicklungskreisen sehr beliebt. Dies ist auch ein interessanter Ansatz, den Sie sich ansehen sollten, wenn Sie viele Erweiterungen für Ihr Objektmodell erwarten.

Chromanoid
quelle
Und es hat sich nicht mit der Frage befasst, wie man sich zwischen diesen Optionen entscheidet. Es ist nur eine weitere Option. Warum ist dies immer besser als alle Optionen, die das OP bot?
Alexanderbird
Sie haben recht, ich werde die Antwort verbessern.
Chromanoid
@ 4castle In Java sind Schnittstellen keine Vererbung! Das Implementieren von Schnittstellen ist eine Möglichkeit, einen Vertrag zu erfüllen, während das Erben einer Klasse deren Verhalten im Grunde erweitert. Sie können mehrere Schnittstellen implementieren, während Sie nicht mehrere Klassen in einer Klasse erweitern können. In meinem Beispiel erweitern Sie mit Schnittstellen, während Sie die Vererbung verwenden, um das Verhalten der ursprünglichen Entität zu erben.
Chromanoid
Macht Sinn. In Ihrem Beispiel PropertiesHolderwäre das wahrscheinlich eine abstrakte Klasse. Aber wofür würden Sie eine Implementierung vorschlagen getPropertyoder getPropertyValue? Die Daten müssen noch irgendwo in einem Instanzfeld gespeichert werden. Oder würden Sie a verwenden Collection<Property>, um die Eigenschaften zu speichern, anstatt Felder zu verwenden?
4castle
1
Ich habe aus Gründen der Übersichtlichkeit eine BookVerlängerung vorgenommen PropertiesHolder. Das PropertiesHoldersollte normalerweise ein Mitglied in allen Klassen sein, die diese Funktionalität benötigen. Die Implementierung würde ein Map<Class<? extends Property<?>>,Property<?>>oder so ähnlich halten. Dies ist der hässliche Teil der Implementierung, bietet aber ein wirklich nettes Framework, das sogar mit JAXB und JPA funktioniert.
Chromanoid
-1

Wenn BookKlasse ohne Farbeigenschaft wie unten abgeleitet werden kann

class E_Book extends Book{
   private String type;
}

dann ist Vererbung sinnvoll.

Adem Caglin
quelle
Ich bin mir nicht sicher, ob ich dein Beispiel verstehe? Wenn colores sich um eine private Klasse handelt, sollte sich keine abgeleitete Klasse darum kümmern müssen color. Fügen Sie einfach einige Konstruktoren hinzu, Bookdie ignoriert colorwerden null.
4castle