it-swarm.com.de

Warum eine Klasse erben und keine Eigenschaften hinzufügen?

Ich habe in unserer (ziemlich großen) Codebasis einen Vererbungsbaum gefunden, der ungefähr so ​​aussieht:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderDateInfo : NamedEntity { }

Soweit ich das beurteilen konnte, wird dies hauptsächlich verwendet, um Dinge am Frontend zu binden.

Für mich ist dies sinnvoll, da es der Klasse einen konkreten Namen gibt, anstatt sich auf das generische NamedEntity zu verlassen. Andererseits gibt es eine Reihe solcher Klassen, die einfach keine zusätzlichen Eigenschaften haben.

Gibt es Nachteile bei diesem Ansatz?

40
robotron

Für mich ist dies sinnvoll, da es der Klasse einen konkreten Namen gibt, anstatt sich auf die generische NamedEntity zu verlassen. Andererseits gibt es eine Reihe solcher Klassen, die einfach keine zusätzlichen Eigenschaften haben.

Gibt es Nachteile bei diesem Ansatz?

Der Ansatz ist nicht schlecht, aber es gibt bessere Lösungen. Kurz gesagt, eine Schnittstelle wäre hierfür eine viel bessere Lösung. Der Hauptgrund, warum Schnittstellen und Vererbung hier unterschiedlich sind, ist, dass Sie nur von einer Klasse erben können, aber Sie können viele Schnittstellen implementieren .

Angenommen, Sie haben Entitäten benannt und Entitäten geprüft. Sie haben mehrere Entitäten:

One ist weder eine geprüfte noch eine benannte Entität. Das ist einfach:

public class One 
{ }

Two ist eine benannte Entität, jedoch keine geprüfte Entität. Das ist im Wesentlichen das, was Sie jetzt haben:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Two : NamedEntity 
{ }

Three ist sowohl ein benannter als auch ein geprüfter Eintrag. Hier stoßen Sie auf ein Problem. Sie können eine AuditedEntity Basisklasse erstellen, aber Sie können Three nicht erben beide AuditedEntity und NamedEntity:

public class AuditedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : NamedEntity, AuditedEntity  // <-- Compiler error!
{ }

Sie können sich jedoch eine Problemumgehung vorstellen, indem Sie AuditedEntity von NamedEntity erben lassen. Dies ist ein cleverer Hack, um sicherzustellen, dass jede Klasse nur (direkt) von einer anderen Klasse erben muss.

public class AuditedEntity : NamedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : AuditedEntity
{ }

