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:
- Welche dieser Intuitionen sind bei Menschen, die über Designmuster sprechen, richtig? Ist die oben beschriebene Unterklasse ein Anti-Pattern?
- 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.
quelle
Antworten:
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.
quelle
SystemDataHelperBuilder
spezifisch ist.Ihr Mitarbeiter ist korrekt (unter der Annahme von Standardtypsystemen).
Denken Sie darüber nach, Klassen repräsentieren mögliche gesetzliche Werte. Wenn die Klasse
A
ein Byte-Feld enthältF
, können Sie davon ausgehen, dassA
256 zulässige Werte vorliegen, diese Intuition jedoch nicht korrekt ist.A
beschränkt "jede Permutation von Werten jemals" auf "muss FeldF
0-255 sein".Wenn Sie erweitern,
A
umB
welches Byte-FeldFF
es sich handelt , werden Einschränkungen hinzugefügt . Anstelle von "value whereF
is byte" haben Sie "value whereF
is byte &&FF
is 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
A
eine Reihe von Subtypen hatte:B
,C
, undD
. Eine Variable vom TypA
kann jeder dieser Untertypen sein, eine Variable vom TypB
ist jedoch spezifischer. Es kann (in den meisten Typsystemen) nicht aC
oder a seinD
.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.
quelle
byte and byte
definitiv mehr Werte als nur Typbyte
; 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.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 vonA
ist auch ein Element vonB
" definiert, weil genau dann, wenn Ihre Mengen einmal unendlich sind, es sein kann, dass sie dieselbe Kardinalität haben, aber unterschiedliche Elemente.Nein, es ist kein Anti-Pattern. Ich kann mir dafür eine Reihe von praktischen Anwendungsfällen vorstellen:
MySQLDao
undSqliteDao
in 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.AlgorithmConfiguration
+ProductClass
undAlgorithmInstance
. Mit anderen Worten, wenn Sie in der EigenschaftendateiFooAlgorithm
für konfigurierenProductClass
, können Sie nur eineFooAlgorithm
für diese Produktklasse abrufen. Die einzige Möglichkeit, zweiFooAlgorithm
s für eine bestimmteProductClass
Klasse zu erhalten, besteht darin, eine Unterklasse zu erstellenFooBarAlgorithm
. 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.Fazit: Das schadet nicht wirklich. Ein Anti-Pattern ist definiert als:
Hier besteht keine Gefahr einer hohen Kontraproduktivität, daher handelt es sich nicht um ein Anti-Pattern.
quelle
Ob etwas ein Muster oder ein Antimuster ist, hängt wesentlich von der Sprache und Umgebung ab, in der Sie schreiben.
for
Schleifen 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:
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).
quelle
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
ReadError
ein Sonderfall vonIOError
usw. ist,ReadError
müssen jedoch keine Methoden von überschreibenIOError
.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
ReadError
fangen kann, ohne alle zu fangenIOError
, 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 überschreibenloggingname()
. 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 NamensSystemDataHelperBuilder
auf 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)
undImmutableSquare(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, obheight == width
und nicht mitinstanceof
. In Ihrem Beispiel gibt es einen Unterschied zwischen aSystemDataHelperBuilder
und a,DataHelperBuilder
die 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.
quelle
ImmutableSquareMatrix:ImmutableMatrix
, vorausgesetzt, es gab ein effizientes Mittel, um einImmutableMatrix
zu bitten , seinen Inhalt alsImmutableSquareMatrix
wenn möglich zu rendern . Selbst wennImmutableSquareMatrix
es sich im Grunde genommen um eine Methode handelt,ImmutableMatrix
deren Konstruktor die Übereinstimmung von Höhe und Breite erfordert, und derenAsSquareMatrix
Methode sich einfach selbst zurückgibt (anstatt z. B. eine Methode zu konstruierenImmutableSquareMatrix
, 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 MatrizenSystemDataHelperBuilder
, und nicht irgendeinDataHelperBuilder
Wille, 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 .height == width
, nicht mitinstanceof
". Möglicherweise bevorzugen Sie etwas, das Sie statisch behaupten können.foo=new ImmutableMatrix(someReadableMatrix)
ist klarer als die vonsomeReadableMatrix.AsImmutable()
oder sogarImmutableMatrix.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.new
Operators. 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.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.
quelle
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.quelle
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.
quelle
Ich verwende Konstruktor-Unterklassen nur ziemlich regelmäßig, um verschiedene Konzepte auszudrücken.
Betrachten Sie beispielsweise die folgende Klasse:
Wir können dann eine Unterklasse erstellen:
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.
quelle
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
DatabaseEngine
undConnectionString
von der Unterklasse, auf die zugegriffen wurdeConfiguration
, 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.
quelle
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.
quelle