Viele kleine Klassen gegen logische (aber) komplizierte Vererbung

24

Ich frage mich, was in Bezug auf gutes OOP-Design, sauberen Code, Flexibilität und Vermeidung von Codegerüchen in Zukunft besser ist. Bildsituation, in der Sie viele sehr ähnliche Objekte haben, die Sie als Klassen darstellen müssen. Diese Klassen haben keine spezifische Funktionalität, sind nur Datenklassen und unterscheiden sich nur durch Namen (und Kontext). Beispiel:

Class A
{
  String name;
  string description;
}

Class B
{
  String name;
  String count;
  String description;
}

Class C
{
  String name;
  String count;
  String description;
  String imageUrl;
}

Class D
{
  String name;
  String count;
}

Class E
{
  String name;
  String count;
  String imageUrl;
  String age;
}

Wäre es besser, sie in getrennten Klassen zu halten, um "bessere" Lesbarkeit zu erzielen, aber mit vielen Code-Wiederholungen, oder wäre es besser, die Vererbung zu verwenden, um trockener zu sein?

Durch die Verwendung der Vererbung erhalten Sie so etwas, aber Klassennamen verlieren ihre kontextbezogene Bedeutung (aufgrund des is-a, nicht des has-a):

Class A
{
  String name;
  String description;
}

Class B : A
{
  String count;
}

Class C : B
{
  String imageUrl;
}

Class D : C
{
  String age;
}

Ich weiß, dass Vererbung nicht für die Wiederverwendung von Code verwendet wird und nicht verwendet werden sollte, aber ich sehe keine andere Möglichkeit, um die Wiederholung von Code in einem solchen Fall zu reduzieren.

user969153
quelle

Antworten:

58

Die allgemeine Regel lautet "Delegierung der Vererbung vorziehen", nicht "Alle Vererbungen vermeiden". Wenn die Objekte eine logische Beziehung haben und ein B überall dort verwendet werden kann, wo ein A erwartet wird, empfiehlt es sich, die Vererbung zu verwenden. Verwenden Sie jedoch keine Vererbung, wenn die Objekte zufällig Felder mit demselben Namen und ohne Domänenbeziehung haben.

Sehr oft lohnt es sich, die Frage nach dem "Grund der Änderung" im Entwurfsprozess zu stellen: Wenn Klasse A ein zusätzliches Feld erhält, würden Sie erwarten, dass Klasse B es auch bekommt? Wenn dies zutrifft, teilen sich die Objekte eine Beziehung, und Vererbung ist eine gute Idee. Wenn nicht, lassen Sie die kleine Wiederholung zu, um unterschiedliche Konzepte in unterschiedlichem Code zu halten.

Thiton
quelle
Sicher, Änderungsgrund ist ein guter Punkt, aber wie sieht es in der Situation aus, in der diese Klassen konstant sind und immer bleiben werden?
user969153
20
@ user969153: Wenn die Klassen wirklich konstant sind, spielt es keine Rolle. Alles gute Software-Engineering ist für den zukünftigen Betreuer, der Änderungen vornehmen muss. Wenn es in Zukunft keine Änderungen mehr gibt, ist alles in Ordnung. Vertrauen Sie mir, Sie werden immer Änderungen haben, es sei denn, Sie programmieren für den Papierkorb.
Thiton
@ user969153: Davon abgesehen ist der "Grund der Veränderung" eine Heuristik. Es hilft Ihnen bei der Entscheidung, mehr nicht.
Thiton
2
Gute Antwort. Gründe für eine Änderung sind S im Akronym SOLID von Onkel Bob, das bei der Entwicklung guter Software hilfreich ist.
Klee
4
@ user969153, Nach meiner Erfahrung "wo sind und bleiben diese Klassen konstant?" ist kein gültiger Anwendungsfall :) Ich muss noch an einer Anwendung mit sinnvoller Größe arbeiten, die keine völlig unerwarteten Änderungen erfahren hat.
CdkMoose
18

Durch die Verwendung der Vererbung erhalten Sie so etwas, aber Klassennamen verlieren ihre kontextbezogene Bedeutung (aufgrund des is-a, nicht des has-a):

Genau. Betrachten Sie dies aus dem Zusammenhang:

Class D : C
{
    String age;
}

Welche Eigenschaften hat ein D? Erinnerst du dich? Was ist, wenn Sie die Hierarchie weiter verkomplizieren:

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

Und was ist dann, wenn Sie, Horror auf Horror, ein imageUrl-Feld zu F, aber nicht zu E hinzufügen möchten? Sie können es nicht von C und E ableiten.

