Nur-Konstruktor-Unterklassen: Ist dies ein Anti-Pattern?


37

Ich hatte eine Diskussion mit einem Kollegen, und es kam zu widersprüchlichen Vorstellungen über den Zweck der Unterklassenbildung. Meine Intuition ist, dass wenn eine primäre Funktion einer Unterklasse darin besteht, einen begrenzten Bereich möglicher Werte ihrer Eltern auszudrücken, es wahrscheinlich keine Unterklasse sein sollte. Er argumentierte für die gegenteilige Intuition: Die Unterklasse repräsentiert das "spezifischere" Objekt, weshalb eine Unterklassenbeziehung angemessener ist.

Um meine Intuition konkreter zu machen, denke ich, dass wenn ich eine Unterklasse habe, die eine Elternklasse erweitert, aber der einzige Code, den die Unterklasse überschreibt, ein Konstruktor ist (ja, ich weiß, dass Konstruktoren im Allgemeinen nicht "überschreiben", dann halte ich mit) Was wirklich gebraucht wurde, war eine Hilfsmethode.

Betrachten Sie zum Beispiel diese etwas realistische Klasse:

public class DataHelperBuilder
{
    public string DatabaseEngine { get; set; }
    public string ConnectionString { get; set; }

    public DataHelperBuilder(string databaseEngine, string connectionString)
    {
        DatabaseEngine = databaseEngine;
        ConnectionString = connectionString;
    }

    // Other optional "DataHelper" configuration settings omitted

    public DataHelper CreateDataHelper()
    {
        Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
        DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
        dh.SetConnectionString(ConnectionString);

        // Omitted some code that applies decorators to the returned object
        // based on omitted configuration settings

        return dh;
    }
}

Sein Anspruch ist, dass es völlig angemessen wäre, eine Unterklasse wie diese zu haben:

public class SystemDataHelperBuilder
{
    public SystemDataHelperBuilder()
        : base(Configuration.GetSystemDatabaseEngine(),
               Configuration.GetSystemConnectionString())
    {
    }
 }

Also die Frage:

  1. Welche dieser Intuitionen sind bei Menschen, die über Designmuster sprechen, richtig? Ist die oben beschriebene Unterklasse ein Anti-Pattern?
  2. Wenn es sich um ein Anti-Pattern handelt, wie heißt es?

Ich entschuldige mich, wenn sich herausstellt, dass dies eine leicht googleable Antwort war; Meine Suchanfragen bei Google ergaben meistens Informationen über das Anti-Pattern des Teleskopkonstruktors und nicht wirklich das, wonach ich suchte.


2
Ich betrachte das Wort "Helfer" in einem Namen als Gegenmuster. Es ist wie das Lesen eines Aufsatzes mit ständigen Hinweisen auf ein Ding und einen Whatsit. Sag was es ist . Ist es eine Fabrik? Konstrukteur? Für mich riecht es nach einem faulen Denkprozess und ermutigt zur Verletzung des Grundsatzes der Einzelverantwortung, da ein richtiger semantischer Name ihn lenken würde. Ich stimme den anderen Wählern zu und Sie sollten die Antwort von @ lxrec akzeptieren. Das Erstellen einer Unterklasse für eine Factory-Methode ist völlig sinnlos, es sei denn, Sie können den Benutzern nicht die in der Dokumentation angegebenen Argumente anvertrauen. In diesem Fall erhalten Sie neue Benutzer.
Aaron Hall

Ich stimme der Antwort von @ Ixrec absolut zu. Immerhin sagt seine Antwort, dass ich die ganze Zeit Recht hatte und mein Kollege sich geirrt hatte. ;-) Aber im Ernst, genau deshalb fand ich es komisch, das zu akzeptieren. Ich dachte, ich könnte der Voreingenommenheit beschuldigt werden. Aber ich bin neu bei den Programmierern von StackExchange. Ich wäre bereit, das zu akzeptieren, wenn ich mir sicher wäre, dass es nicht gegen die Etikette verstößt.
HardlyKnowEm