Das funktioniert immer noch. Was Sie hier getan haben, ist jedoch, dass jede geprüfte Entität von Natur aus auch eine benannte Entität ist. Das bringt mich zu meinem letzten Beispiel. Four ist eine geprüfte Entität, jedoch keine benannte Entität. Sie können jedoch nicht zulassen, dass Four von AuditedEntity erbt, da Sie es dann aufgrund der Vererbung zwischen AuditedEntityNamedEntityNamedEntity auch zu einem and machen würden `.

Bei Verwendung der Vererbung gibt es keine Möglichkeit, sowohl Three als auch Four zum Laufen zu bringen, es sei denn, Sie beginnen mit dem Duplizieren von Klassen (was eine ganze Reihe neuer Probleme aufwirft).

Mit Schnittstellen kann dies leicht erreicht werden:

public interface INamedEntity
{
    int Id { get; set; }
    string Name { get; set; }
}

public interface IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class One 
{ }

public class Two : INamedEntity 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Three : INamedEntity, IAuditedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class Four : IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

Der einzige kleine Nachteil hierbei ist, dass Sie die Schnittstelle noch implementieren müssen. Sie erhalten jedoch alle Vorteile eines gemeinsamen wiederverwendbaren Typs, ohne die Nachteile, die auftreten, wenn Sie Variationen mehrerer gemeinsamer Typen für eine bestimmte Entität benötigen.

Ihr Polymorphismus bleibt jedoch erhalten:

var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();

public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}

HandleNamedEntity(one);    //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);  
HandleNamedEntity(four);   //Error - not a named entity

HandleAuditedEntity(one);    //Error - not an audited entity
HandleAuditedEntity(two);    //Error - not an audited entity
HandleAuditedEntity(three);  
HandleAuditedEntity(four);

Andererseits gibt es eine Reihe solcher Klassen, die einfach keine zusätzlichen Eigenschaften haben.

Dies ist eine Variation des Marker-Schnittstellenmusters , bei der Sie eine leere Schnittstelle implementieren, um anhand des Schnittstellentyps zu überprüfen, ob eine bestimmte Klasse mit dieser Schnittstelle "markiert" ist.
Sie verwenden geerbte Klassen anstelle von implementierten Schnittstellen, aber das Ziel ist dasselbe, daher werde ich es als "markierte Klasse" bezeichnen.

Auf den ersten Blick ist an Markierungsschnittstellen/-klassen nichts auszusetzen. Sie sind syntaktisch und technisch gültig, und ihre Verwendung hat keine inhärenten Nachteile , vorausgesetzt, der Marker ist universell wahr (zur Kompilierungszeit) und nicht bedingt .

Genau so sollten Sie zwischen verschiedenen Ausnahmen unterscheiden, auch wenn diese Ausnahmen im Vergleich zur Basismethode keine zusätzlichen Eigenschaften/Methoden aufweisen.

Es ist also an sich nichts Falsches daran, aber ich würde raten, dies vorsichtig zu verwenden, um sicherzustellen, dass Sie nicht nur versuchen, einen vorhandenen architektonischen Fehler mit schlecht entworfenem Polymorphismus zu vertuschen.

15
Flater

Dies ist etwas, das ich benutze, um zu verhindern, dass Polymorphismus verwendet wird.

Angenommen, Sie haben 15 verschiedene Klassen, die irgendwo in ihrer Vererbungskette NamedEntity als Basisklasse haben, und Sie schreiben eine neue Methode, die nur für OrderDateInfo gilt.

Sie "könnten" einfach die Signatur schreiben als

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

Und hoffe und bete, dass niemand das Typensystem missbraucht, um ein FooBazNamedEntity hinein zu schieben.

Oder Sie "könnten" einfach void MyMethod(OrderDateInfo foo) schreiben. Dies wird nun vom Compiler erzwungen. Einfach, elegant und nicht darauf angewiesen, dass Menschen keine Fehler machen.

Auch wie @candied_orange hervorhob , Ausnahmen sind ein guter Fall dafür. Sehr selten (und ich meine sehr, sehr, sehr selten) möchten Sie jemals alles mit catch (Exception e) abfangen. Wahrscheinlicher ist es, dass Sie ein SqlException oder ein FileNotFoundException oder eine benutzerdefinierte Ausnahme für Ihre Anwendung abfangen möchten. Diese Klassen bieten häufig nicht mehr Daten oder Funktionen als die Basisklasse Exception, aber Sie können unterscheiden, was sie darstellen, ohne sie untersuchen und ein Typfeld überprüfen oder nach einem bestimmten Text suchen zu müssen.

Insgesamt ist es ein Trick, das Typsystem so zu gestalten, dass Sie eine engere Gruppe von Typen verwenden können, als Sie es könnten, wenn Sie eine Basisklasse verwenden würden. Ich meine, Sie könnten alle Ihre Variablen und Argumente so definieren, dass sie den Typ Object haben, aber das würde Ihre Arbeit nur erschweren, nicht wahr?

63
Becuzz

Dies ist meine Lieblingsverwendung. Ich benutze es hauptsächlich für Ausnahmen, die bessere, spezifischere Namen verwenden könnten

Das übliche Problem der Vererbung, das zu langen Ketten führt und das Jojo-Problem verursacht, gilt hier nicht, da es nichts gibt, was Sie zur Kette motivieren könnte.

28
candied_orange

IMHO ist das Klassendesign falsch. Es sollte sein.

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfo HAS A Name ist eine natürlichere Beziehung und erstellt zwei leicht verständliche Klassen, die die ursprüngliche Frage nicht provoziert hätten.

Jede Methode, die NamedEntity als Parameter akzeptiert hat, sollte nur an den Eigenschaften Id und Name interessiert sein. Daher sollten solche Methoden geändert werden, um EntityName zu akzeptieren stattdessen.

Der einzige technische Grund, den ich für das ursprüngliche Design akzeptieren würde, ist die Eigentumsbindung, die das OP erwähnte. Ein Mist-Framework wäre nicht in der Lage, mit der zusätzlichen Eigenschaft fertig zu werden und an object.Name.Id Zu binden. Aber wenn Ihr verbindlicher Rahmen damit nicht fertig wird, müssen Sie der Liste weitere technische Schulden hinzufügen.

Ich würde der Antwort von @ Flater folgen, aber mit vielen Schnittstellen, die Eigenschaften enthalten, schreiben Sie am Ende viel Code auf dem Boilerplate, selbst mit den schönen automatischen Eigenschaften von C #. Stellen Sie sich vor, Sie machen es in Java!

1
batwad

Klassen machen Verhalten verfügbar, und Datenstrukturen machen Daten verfügbar.

Ich sehe die Klassenschlüsselwörter, aber ich sehe kein Verhalten. Dies bedeutet, dass ich diese Klasse als Datenstruktur anzeigen würde. In diesem Sinne werde ich Ihre Frage umformulieren als

Warum eine gemeinsame Datenstruktur auf oberster Ebene?

Sie können also den Datentyp der obersten Ebene verwenden. Dies ermöglicht die Nutzung des Typsystems, um eine Richtlinie über große Mengen unterschiedlicher Datenstrukturen hinweg sicherzustellen, indem sichergestellt wird, dass alle Eigenschaften vorhanden sind.

Warum eine Datenstruktur, die die oberste Ebene enthält, aber nichts hinzufügt?

Sie können also den untergeordneten Datentyp verwenden. Dies ermöglicht das Einfügen von Hinweisen in das Typisierungssystem, um den Zweck der Variablen auszudrücken

Top level data structure - Named
   property: name;

Bottom level data structure - Person

In der obigen Hierarchie finden wir es zweckmäßig, anzugeben, dass eine Person benannt ist, damit Personen ihren Namen erhalten und ändern können. Obwohl es möglicherweise sinnvoll ist, der Datenstruktur Person zusätzliche Eigenschaften hinzuzufügen, erfordert das von uns gelöste Problem keine perfekte Modellierung der Person. Daher haben wir es versäumt, allgemeine Eigenschaften wie age usw. hinzuzufügen.

Es ist also eine Nutzung des Typisierungssystems, um die Absicht dieses benannten Elements so auszudrücken, dass es nicht mit Aktualisierungen (wie Dokumentation) bricht und zu einem späteren Zeitpunkt problemlos erweitert werden kann (wenn Sie feststellen, dass Sie das Feld age wirklich benötigen später).

1
Edwin Buck