Betrachten Sie das folgende Design
public class Person
{
public virtual string Name { get; }
public Person (string name)
{
this.Name = name;
}
}
public class Karl : Person
{
public override string Name
{
get
{
return "Karl";
}
}
}
public class John : Person
{
public override string Name
{
get
{
return "John";
}
}
}
Denken Sie, dass hier etwas nicht stimmt? Für mich sollten Karl- und John-Klassen nur Instanzen anstelle von Klassen sein, da sie genau die gleichen sind wie:
Person karl = new Person("Karl");
Person john = new Person("John");
Warum sollte ich neue Klassen erstellen, wenn Instanzen ausreichen? Die Klassen fügen der Instanz nichts hinzu.
programming-practices
inheritance
class-design
anti-patterns
Ignacio Soler Garcia
quelle
quelle
Antworten:
Es ist nicht erforderlich, für jede Person spezifische Unterklassen zu haben.
Du hast recht, das sollten stattdessen Instanzen sein.
Ziel der Unterklassen: Erweiterung der Elternklassen
Unterklassen werden verwendet, um die von der übergeordneten Klasse bereitgestellten Funktionen zu erweitern . Zum Beispiel haben Sie vielleicht:
Eine Elternklasse,
Battery
diePower()
etwas kann und eineVoltage
Eigenschaft haben kann,Und eine Unterklasse
RechargeableBattery
, diePower()
und erben kannVoltage
, aber auchRecharge()
d sein kann.Beachten Sie, dass Sie eine Instanz von
RechargeableBattery
class als Parameter an jede Methode übergeben können, die aBattery
als Argument akzeptiert . Dies nennt man Liskov-Substitutionsprinzip , eines der fünf SOLID-Prinzipien. In ähnlicher Weise kann ich in der Praxis zwei wiederaufladbare AA-Batterien einsetzen, wenn mein MP3-Player zwei AA-Batterien akzeptiert.Beachten Sie, dass es manchmal schwierig ist, zu bestimmen, ob Sie ein Feld oder eine Unterklasse verwenden müssen, um einen Unterschied zwischen etwas darzustellen. Wenn Sie zum Beispiel mit AA-, AAA- und 9-Volt-Batterien umgehen müssen, würden Sie drei Unterklassen erstellen oder eine Aufzählung verwenden? „Ersetzen Sie die Unterklasse durch Felder“ in Refactoring von Martin Fowler, Seite 241, gibt Ihnen möglicherweise Anregungen und Hinweise zum Wechseln von einer zur anderen.
In Ihrem Beispiel,
Karl
undJohn
erweitern Sie nichts, noch bieten sie einen zusätzlichen Wert: Sie können die gleiche Funktionalität mitPerson
class direkt haben. Mehr Codezeilen ohne zusätzlichen Wert zu haben, ist niemals gut.Ein Beispiel für einen Business Case
Was könnte ein Geschäftsfall sein, bei dem es tatsächlich sinnvoll wäre, eine Unterklasse für eine bestimmte Person zu erstellen?
Angenommen, wir erstellen eine Anwendung, mit der Personen verwaltet werden, die in einem Unternehmen arbeiten. Die Anwendung verwaltet auch Berechtigungen, sodass die Buchhalterin Helen nicht auf das SVN-Repository zugreifen kann, die beiden Programmierer Thomas und Mary jedoch nicht auf buchhaltungsbezogene Dokumente.
Jimmy, der Big Boss (Gründer und CEO des Unternehmens), hat ganz bestimmte Privilegien, die niemand hat. Er kann beispielsweise das gesamte System herunterfahren oder eine Person entlassen. Du hast die Idee.
Das schlechteste Modell für eine solche Anwendung sind Klassen wie:
weil Code-Duplizierung sehr schnell auftreten wird. Selbst im einfachen Beispiel von vier Mitarbeitern duplizieren Sie den Code zwischen den Klassen von Thomas und Mary. Dadurch werden Sie aufgefordert, eine gemeinsame übergeordnete Klasse zu erstellen
Programmer
. Da Sie möglicherweise auch mehrere Buchhalter haben, werden Sie wahrscheinlich auch eineAccountant
Klasse erstellen .Sie bemerken jetzt, dass es
Helen
nicht sehr nützlich ist, die Klasse zu haben , und auch nicht,Thomas
undMary
: Der größte Teil Ihres Codes funktioniert sowieso auf der höheren Ebene - auf der Ebene der Buchhalter, Programmierer und Jimmy. Dem SVN-Server ist es egal, ob es Thomas oder Mary ist, die auf das Protokoll zugreifen müssen. Er muss nur wissen, ob es sich um einen Programmierer oder einen Buchhalter handelt.So entfernen Sie am Ende die Klassen, die Sie nicht verwenden:
"Aber ich kann Jimmy so lassen, wie er ist, denn es würde immer nur einen CEO geben, einen großen Boss - Jimmy", denken Sie. Darüber hinaus wird Jimmy häufig in Ihrem Code verwendet, was tatsächlich eher so aussieht, und nicht wie im vorherigen Beispiel:
Das Problem bei diesem Ansatz ist, dass Jimmy immer noch von einem Bus angefahren werden kann und es einen neuen CEO geben würde. Oder der Verwaltungsrat entscheidet, dass Mary so großartig ist, dass sie eine neue CEO sein sollte, und Jimmy würde zum Verkäufer degradiert. Jetzt müssen Sie Ihren gesamten Code durchgehen und alles ändern.
quelle
Es ist albern, diese Art von Klassenstruktur zu verwenden, nur um Details zu variieren, bei denen es sich offensichtlich um einstellbare Felder für eine Instanz handeln sollte. Dies ist jedoch speziell für Ihr Beispiel.
Es
Person
ist mit ziemlicher Sicherheit eine schlechte Idee, verschiedene Klassen zu erstellen. Sie müssen Ihr Programm ändern und jedes Mal neu kompilieren, wenn eine neue Person Ihre Domain betritt. Dies bedeutet jedoch nicht, dass das Erben von einer vorhandenen Klasse, sodass eine andere Zeichenfolge zurückgegeben wird, manchmal nicht sinnvoll ist.Der Unterschied besteht darin, wie Sie erwarten, dass die von dieser Klasse dargestellten Entitäten variieren. Es wird fast immer davon ausgegangen, dass Benutzer kommen und gehen, und von fast jeder seriösen Anwendung, die sich mit Benutzern befasst, wird erwartet, dass sie Benutzer zur Laufzeit hinzufügen und entfernen kann. Wenn Sie jedoch Dinge wie unterschiedliche Verschlüsselungsalgorithmen modellieren, ist die Unterstützung eines neuen wahrscheinlich ohnehin eine große Änderung, und es ist sinnvoll, eine neue Klasse zu erfinden, deren
myName()
Methode einen anderen String zurückgibt (und derenperform()
Methode vermutlich etwas anderes tut).quelle
In den meisten Fällen würden Sie nicht. Ihr Beispiel ist wirklich ein guter Fall, in dem dieses Verhalten keinen echten Mehrwert bringt.
Es verstößt auch gegen das Open Closed-Prinzip , da die Unterklassen im Grunde keine Erweiterung sind, sondern das Innenleben der Eltern verändern. Darüber hinaus ist der öffentliche Konstruktor des übergeordneten Elements jetzt in den Unterklassen irritierend und die API wurde somit weniger verständlich.
Manchmal ist es jedoch praktischer und weniger zeitaufwendig, nur ein übergeordnetes Element mit einem komplexen Konstruktor in Unterklassen zu unterteilen, wenn Sie immer nur eine oder zwei spezielle Konfigurationen haben, die häufig im gesamten Code verwendet werden. In solchen Sonderfällen kann ich an einem solchen Ansatz nichts Falsches erkennen. Nennen wir es Konstruktor currying j / k
quelle
Wenn dies das Ausmaß der Praxis ist, dann stimme ich zu, dass dies eine schlechte Praxis ist.
Wenn sich John und Karl unterschiedlich verhalten, ändern sich die Dinge ein wenig. Es könnte sein, dass
person
es eine Methode gibt,cleanRoom(Room room)
bei der sich herausstellt, dass John ein großartiger Mitbewohner ist und sehr effektiv putzt, aber Karl putzt das Zimmer nicht und nicht sehr viel.In diesem Fall ist es sinnvoll, sie als eigene Unterklasse mit dem definierten Verhalten zu definieren. Es gibt bessere Wege, um dasselbe zu erreichen (zum Beispiel eine
CleaningBehavior
Klasse), aber zumindest ist dies kein schrecklicher Verstoß gegen die OO-Prinzipien.quelle
In einigen Fällen wissen Sie, dass es nur eine bestimmte Anzahl von Instanzen gibt, und dann wäre es in Ordnung (obwohl meiner Meinung nach hässlich), diesen Ansatz zu verwenden. Es ist besser, eine Aufzählung zu verwenden, wenn die Sprache dies zulässt.
Java Beispiel:
quelle
Viele Leute haben bereits geantwortet. Ich dachte, ich würde meine persönliche Perspektive geben.
Ich habe einmal an einer App gearbeitet (und mache es immer noch), mit der Musik erstellt wird.
Die App hatte eine abstrakte
Scale
Klasse mit mehreren Unterklassen:CMajor
,DMinor
etc.Scale
sah so etwas wie so:Die Musikgeneratoren arbeiteten mit einer bestimmten
Scale
Instanz, um Musik zu generieren. Der Benutzer würde eine Skala aus einer Liste auswählen, aus der Musik erzeugt werden soll.Eines Tages kam mir eine coole Idee in den Sinn: Warum nicht dem Benutzer erlauben, seine eigenen Skalen zu erstellen? Der Benutzer wählt Notizen aus einer Liste aus, drückt eine Taste, und der Liste der verfügbaren Skalen wird eine neue Skala hinzugefügt.
Aber das konnte ich nicht. Das lag daran, dass alle Maßstäbe bereits zur Kompilierungszeit festgelegt sind - da sie als Klassen ausgedrückt werden. Dann traf es mich:
Es ist oft intuitiv, in Über- und Unterklassen zu denken. Fast alles kann durch dieses System ausgedrückt werden: Ober-
Person
und UnterklassenJohn
undMary
; Ober-Car
und UnterklassenVolvo
undMazda
; SuperklasseMissile
und UnterklassenSpeedRocked
,LandMine
undTrippleExplodingThingy
.Es ist sehr natürlich, auf diese Weise zu denken, besonders für die Person, die für OO relativ neu ist.
Wir sollten uns jedoch immer daran erinnern, dass Klassen Vorlagen sind und dass Objekte in diese Vorlagen eingegossen werden . Sie können beliebige Inhalte in die Vorlage einfügen und so unzählige Möglichkeiten schaffen.
Es ist nicht die Aufgabe der Unterklasse, die Vorlage auszufüllen. Es ist die Aufgabe des Objekts. Die Aufgabe der Unterklasse besteht darin, die eigentliche Funktionalität hinzuzufügen oder die Vorlage zu erweitern .
Aus diesem Grund hätte ich eine konkrete
Scale
Klasse mit einemNote[]
Feld erstellen und Objekte diese Vorlage ausfüllen lassen sollen . möglicherweise durch den Konstruktor oder so. Und irgendwann tat ich es auch.Denken Sie jedes Mal, wenn Sie eine Vorlage in einer Klasse entwerfen (z. B. ein leeres
Note[]
Element, das ausgefüllt werden muss, oder einString name
Feld, dem ein Wert zugewiesen werden muss), daran, dass es Aufgabe der Objekte dieser Klasse ist, die Vorlage auszufüllen ( oder möglicherweise diejenigen, die diese Objekte erstellen). Unterklassen sollen Funktionen hinzufügen, nicht Vorlagen ausfüllen.Sie könnten versucht sein, eine "Superklasse
Person
, UnterklassenJohn
undMary
" Art von System zu erstellen , wie Sie es getan haben, weil Sie die Formalität mögen, die Ihnen dies bringt.Auf diese Weise können Sie einfach sagen
Person p = new Mary()
, anstattPerson p = new Person("Mary", 57, Sex.FEMALE)
. Es macht die Dinge organisierter und strukturierter. Wie wir bereits sagten, ist es kein guter Ansatz, für jede Kombination von Daten eine neue Klasse zu erstellen, da dies den Code für nichts aufbläht und Sie in Bezug auf die Laufzeitfähigkeiten einschränkt.Hier ist eine Lösung: Verwenden Sie eine Basisfabrik, möglicherweise sogar eine statische. Wie so:
Auf diese Weise können Sie leicht die Voreinstellungen verwenden, die mit dem Programm geliefert werden
Person mary = PersonFactory.createMary()
, aber Sie behalten sich auch das Recht vor, neue Personen dynamisch zu entwerfen, zum Beispiel für den Fall, dass Sie dem Benutzer dies erlauben möchten . Z.B:Oder noch besser: Mach so etwas:
Ich wurde weggetragen. Ich denke, Sie haben die Idee.
Unterklassen sind nicht dazu gedacht, die von ihren Oberklassen festgelegten Vorlagen auszufüllen. Unterklassen sollen Funktionalität hinzufügen . Objekte sollen die Vorlagen ausfüllen, dafür sind sie da.
Sie sollten nicht für jede mögliche Kombination von Daten eine neue Klasse erstellen. (Genau wie ich nicht
Scale
für jede mögliche Kombination vonNote
s eine neue Unterklasse hätte erstellen sollen).Hier ist eine Richtlinie: Überlegen Sie beim Erstellen einer neuen Unterklasse, ob sie neue Funktionen hinzufügt, die in der Oberklasse nicht vorhanden sind. Wenn die Antwort auf diese Frage "Nein" lautet, versuchen Sie möglicherweise, die Vorlage der Superklasse auszufüllen. Erstellen Sie in diesem Fall einfach ein Objekt. (Und evtl. eine Factory mit 'Presets', um das Leben leichter zu machen).
Hoffentlich hilft das.
quelle
Dies ist eine Ausnahme von den 'sollte Instanzen sein' hier - obwohl etwas extrem.
Wenn Sie möchten, dass der Compiler Berechtigungen für den Zugriff auf Methoden erzwingt, basierend darauf, ob es sich um Karl oder John handelt (in diesem Beispiel), anstatt von der Methodenimplementierung zu überprüfen, für welche Instanz übergeben wurde, können Sie verschiedene Klassen verwenden. Durch die Durchsetzung verschiedene Klassen statt Instanziierung dann machen Sie Differenzierung Kontrollen zwischen ‚Karl‘ und ‚John‘ möglich bei der Kompilierung , anstatt Laufzeit und dies könnte nützlich sein - vor allem in einem Sicherheitskontext , wo der Compiler als Laufzeit traut mehr kann Code-Checks von (zum Beispiel) externen Bibliotheken.
quelle
Es wird als schlechte Praxis angesehen, Code dupliziert zu haben .
Dies liegt zumindest teilweise daran , dass Codezeilen und damit die Größe der Codebasis mit den Wartungskosten und Fehlern korrelieren .
Hier ist die relevante Logik des gesunden Menschenverstands für mich zu diesem bestimmten Thema:
1) Wenn ich dies ändern müsste, wie viele Orte müsste ich besuchen? Hier ist weniger besser.
2) Gibt es einen Grund, warum dies so gemacht wird? In Ihrem Beispiel könnte es keinen Grund dafür geben, aber in einigen Beispielen könnte es einen Grund dafür geben. Eine, die mir auf den ersten Blick einfällt, ist in einigen Frameworks wie Early Grails, in denen die Vererbung mit einigen Plugins für GORM nicht unbedingt gut funktioniert hat. Dünn gesät.
3) Ist es auf diese Weise sauberer und verständlicher? Wiederum ist dies nicht in Ihrem Beispiel der Fall - aber es gibt einige Vererbungsmuster, die sich eher dahingehend erstrecken, dass getrennte Klassen tatsächlich leichter zu pflegen sind (bestimmte Verwendungszwecke des Befehls oder Strategiemuster fallen mir ein).
quelle
Es gibt einen Anwendungsfall: das Bilden einer Referenz auf eine Funktion oder Methode als reguläres Objekt.
Sie können dies in Java GUI-Code viel sehen:
Der Compiler hilft Ihnen dabei, indem er eine neue anonyme Unterklasse erstellt.
quelle