1
Nein, es wird erwartet, dass Sie die Antwort akzeptieren, die Sie für am wertvollsten halten. Diejenige, die die Community bisher für die wertvollste hält, ist die von Ixrec mit Abstand um mehr als 3 zu 1. Sie würden also erwarten, dass Sie sie akzeptieren. In dieser Community gibt es sehr wenig Frustration, wenn ein Fragesteller die falsche Antwort akzeptiert, aber es gibt viel Frustration, wenn ein Fragesteller niemals eine Antwort akzeptiert. Beachten Sie, dass sich hier niemand über die akzeptierte Antwort beschwert, sondern über das Voting, das sich zu richten
Aaron Hall

Sehr gut, ich werde es akzeptieren. Vielen Dank, dass Sie sich die Zeit genommen haben, mich über die Community-Standards hier zu informieren.
HardlyKnowEm

Antworten:


54

Wenn Sie nur Klasse X mit bestimmten Argumenten erstellen möchten, ist die Unterklasse eine ungewöhnliche Methode, um diese Absicht auszudrücken, da Sie keine der Funktionen verwenden, die Klassen und Vererbung Ihnen bieten. Es ist nicht wirklich ein Anti-Muster, es ist nur seltsam und ein bisschen sinnlos (es sei denn, Sie haben andere Gründe dafür). Eine natürlichere Art, diese Absicht auszudrücken, wäre eine Factory-Methode , die in diesem Fall ein ausgefallener Name für Ihre "Helfer-Methode" ist.

In Bezug auf die allgemeine Intuition sind sowohl "spezifischer" als auch "ein begrenzter Bereich" potenziell schädliche Denkweisen für Unterklassen, da beide implizieren, dass es eine gute Idee ist, Square zu einer Unterklasse von Rectangle zu machen. Ohne mich auf etwas Formales wie LSP zu verlassen, würde ich sagen, dass eine Unterklasse entweder eine Implementierung der Basisschnittstelle bereitstellt oder die Schnittstelle erweitert , um einige neue Funktionen hinzuzufügen.


2
Mit Factory-Methoden können Sie jedoch bestimmte (durch Argumente bestimmte) Verhaltensweisen in Ihrer Codebasis nicht erzwingen. Und manchmal ist es nützlich, explizit zu kommentieren, dass diese Klasse / Methode / dieses Feld SystemDataHelperBuilderspezifisch ist.
Telastyn

3
Ah, das Quadrat-gegen-Rechteck-Argument war genau das, wonach ich gesucht habe, glaube ich. Vielen Dank!
HardlyKnowEm

7
Natürlich kann es in Ordnung sein, wenn Quadrat eine Unterklasse von Rechtecken ist, wenn sie unveränderlich sind. Man muss sich wirklich um den LSP kümmern.
David Conrad

10
Das Problem mit dem Quadrat / Rechteck ist, dass geometrische Formen unveränderlich sind. Sie können ein Quadrat immer dann verwenden, wenn ein Rechteck Sinn macht, aber die Größenänderung ist nichts, was Quadrate oder Rechtecke tun.
Doval

3
@nha LSP = Liskov Substitution Principle
Ixrec

16

Welche dieser Anschauungen ist richtig?

Ihr Mitarbeiter ist korrekt (unter der Annahme von Standardtypsystemen).

Denken Sie darüber nach, Klassen repräsentieren mögliche gesetzliche Werte. Wenn die Klasse Aein Byte-Feld enthält F, können Sie davon ausgehen, dass A256 zulässige Werte vorliegen, diese Intuition jedoch nicht korrekt ist. Abeschränkt "jede Permutation von Werten jemals" auf "muss Feld F0-255 sein".