Ich habe eine tatsächliche Situation gesehen, in der viele verschiedene Komponententypen einen Namen, eine ID und eine Beschreibung hatten. Aber das lag nicht in der Natur ihrer Bestandteile, es war nur ein Zufall. Jeder hatte eine ID, weil sie in einer Datenbank gespeichert waren. Der Name und die Beschreibung wurden angezeigt.

Produkte hatten aus ähnlichen Gründen auch eine ID, einen Namen und eine Beschreibung. Aber sie waren weder Komponenten noch Komponentenprodukte. Sie würden die beiden Dinge nie zusammen sehen.

Einige Entwickler hatten jedoch etwas über DRY gelesen und beschlossen, jeden Hinweis auf eine Vervielfältigung aus dem Code zu entfernen. Also nannte er alles ein Produkt und machte die Definition dieser Felder überflüssig. Sie wurden in Product definiert und alles, was diese Felder benötigt, kann aus Product abgeleitet werden.

Ich kann das nicht stark genug sagen: Das ist keine gute Idee.

Bei DRY geht es nicht darum, die Notwendigkeit gemeinsamer Eigenschaften zu beseitigen. Wenn ein Objekt kein Produkt ist, bezeichnen Sie es nicht als Produkt. Wenn für ein Produkt keine Beschreibung erforderlich ist, definieren Sie die Beschreibung nicht als Eigenschaft des Produkts.

Es kam schließlich vor, dass wir Komponenten ohne Beschreibung hatten. Also haben wir begonnen, Null-Beschreibungen in diesen Objekten zu haben. Nicht nullbar. Null. Immer. Für jedes Produkt vom Typ X ... oder später Y ... und N.

Dies ist ein schwerwiegender Verstoß gegen das Liskov-Substitutionsprinzip.

Bei DRY geht es darum, sich niemals in die Lage zu versetzen, einen Fehler an einer Stelle zu beheben und ihn an einer anderen Stelle zu vergessen. Dies wird nicht passieren, da Sie zwei Objekte mit derselben Eigenschaft haben. Noch nie. Also mach dir keine Sorgen.

Wenn der Kontext der Klassen deutlich macht, dass Sie sollten, was sehr selten sein wird, dann und nur dann sollten Sie eine gemeinsame Elternklasse in Betracht ziehen. Wenn es sich jedoch um Eigenschaften handelt, überlegen Sie, warum Sie stattdessen keine Schnittstelle verwenden.

pdr
quelle
4

Vererbung ist ein Weg, um polymorphes Verhalten zu erreichen. Wenn Ihre Klassen kein Verhalten aufweisen, ist die Vererbung nutzlos. In diesem Fall würde ich sie nicht einmal als Objekte / Klassen betrachten, sondern stattdessen als Strukturen.

Wenn Sie in der Lage sein möchten, verschiedene Strukturen in verschiedene Teile Ihres Verhaltens einzufügen, ziehen Sie Schnittstellen mit get / set-Methoden oder -Eigenschaften in Betracht, wie z Art von Hiearchies.

Euphorisch
quelle
3

Ist das nicht was für Schnittstellen sind? Nicht verwandte Klassen mit unterschiedlichen Funktionen, jedoch mit einer ähnlichen Eigenschaft.

IName = Interface(IInterface)
  function Getname : String;
  property Name : String read Getname
end;

Class Dog(InterfacedObject, IName)
private
  FName : String;
  Function GetName : String;
Public
  Name : Read Getname;
end;

Class Movie(InterfacedObject, IName)
private
  FCount;
  FName : String;
  Function GetName : String;
Public
  Name : String Read Getname;
  Count : read FCount; 
end;
Pieter B
quelle
1

Ihre Frage scheint im Kontext einer Sprache umrahmt zu sein, die Mehrfachvererbung nicht unterstützt, dies jedoch nicht speziell spezifiziert. Wenn Sie mit einer Sprache arbeiten, die Mehrfachvererbung unterstützt, ist dies mit nur einer einzigen Vererbungsebene trivial einfach zu lösen.

Hier ist ein Beispiel, wie dies durch Mehrfachvererbung gelöst werden kann.

trait Nameable { String name; }
trait Describable { String description; }
trait Countable { Integer count; }
trait HasImageUrl { String imageUrl; }
trait Living { String age; }

class A : Nameable, Describable;
class B : Nameable, Describable, Countable;
class C : Nameable, Describable, Countable, HasImageUrl;
class D : Nameable, Countable;
class E : Nameable, Countable, HasImageUrl, Living;
pgraham
quelle