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 kann, wird dies hauptsächlich zum Binden von Frontend-Inhalten verwendet.
Für mich ist dies sinnvoll, da es der Klasse einen konkreten Namen gibt, anstatt sich auf das Generikum zu verlassen NamedEntity
. Andererseits gibt es eine Reihe solcher Klassen, die einfach keine zusätzlichen Eigenschaften haben.
Gibt es Nachteile bei diesem Ansatz?
object-oriented-design
inheritance
Robotron
quelle
quelle
OrderDateInfo
s relevant sind, von denen unterscheiden, die für andereNamedEntity
s relevant sindIdentifier
Klasse hätten, die nichts damit zu tun hat,NamedEntity
sondern sowohl einId
als auch eineName
Eigenschaft benötigtNamedEntity
. Der Kontext und die ordnungsgemäße Verwendung einer Klasse sind mehr als nur die Eigenschaften und Methoden, die sie enthält.Antworten:
Der Ansatz ist nicht schlecht, aber es gibt bessere Lösungen. Kurz gesagt, eine Schnittstelle wäre dafür eine viel bessere Lösung. Der Hauptgrund , warum Schnittstellen und Vererbung hier anders ist , weil man nur von einer Klasse erben kann, aber man kann viele Schnittstellen implementieren .
Angenommen, Sie haben Entitäten und geprüfte Entitäten benannt. Sie haben mehrere Entitäten:
One
ist weder eine geprüfte noch eine benannte Stelle. Das ist einfach:Two
ist eine benannte Stelle, aber keine geprüfte Stelle. Das ist im Wesentlichen, was Sie jetzt haben:Three
ist sowohl ein benannter als auch ein geprüfter Eintrag. Hier stößt man auf ein Problem. Sie können eineAuditedEntity
Basisklasse erstellen , aber nicht beideThree
erben und :AuditedEntity
NamedEntity
Sie könnten jedoch auch von einer Vermeidung des Problems denken , indem
AuditedEntity
vererbenNamedEntity
. Dies ist ein cleverer Hack, um sicherzustellen, dass jede Klasse nur (direkt) von einer anderen Klasse erben muss.Das funktioniert immer noch. Aber was Sie hier getan haben, ist, 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, aber keine benannte Entität. Aber Sie können nicht vonFour
erben lassen,AuditedEntity
da Sie es dann auchNamedEntity
aufgrund der Vererbung zwischen AuditedEntityand
NamedEntity` machen würden.Wenn Sie die Vererbung verwenden, gibt es keine Möglichkeit, beides zu erreichen
Three
und zuFour
funktionieren, es sei denn, Sie beginnen mit dem Duplizieren von Klassen (was eine ganze Reihe neuer Probleme aufwirft).Mit Hilfe von Schnittstellen kann dies leicht erreicht werden:
Der einzige kleine Nachteil hierbei ist, dass Sie die Schnittstelle noch implementieren müssen. Sie profitieren jedoch von allen Vorteilen eines gemeinsamen wiederverwendbaren Typs, ohne die Nachteile zu haben, die sich ergeben, wenn Sie Variationen mehrerer gemeinsamer Typen für eine bestimmte Entität benötigen.
Ihr Polymorphismus bleibt jedoch erhalten:
Dies ist eine Variation des Markierungsschnittstellenmusters , bei dem 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 das gleiche, daher werde ich es als "markierte Klasse" bezeichnen.
Mit dem Nennwert ist nichts falsch an Marker-Interfaces / -Klassen. Sie sind syntaktisch und technisch gültig und haben keine inhärenten Nachteile, vorausgesetzt, der Marker ist universell gültig (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 von Natur aus nichts Falsches daran, aber ich würde raten, dies vorsichtig zu verwenden, um sicherzustellen, dass Sie nicht nur versuchen, einen vorhandenen Architekturfehler mit schlecht entworfenem Polymorphismus zu vertuschen.
quelle
three
in Ihrem Beispiel für die Fallstricke, keine Schnittstellen zu verwenden, ist beispielsweise in C ++ tatsächlich gültig. Tatsächlich funktioniert dies für alle vier Beispiele ordnungsgemäß. Tatsächlich scheint dies alles eine Einschränkung von C # als Sprache zu sein und keine Warnung, wenn Sie keine Vererbung verwenden, wenn Sie Schnittstellen verwenden sollten.Dies ist etwas, das ich benutze, um zu verhindern, dass Polymorphismus verwendet wird.
Angenommen, Sie haben 15 verschiedene Klassen, die
NamedEntity
irgendwo in ihrer Vererbungskette eine Basisklasse haben , und Sie schreiben eine neue Methode, die nur auf anwendbar istOrderDateInfo
.Sie "könnten" einfach die Signatur schreiben als
void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)
Und hoffe und bete, dass niemand das Typensystem missbraucht, um ein einzuschieben
FooBazNamedEntity
.Oder du "könntest" einfach schreiben
void MyMethod(OrderDateInfo foo)
. Nun wird das vom Compiler erzwungen. Einfach, elegant und nicht darauf angewiesen, dass Menschen keine Fehler machen.Auch als @candied_orange wies darauf hin , Ausnahmen sind ein großer Fall dafür. Sehr selten (und ich meine sehr, sehr, sehr selten) möchten Sie jemals alles mit fangen
catch (Exception e)
. Mit größerer Wahrscheinlichkeit möchten Sie eineSqlException
oder eineFileNotFoundException
oder eine benutzerdefinierte Ausnahme für Ihre Anwendung abfangen . Diese Klassen bieten oftmals nicht mehr Daten oder Funktionen als die BasisklasseException
, aber sie ermöglichen es Ihnen, zu unterscheiden, was sie darstellen, ohne sie untersuchen und ein Typfeld überprüfen oder nach spezifischem Text suchen zu müssen.Insgesamt ist es ein Trick, das Typensystem so zu gestalten, dass Sie eine engere Menge von Typen verwenden können als mit einer Basisklasse. Ich meine, Sie könnten alle Ihre Variablen und Argumente so definieren
Object
, dass sie den Typ haben , aber das würde Ihre Arbeit nur erschweren, nicht wahr?quelle
NamedEntity
zB auf umbenennenEntityName
, damit die Komposition bei der Beschreibung Sinn macht.Dies ist meine Lieblingsvererbung. Ich benutze es meistens für Ausnahmen, die bessere, spezifischere Namen verwenden könnten
Das übliche Problem der Vererbung, die zu langen Ketten führt und das Jojo-Problem verursacht, trifft hier nicht zu, da es nichts gibt, das Sie zum Verketten motiviert.
quelle
IMHO ist das Klassendesign falsch. Es sollte sein.
OrderDateInfo
HAS AName
ist eine natürlichere Beziehung und erzeugt zwei leicht verständliche Klassen, die die ursprüngliche Frage nicht provoziert hätten.Jede Methode, die
NamedEntity
als Parameter akzeptiert wurde , sollte nur an den EigenschaftenId
und interessiert sein.Name
Daher sollten solche Methoden so geändert werden, dass sieEntityName
stattdessen akzeptieren .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 sich daran zu binden
object.Name.Id
. Aber wenn Ihr verbindliches Framework damit nicht zurechtkommt, müssen Sie der Liste weitere technische Schulden hinzufügen.Ich würde der Antwort von @ Flater zustimmen, aber mit vielen Schnittstellen, die Eigenschaften enthalten, schreiben Sie am Ende eine Menge Code, selbst mit den schönen automatischen Eigenschaften von C #. Stellen Sie sich vor, Sie machen es in Java!
quelle
Klassen legen das Verhalten offen, und Datenstrukturen legen Daten offen.
Ich sehe die Schlüsselwörter der Klasse, 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
Sie können also den Datentyp der obersten Ebene verwenden. Dies ermöglicht die Nutzung des Typsystems, um eine Richtlinie für große Mengen unterschiedlicher Datenstrukturen zu gewährleisten, indem sichergestellt wird, dass alle Eigenschaften vorhanden sind.
Sie können also den Datentyp der unteren Ebene verwenden. Dies ermöglicht das Einfügen von Hinweisen in das Typisierungssystem, um den Zweck der Variablen auszudrücken
In der obigen Hierarchie ist es zweckmäßig anzugeben, dass eine Person benannt ist, damit Personen ihren Namen erhalten und ändern können. Es mag zwar sinnvoll sein, der
Person
Datenstruktur zusätzliche Eigenschaften hinzuzufügen , das Problem, das wir lösen, erfordert jedoch keine perfekte Modellierung der Person. Daher haben wir es versäumt, gemeinsame Eigenschaften wieage
usw. hinzuzufügen .Es ist also eine Hebelwirkung des Schreibsystems, um die Absicht dieses benannten Elements in einer Weise auszudrücken, die nicht mit Aktualisierungen (wie Dokumentation) bricht und zu einem
age
späteren Zeitpunkt problemlos erweitert werden kann (wenn Sie feststellen, dass Sie das Feld später wirklich benötigen) ).quelle