Wenn Sie erweitern, Aum Bwelches Byte-Feld FFes sich handelt , werden Einschränkungen hinzugefügt . Anstelle von "value where Fis byte" haben Sie "value where Fis byte && FFis also byte". Da Sie die alten Regeln beibehalten, funktioniert alles, was für den Basistyp funktioniert hat, auch für Ihren Subtyp. Aber da Sie Regeln hinzufügen, spezialisieren Sie sich weiter weg von "könnte alles sein".

Oder denken Sie an es einen anderen Weg, davon ausgehen , dass Aeine Reihe von Subtypen hatte: B, C, und D. Eine Variable vom Typ Akann jeder dieser Untertypen sein, eine Variable vom Typ Bist jedoch spezifischer. Es kann (in den meisten Typsystemen) nicht a Coder a sein D.

Ist das ein Anti-Muster?

Enh? Spezialisierte Objekte sind nützlich und für den Code nicht eindeutig schädlich. Das Erreichen dieses Ergebnisses durch Subtypisierung ist vielleicht etwas fraglich, hängt jedoch davon ab, über welche Tools Sie verfügen. Auch mit besseren Tools ist diese Implementierung einfach, unkompliziert und robust.

Ich würde dies nicht als Anti-Muster betrachten, da es in keiner Situation eindeutig falsch und schlecht ist.


5
"Mehr Regeln bedeuten weniger mögliche Werte, bedeutet spezifischer." Ich folge dem wirklich nicht. Typ hat byte and bytedefinitiv mehr Werte als nur Typ byte; 256 mal so viele. Abgesehen davon glaube ich nicht, dass Sie Regeln mit der Anzahl der Elemente (oder der Kardinalität, wenn Sie präzise sein möchten) in Konflikt bringen können. Es bricht zusammen, wenn Sie unendliche Mengen betrachten. Es gibt genauso viele gerade Zahlen wie ganze Zahlen, aber zu wissen, dass eine Zahl gerade ist, ist immer noch eine Einschränkung / gibt mir mehr Informationen, als nur zu wissen, dass es eine ganze Zahl ist! Gleiches gilt für die natürlichen Zahlen.
Doval

6
@Telastyn Wir kommen vom Thema ab, aber es ist nachweislich, dass die Kardinalität (lose, "Größe") der Menge der geraden Ganzzahlen dieselbe ist wie die Menge aller Ganzzahlen oder sogar aller rationalen Zahlen. Es ist wirklich cooles Zeug. Siehe math.grinnell.edu/~miletijo/museum/infinite.html
HardlyKnowEm

2
Mengenlehre ist ziemlich unintuitiv. Sie können die Ganzzahlen und Gleichheiten in eine Eins-zu-Eins-Korrespondenz setzen (z. B. mit der Funktion n -> 2n). Das ist nicht immer möglich; Sie können die ganzen Zahlen nicht den reellen Zahlen zuordnen, daher ist die Menge der reellen Zahlen "größer" als die ganzen Zahlen. Sie können jedoch das (reelle) Intervall [0, 1] auf die Menge aller reellen Werte abbilden, sodass sie dieselbe "Größe" haben. Untermengen werden als "jedes Element von Aist auch ein Element von B" definiert, weil genau dann, wenn Ihre Mengen einmal unendlich sind, es sein kann, dass sie dieselbe Kardinalität haben, aber unterschiedliche Elemente.
Doval

1
Das Beispiel, das Sie geben, bezieht sich eher auf das vorliegende Thema und stellt eine Einschränkung dar, die in Bezug auf die objektorientierte Programmierung die Funktionalität tatsächlich um ein zusätzliches Feld erweitert. Ich spreche von Unterklassen, die die Funktionalität nicht erweitern - wie das Kreisellipsenproblem.
HardlyKnowEm

1
@Doval: Es gibt mehr als eine mögliche Bedeutung von "weniger" - dh mehr als eine Ordnungsbeziehung für Mengen. Die Beziehung "ist eine Untermenge von" gibt eine genau definierte (teilweise) Reihenfolge von Mengen an. Darüber hinaus kann unter "mehr Regeln" verstanden werden, dass "alle Elemente der Menge mehr (dh eine Obermenge von) Aussagen (von einer bestimmten Menge) erfüllen". Mit diesen Definitionen bedeutet "mehr Regeln" in der Tat "weniger Werte". Dies ist grob gesagt der Ansatz einer Reihe semantischer Modelle von Computerprogrammen, z. B. von Scott-Domänen.
Psmears

12

Nein, es ist kein Anti-Pattern. Ich kann mir dafür eine Reihe von praktischen Anwendungsfällen vorstellen:

  1. Wenn Sie die Überprüfung der Kompilierungszeit verwenden möchten, um sicherzustellen, dass Objektgruppen nur einer bestimmten Unterklasse entsprechen. Wenn Sie beispielsweise MySQLDaound SqliteDaoin Ihrem System haben, aber aus irgendeinem Grund sicherstellen möchten, dass eine Sammlung nur Daten aus einer Quelle enthält, können Sie den Compiler diese bestimmte Richtigkeit überprüfen lassen, wenn Sie wie beschrieben Unterklassen verwenden. Wenn Sie die Datenquelle hingegen als Feld speichern, wird dies zu einer Laufzeitüberprüfung.
  2. Meine aktuelle Bewerbung hat eine Eins-zu-Eins-Beziehung zwischen AlgorithmConfiguration+ ProductClassund AlgorithmInstance. Mit anderen Worten, wenn Sie in der Eigenschaftendatei FooAlgorithmfür konfigurieren ProductClass, können Sie nur eine FooAlgorithmfür diese Produktklasse abrufen. Die einzige Möglichkeit, zwei FooAlgorithms für eine bestimmte ProductClassKlasse zu erhalten, besteht darin, eine Unterklasse zu erstellen FooBarAlgorithm. Dies ist in Ordnung, da dadurch die Verwendung der Eigenschaftendatei vereinfacht wird (für viele verschiedene Konfigurationen in einem Abschnitt der Eigenschaftendatei ist keine Syntax erforderlich) und es sehr selten mehr als eine Instanz für eine bestimmte Produktklasse gibt.
  3. @ Telastyns Antwort gibt ein weiteres gutes Beispiel.

Fazit: Das schadet nicht wirklich. Ein Anti-Pattern ist definiert als:

Ein Antimuster (oder Antimuster) ist eine häufige Antwort auf ein wiederkehrendes Problem, das normalerweise ineffektiv ist und das Risiko birgt, äußerst kontraproduktiv zu sein.

Hier besteht keine Gefahr einer hohen Kontraproduktivität, daher handelt es sich nicht um ein Anti-Pattern.


2
Ja, ich denke, wenn ich die Frage noch einmal formulieren müsste, würde ich "Codegeruch" sagen. Es ist nicht schlecht genug , um zu rechtfertigen , die nächste Generation von jungen Entwicklern warnen , es zu vermeiden, aber es ist etwas , dass , wenn Sie es sehen, es kann zeigen , dass es Thema in dem übergeordneten Design, sondern auch letztlich korrekt.
HardlyKnowEm

Punkt 1 ist genau richtig. Ich habe Punkt 2 nicht wirklich verstanden, aber ich bin sicher, es ist genauso gut ... :)
Phil

9

Ob etwas ein Muster oder ein Antimuster ist, hängt wesentlich von der Sprache und Umgebung ab, in der Sie schreiben. forSchleifen sind beispielsweise ein Muster in Assembly-, C- und ähnlichen Sprachen und ein Antimuster in Lisp.

Lassen Sie mich Ihnen eine Geschichte über einen Code erzählen, den ich vor langer Zeit geschrieben habe ...

Es war in einer Sprache namens LPC und implementierte ein Framework zum Wirken von Zaubersprüchen in einem Spiel. Du hattest eine Zaubersuperklasse, die eine Unterklasse von Kampfzaubern enthielt, die einige Prüfungen abwickelte, und dann eine Unterklasse für einzelne Ziel-Direktschadenszauber, die dann den einzelnen Zaubern zugeordnet wurden - Magische Rakete, Blitz und dergleichen.

Die Art und Weise, wie LPC funktionierte, war, dass Sie ein Master-Objekt hatten, das ein Singleton war. bevor du gehst "eww Singleton" - es war und es war überhaupt nicht "eww". Man hat keine Kopien der Zaubersprüche angefertigt, sondern die (effektiv) statischen Methoden in den Zaubersprüchen aufgerufen. Der Code für Magic Missile sah folgendermaßen aus:

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

Und siehst du das? Es ist nur eine Klasse für Konstruktoren. Es wurden einige Werte festgelegt, die in der übergeordneten abstrakten Klasse enthalten waren (nein, es sollten keine Setter verwendet werden - es ist unveränderlich) und das war es. Es hat richtig funktioniert . Es war einfach zu programmieren, leicht zu erweitern und leicht zu verstehen.

Diese Art von Muster lässt sich auf andere Situationen übertragen, in denen Sie eine Unterklasse haben, die eine abstrakte Klasse erweitert und einige eigene Werte festlegt, die gesamte Funktionalität sich jedoch in der abstrakten Klasse befindet, die sie erbt. Werfen Sie einen Blick auf StringBuilder in Java, um ein Beispiel dafür zu finden - nicht nur ein Konstruktor, sondern ich bin gezwungen, viel Logik in den Methoden selbst zu finden (alles ist im AbstractStringBuilder enthalten).

Dies ist keine schlechte Sache, und es ist sicherlich kein Anti-Muster (und in einigen Sprachen kann es ein Muster selbst sein).


2

Die gebräuchlichste Verwendung solcher Unterklassen sind Ausnahmehierarchien, obwohl dies ein entarteter Fall ist, in dem wir in der Klasse normalerweise überhaupt nichts definieren würden, solange die Sprache uns Konstruktoren erben lässt. Wir verwenden die Vererbung, um auszudrücken, dass dies ReadErrorein Sonderfall von IOErrorusw. ist, ReadErrormüssen jedoch keine Methoden von überschreiben IOError.

Wir tun dies, weil Ausnahmen durch Überprüfen ihres Typs abgefangen werden. Daher müssen wir die Spezialisierung in den Typ kodieren, damit jemand nur ReadErrorfangen kann, ohne alle zu fangen IOError, wenn er möchte.

Normalerweise ist es furchtbar schlecht, die Art der Dinge zu überprüfen, und wir versuchen, dies zu vermeiden. Wenn es uns gelingt, dies zu vermeiden, müssen keine Spezialisierungen im Typensystem vorgenommen werden.

Es kann dennoch nützlich sein, wenn der Name des Typs beispielsweise in der protokollierten Zeichenfolgenform des Objekts angezeigt wird: Dies ist eine Sprache und möglicherweise ein spezifisches Framework. Sie können den Namen des Typs in einem solchen System im Prinzip durch einen Aufruf einer Methode der Klasse ersetzen. In diesem Fall würden wir nicht nur den Konstruktor in SystemDataHelperBuilderüberschreiben, sondern auch überschreiben loggingname(). Wenn es also eine solche Protokollierung in Ihrem System gibt, können Sie sich vorstellen, dass Ihre Klasse zwar buchstäblich nur den Konstruktor überschreibt, aber "moralisch" auch den Klassennamen überschreibt und daher aus praktischen Gründen keine Unterklasse nur für Konstruktoren ist. Es hat wünschenswert anderes Verhalten, es '

Ich würde also sagen, dass sein Code eine gute Idee sein kann, wenn es irgendwo anders Code gibt, der auf Instanzen von prüft SystemDataHelperBuilder, entweder explizit mit einer Verzweigung (wie das Abfangen von Verzweigungen basierend auf dem Typ) oder vielleicht, weil es irgendeinen generischen Code gibt, der enden wird Verwenden des Namens SystemDataHelperBuilderauf nützliche Weise (auch wenn es sich nur um Protokollierung handelt).

Allerdings ist mit Typen für Sonderfälle zu einiger Verwirrung führen, denn wenn ImmutableRectangle(1,1)und ImmutableSquare(1)verhalten sich anders in irgendeiner Weise dann schließlich jemand zu fragen , warum wird und vielleicht wollen sie nicht. Im Gegensatz zu Ausnahmehierarchien prüfen Sie, ob ein Rechteck ein Quadrat ist, indem Sie prüfen, ob height == widthund nicht mit instanceof. In Ihrem Beispiel gibt es einen Unterschied zwischen a SystemDataHelperBuilderund a, DataHelperBuilderdie mit genau den gleichen Argumenten erstellt wurden, und das kann Probleme verursachen. Daher scheint mir eine Funktion zum Zurückgeben eines Objekts, das mit den "richtigen" Argumenten erstellt wurde, normalerweise das Richtige zu sein: Die Unterklasse führt zu zwei verschiedenen Darstellungen von "derselben" Sache, und es hilft nur, wenn Sie etwas tun, was Sie tun Normalerweise würde ich versuchen, es nicht zu tun.

Beachten Sie, dass ich nicht von Entwurfsmustern und Anti-Mustern spreche, weil ich nicht glaube, dass es möglich ist, eine Taxonomie aller guten und schlechten Ideen bereitzustellen, an die ein Programmierer jemals gedacht hat. Somit ist nicht jede Idee ein Muster oder ein Anti-Muster, sondern es wird erst, wenn jemand erkennt, dass es in verschiedenen Kontexten wiederholt vorkommt und benennt ;-) Mir ist kein bestimmter Name für diese Art von Unterklassen bekannt.


Ich konnte Verwendungen für sehen ImmutableSquareMatrix:ImmutableMatrix, vorausgesetzt, es gab ein effizientes Mittel, um ein ImmutableMatrixzu bitten , seinen Inhalt als ImmutableSquareMatrixwenn möglich zu rendern . Selbst wenn ImmutableSquareMatrixes sich im Grunde genommen um eine Methode handelt, ImmutableMatrixderen Konstruktor die Übereinstimmung von Höhe und Breite erfordert, und deren AsSquareMatrixMethode sich einfach selbst zurückgibt (anstatt z. B. eine Methode zu konstruieren ImmutableSquareMatrix, die dasselbe Backing-Array umschließt), würde ein solches Design die Validierung von Parametern zur Kompilierungszeit für Methoden ermöglichen, die ein Quadrat erfordern Matrizen
Supercat

@supercat: Guter Punkt, und im Beispiel des Fragestellers, wenn es Funktionen gibt, die übergeben werden müssen SystemDataHelperBuilder, und nicht irgendein DataHelperBuilderWille, dann ist es sinnvoll, einen Typ dafür zu definieren (und eine Schnittstelle, vorausgesetzt, Sie behandeln DI auf diese Weise). . In Ihrem Beispiel gibt es einige Operationen, die eine Quadratmatrix haben könnte, die ein Nichtquadrat nicht hätte, wie z. B. Determinante, aber Sie haben den Vorteil, dass Sie diese nach Typ überprüfen möchten, auch wenn das System diese nicht benötigt .
Steve Jessop

... also ich nehme an, es ist nicht ganz wahr, was ich gesagt habe, dass "man prüft, ob ein Rechteck ein Quadrat ist, indem man prüft, ob height == width, nicht mit instanceof". Möglicherweise bevorzugen Sie etwas, das Sie statisch behaupten können.
Steve Jessop

Ich wünschte, es gäbe einen Mechanismus, mit dem so etwas wie Konstruktorsyntax statische Factory-Methoden aufrufen könnte. Die Art des Ausdrucks foo=new ImmutableMatrix(someReadableMatrix)ist klarer als die von someReadableMatrix.AsImmutable()oder sogar ImmutableMatrix.Create(someReadableMatrix), aber die letzteren Formen bieten nützliche semantische Möglichkeiten, die den ersteren fehlen; Wenn der Konstruktor erkennen kann, dass die Matrix quadratisch ist, kann sein Typ reflektiert werden, was sauberer sein könnte, als dass Code nachgeschaltet werden muss, der eine quadratische Matrix benötigt, um eine neue Objektinstanz zu erstellen.
Supercat

@supercat: Eine kleine Sache an Python, die ich mag, ist das Fehlen eines newOperators. Eine Klasse ist nur eine aufrufbare Klasse, die beim Aufruf (standardmäßig) eine Instanz von sich selbst zurückgibt. Wenn Sie die Konsistenz der Benutzeroberfläche beibehalten möchten, ist es durchaus möglich, eine Factory-Funktion zu schreiben, deren Name mit einem Großbuchstaben beginnt. Daher ähnelt sie einem Konstruktoraufruf. Nicht, dass ich das routinemäßig mit Werksfunktionen mache, aber es ist da, wenn Sie es brauchen.
Steve Jessop

2

Die strengste objektorientierte Definition einer Unterklassenbeziehung ist als «is-a» bekannt. Mit dem traditionellen Beispiel eines Quadrats und eines Rechtecks ​​ist ein Quadrat ein Rechteck.

Auf diese Weise sollten Sie Unterklassen für alle Situationen verwenden, in denen Sie der Meinung sind, dass ein "Ist-Ist" eine sinnvolle Beziehung für Ihren Code ist.

In Ihrem speziellen Fall einer Unterklasse mit nichts als einem Konstruktor sollten Sie diese aus einer "Ist-Ist" -Perspektive analysieren. Es ist klar, dass ein SystemDataHelperBuilder «ein» DataHelperBuilder «ist. Der erste Durchgang legt also nahe, dass es sich um eine gültige Verwendung handelt.

Die Frage, die Sie beantworten sollten, ist, ob einer der anderen Codes diese «is-a» -Beziehung nutzt. Versucht irgendein anderer Code, SystemDataHelperBuilders von der allgemeinen Population von DataHelperBuilders zu unterscheiden? In diesem Fall ist die Beziehung durchaus sinnvoll, und Sie sollten die Unterklasse beibehalten.

Wenn dies jedoch kein anderer Code tut, haben Sie eine Beziehung, die eine "Ist-Ist" -Beziehung sein kann, oder die mit einer Factory implementiert werden kann. In diesem Fall könnten Sie beide Wege einschlagen, aber ich würde eher eine Fabrik als eine «Ist-Eine» -Beziehung empfehlen. Bei der Analyse von objektorientiertem Code, der von einer anderen Person geschrieben wurde, ist die Klassenhierarchie eine wichtige Informationsquelle. Es bietet die "Ist-Ist" -Beziehungen, die sie benötigen, um den Code zu verstehen. Dementsprechend fördert das Einordnen dieses Verhaltens in eine Unterklasse das Verhalten von "etwas, das jemand findet, wenn er in die Methoden der Oberklasse eintaucht" bis zu "etwas, das jeder durchschaut, bevor er überhaupt mit dem Eintauchen in den Code beginnt". Guido van Rossum zitiert: "Code wird öfter gelesen als geschrieben." Eine schlechtere Lesbarkeit Ihres Codes für zukünftige Entwickler ist daher ein schwerer Preis. Ich würde mich dafür entscheiden, es nicht zu bezahlen, wenn ich stattdessen eine Fabrik benutzen könnte.


1

Ein Subtyp ist niemals "falsch", solange Sie immer eine Instanz seines Supertyps durch eine Instanz des Subtyps ersetzen können und alles noch korrekt funktioniert. Dies wird überprüft, solange die Unterklasse niemals versucht, die Garantien des Supertyps zu schwächen. Es kann stärkere (spezifischere) Garantien geben, in diesem Sinne ist die Intuition Ihres Mitarbeiters korrekt.

Angesichts dessen ist die von Ihnen erstellte Unterklasse wahrscheinlich nicht falsch (da ich nicht genau weiß, was sie tun, kann ich nicht sicher sagen) interfaceÜberlegen Sie, ob eine Ihnen zugute kommen würde, aber das gilt für alle Vererbungszwecke.


1

Ich denke, der Schlüssel zur Beantwortung dieser Frage liegt in der Betrachtung dieses einen bestimmten Verwendungsszenarios.

Die Vererbung wird verwendet, um Datenbankkonfigurationsoptionen wie die Verbindungszeichenfolge zu erzwingen.

Dies ist ein Anti-Pattern, da die Klasse gegen das Prinzip der Einzelverantwortung verstößt. Es konfiguriert sich selbst mit einer bestimmten Quelle dieser Konfiguration und nicht mit "Eine Sache tun und es gut machen". Die Konfiguration einer Klasse sollte nicht in die Klasse selbst eingebrannt werden. Beim Festlegen von Datenbankverbindungszeichenfolgen habe ich dies in den meisten Fällen auf Anwendungsebene während eines Ereignisses gesehen, das beim Starten der Anwendung auftritt.


1

Ich verwende Konstruktor-Unterklassen nur ziemlich regelmäßig, um verschiedene Konzepte auszudrücken.

Betrachten Sie beispielsweise die folgende Klasse:

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

Wir können dann eine Unterklasse erstellen:

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

Sie können dann einen CapitalizeAndPrintOutputter an einer beliebigen Stelle im Code verwenden und wissen genau, um welchen Typ von Ausgabe es sich handelt. Darüber hinaus macht es dieses Muster tatsächlich sehr einfach, einen DI-Container zum Erstellen dieser Klasse zu verwenden. Wenn der DI-Container mit PrintOutputMethod und Capitalizer vertraut ist, kann er den CapitalizeAndPrintOutputter sogar automatisch für Sie erstellen.

Die Verwendung dieses Musters ist sehr flexibel und nützlich. Ja, Sie können Factory-Methoden verwenden, um das Gleiche zu tun, aber dann (zumindest in C #) verlieren Sie einen Teil der Leistung des Typsystems, und die Verwendung von DI-Containern wird mit Sicherheit umständlicher.


1

Lassen Sie mich zunächst feststellen, dass die Unterklasse beim Schreiben des Codes nicht spezifischer ist als die übergeordnete Klasse, da die beiden initialisierten Felder beide durch den Clientcode einstellbar sind.

Eine Instanz der Unterklasse kann nur durch einen Downcast unterschieden werden.

Wenn Sie nun ein Design haben, in dem die Eigenschaften DatabaseEngineund ConnectionStringvon der Unterklasse, auf die zugegriffen wurde Configuration, erneut implementiert wurden , haben Sie eine Einschränkung, die für die Unterklasse sinnvoller ist.

Abgesehen davon ist "Anti-Muster" für meinen Geschmack zu stark; Hier ist nichts wirklich falsch.


0

In dem von Ihnen gegebenen Beispiel hat Ihr Partner Recht. Die beiden Data Helper Builder sind Strategien. Eines ist spezifischer als das andere, aber beide sind nach der Initialisierung austauschbar.

Grundsätzlich würde nichts kaputt gehen, wenn ein Client des Datahelperbuilders den Systemdatahelperbuilder erhalten würde. Eine andere Möglichkeit wäre gewesen, dass der systemdatahelperbuilder eine statische Instanz der datahelperbuilder-Klasse ist.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.