Hintergrund:
Als Java-Programmierer erbe ich weitgehend (eher: implementiere) von Schnittstellen und entwerfe manchmal abstrakte Basisklassen. Ich hatte jedoch nie wirklich das Bedürfnis, eine konkrete (nicht abstrakte) Klasse zu unterordnen (in den Fällen, in denen ich dies tat, stellte sich später heraus, dass eine andere Lösung wie die Delegation besser gewesen wäre).
Jetzt habe ich das Gefühl, dass es fast keine Situation gibt, in der das Erben von einer konkreten Klasse angemessen ist. Zum einen scheint das Liskov-Substitutionsprinzip (LSP) für nicht triviale Klassen fast unmöglich zu sein; auch viele andere Fragen hier scheinen eine ähnliche Meinung zu wiederholen.
Also meine Frage:
In welcher Situation (falls vorhanden) ist es tatsächlich sinnvoll, von einer konkreten Klasse zu erben? Können Sie ein konkretes, reales Beispiel für eine Klasse geben, die von einer anderen konkreten Klasse erbt, bei der Sie der Meinung sind, dass dies angesichts der Einschränkungen das beste Design ist? Ich bin besonders an Beispielen interessiert, die den LSP erfüllen (oder an Beispielen, bei denen das Erfüllen des LSP unwichtig erscheint).
Ich habe hauptsächlich einen Java-Hintergrund, interessiere mich aber für Beispiele aus jeder Sprache.
Antworten:
Sie haben häufig eine Skelettimplementierung für eine Schnittstelle
I
. Wenn Sie Erweiterbarkeit ohne abstrakte Methoden anbieten können (z. B. über Hooks), ist es vorzuziehen, eine nicht abstrakte Skelettklasse zu haben, da Sie diese instanziieren können.Ein Beispiel wäre eine Weiterleitungs-Wrapper-Klasse , um an ein anderes Objekt einer konkreten Klasse weiterleiten zu können, das
C
implementiertI
, z. B. Dekoration oder einfache Wiederverwendung von Code ermöglicht,C
ohne von erben zu müssenC
. Sie finden ein solches Beispiel in Effective Java Item 16, bei dem die Komposition der Vererbung vorgezogen wird. (Ich möchte es hier wegen Urheberrechten nicht veröffentlichen, aber es leitet wirklich einfach alle MethodenaufrufeI
an die umschlossene Implementierung weiter).quelle
MouseAdapter
usw.) von Swing, die NOOP-Implementierungen von Ereignis-Listener-Schnittstellen mit vielen Methoden bereitstellen, wäre das Schreiben von GUI-Komponenten noch schmerzhafter.abstract
, sodass dieses Beispiel meine Frage nicht wirklich beantwortet.foo
müsste entweder abstrakt oder versiegelt sein, aber eine Deklaration einer konkreten vererbbaren Klasse würde sowohl eine abstrakte Klassefoo
als auch eine versiegelte Klasse deklarieren,__impl_foo
die geerbt hat,foo
aber nichts anderes enthielt. Nehmen wir weiter an, dasnew foo()
würde konvertiert werdennew __impl_foo()
. Würden solche Substitutionen eine andere Semantik als den Reflexionsnamen des konkreten Typs ändern?Ich denke, das Folgende ist ein gutes Beispiel, wenn es angemessen sein kann:
public class LinkedHashMap<K,V> extends HashMap<K,V>
Ein weiteres gutes Beispiel ist die Vererbung von Ausnahmen:
public class IllegalFormatPrecisionException extends IllegalFormatException public class IllegalFormatException extends IllegalArgumentException public class IllegalArgumentException extends RuntimeException public class RuntimeException extends Exception public class Exception extends Throwable
quelle
Ein sehr häufiger Fall, den ich mir vorstellen kann, besteht darin, von grundlegenden Steuerelementen der Benutzeroberfläche wie Formularen, Textfeldern, Kombinationsfeldern usw. abzuleiten. Sie sind vollständig, konkret und können gut für sich allein stehen. Die meisten von ihnen sind jedoch auch sehr einfach, und manchmal ist ihr Standardverhalten nicht das, was Sie wollen. Zum Beispiel würde praktisch niemand eine Instanz eines unverfälschten Formulars verwenden, es sei denn, sie würden möglicherweise eine vollständig dynamische UI-Ebene erstellen.
Zum Beispiel habe ich in einer Software, die ich geschrieben habe und die kürzlich die relative Reife erreicht hat (was bedeutet, dass mir die Zeit ausgegangen ist, um mich hauptsächlich auf die Entwicklung zu konzentrieren :)), festgestellt, dass ich ComboBoxes die Funktion "Lazy Loading" hinzufügen muss, damit dies nicht der Fall ist. Es dauert 50 Jahre (in Computerjahren), bis das erste Fenster geladen ist. Ich brauchte auch die Fähigkeit, die verfügbaren Optionen in einer ComboBox automatisch zu filtern, basierend auf dem, was in einer anderen angezeigt wurde, und schließlich brauchte ich eine Möglichkeit, den Wert einer ComboBox in einem anderen bearbeitbaren Steuerelement zu "spiegeln" und eine Änderung in einem Steuerelement für das zu bewirken auch andere. Also habe ich die grundlegende ComboBox um diese zusätzlichen Funktionen erweitert und zwei neue Typen erstellt: LazyComboBox und dann MirroringComboBox. Beide basieren auf der vollständig zu wartenden, konkreten ComboBox-Steuerung. nur einige Verhaltensweisen überschreiben und ein paar andere hinzufügen. Sie sind nicht sehr locker gekoppelt und daher nicht zu FEST, aber die hinzugefügte Funktionalität ist allgemein genug, dass ich, wenn ich müsste, jede dieser Klassen von Grund auf neu schreiben könnte, um den gleichen Job zu erledigen, möglicherweise besser.
quelle
Im Allgemeinen leite ich konkrete Klassen nur dann ab, wenn sie sich im Framework befinden. Ableiten von
Applet
oderJApplet
als triviales Beispiel.quelle
JPanel
oderJComponent
wenn Sie eine benutzerdefinierte Zeichnung für eine Swing-Komponente erstellen möchten.Dies ist ein Beispiel für eine aktuelle Implementierung, die ich durchführe.
In der OAuth 2-Umgebung ändert sich die Spezifikation ständig , da sich die Dokumentation noch im Entwurfsstadium befindet (zum Zeitpunkt des Schreibens befinden wir uns in Version 21).
Daher musste ich meine konkrete
AccessToken
Klasse erweitern, um die verschiedenen Zugriffstoken aufzunehmen.In früheren Entwürfen war kein
token_type
Feld festgelegt, daher lautet das tatsächliche Zugriffstoken wie folgt:public class AccessToken extends OAuthToken { /** * */ private static final long serialVersionUID = -4419729971477912556L; private String accessToken; private String refreshToken; private Map<String, String> additionalParameters; //Getters and setters are here }
Mit den zurückgegebenen Zugriffstoken habe
token_type
ich jetztpublic class TokenTypedAccessToken extends AccessToken { private String tokenType; //Getter and setter are here... }
Ich kann also beide zurückgeben und der Endbenutzer ist nicht klüger. :-)
Zusammenfassend: Wenn Sie eine angepasste Klasse möchten, die dieselbe Funktionalität wie Ihre konkrete Klasse aufweist, ohne die Struktur der konkreten Klasse zu ändern, empfehle ich, die konkrete Klasse zu erweitern.
quelle
Wie viele Frameworks nutzt ASP.NET die Vererbung stark, um das Verhalten zwischen Klassen zu teilen. Beispielsweise hat HtmlInputPassword diese Vererbungshierarchie:
System.Object System.Web.UI.Control System.Web.UI.HtmlControls.HtmlControl // abstract System.Web.UI.HtmlControls.HtmlInputControl // abstract System.Web.UI.HtmlControls.HtmlInputText System.Web.UI.HtmlControls.HtmlInputPassword
Darin sind Beispiele konkreter Klassen zu sehen, aus denen abgeleitet wird.
Wenn Sie ein Framework erstellen - und Sie sind sicher, dass Sie dies tun möchten -, möchten Sie möglicherweise eine schöne große Vererbungshierarchie.
quelle
Ein anderer Anwendungsfall wäre das Überschreiben des Standardverhaltens:
Nehmen wir an, es gibt eine Klasse, die den Standard-Jaxb-Parser zum Parsen verwendet
public class Util{ public void mainOperaiton(){..} protected MyDataStructure parse(){ //standard Jaxb code } }
Angenommen, ich möchte eine andere Bindung (Say XMLBean) für die Analyseoperation verwenden.
public class MyUtil extends Util{ protected MyDataStructure parse(){ //XmlBean code code } }
Jetzt kann ich die neue Bindung mit Code-Wiederverwendung der Superklasse verwenden.
quelle
Das Dekorationsmuster , eine praktische Möglichkeit, einer Klasse zusätzliches Verhalten hinzuzufügen, ohne es zu allgemein zu gestalten, nutzt die Vererbung konkreter Klassen in hohem Maße. Es wurde hier bereits erwähnt, jedoch unter dem wissenschaftlichen Namen "Forwarding Wrapper Class".
quelle
Viele Antworten, aber ich dachte, ich würde meine eigenen $ 0,02 hinzufügen.
Ich überschreibe Concreate-Klassen selten, aber unter bestimmten Umständen. Mindestens 1 wurde bereits erwähnt, wenn Framework-Klassen erweitert werden sollen. Zwei weitere kommen mit einigen Beispielen in den Sinn:
1) Wenn ich das Verhalten einer konkreten Klasse optimieren möchte. Manchmal möchte ich die Funktionsweise der konkreten Klasse ändern oder wissen, wann eine bestimmte Methode aufgerufen wird, damit ich etwas auslösen kann. Häufig definieren konkrete Klassen eine Hook-Methode, deren einzige Verwendung darin besteht, dass Unterklassen die Methode überschreiben.
Beispiel: Wir haben überschrieben,
MBeanExporter
weil wir in der Lage sein müssen, die Registrierung einer JMX-Bean aufzuheben :public class MBeanRegistrationSupport { // the concrete class has a hook defined protected void onRegister(ObjectName objectName) { }
Unsere Klasse:
public class UnregisterableMBeanExporter extends MBeanExporter { @Override protected void onUnregister(ObjectName name) { // always a good idea super.onRegister(name); objectMap.remove(name); }
Hier ist ein weiteres gutes Beispiel.
LinkedHashMap
wurde entwickelt, um seineremoveEldestEntry
Methode überschreiben zu lassen.private static class LimitedLinkedHashMap<K, V> extends LinkedHashMap<K, V> { @Override protected boolean removeEldestEntry(Entry<K, V> eldest) { return size() > 1000; }
2) Wenn eine Klasse eine erhebliche Überlappung mit der konkreten Klasse aufweist, mit Ausnahme einiger Änderungen an der Funktionalität.
Beispiel: Mein ORMLite- Projekt verarbeitet persistierende
Long
Objektfelder undlong
primitive Felder. Beide haben fast die gleiche Definition.LongObjectType
bietet alle Methoden, die beschreiben, wie die Datenbank mitlong
Feldern umgeht.public class LongObjectType { // a whole bunch of methods
während
LongType
überschreibtLongObjectType
und nur eine einzige Methode optimiert, um zu sagen, dass Primitive behandelt werden.public class LongType extends LongObjectType { ... @Override public boolean isPrimitive() { return true; } }
Hoffe das hilft.
quelle
BaseDataType
, die ich nicht beschrieben habeisPrimitive()
und die falsch ist. DieLongObjectType
Unterklasse beschreibt dann, wie Longs beibehalten undLongType
überschrieben werdenisPrimitive()
, um true zurückzugeben. EinBaseLongObjectType
Gerechtes zu haben, scheint mir fremd zu sein.Das Erben einer konkreten Klasse ist nur möglich, wenn Sie die Funktionalität der Seitenbibliothek erweitern möchten.
Zum Beispiel der realen Nutzung können Sie sich die Hierarchie von DataInputStream ansehen, die die DataInput-Schnittstelle für FilterInputStream implementiert.
quelle
Dies ist eine "fast". Versuchen Sie, eine zu schreibenAppletohne zu verlängern
Applet
oderJApplet
.Hier ist ein zB aus dem Applet info. Seite .
/* <!-- Defines the applet element used by the appletviewer. --> <applet code='HelloWorld' width='200' height='100'></applet> */ import javax.swing.*; /** An 'Hello World' Swing based applet. To compile and launch: prompt> javac HelloWorld.java prompt> appletviewer HelloWorld.java */ public class HelloWorld extends JApplet { public void init() { // Swing operations need to be performed on the EDT. // The Runnable/invokeLater() ensures that happens. Runnable r = new Runnable() { public void run() { // the crux of this simple applet getContentPane().add( new JLabel("Hello World!") ); } }; SwingUtilities.invokeLater(r); } }
quelle
Ein weiteres gutes Beispiel wären Datenspeichertypen. Um ein genaues Beispiel zu nennen: Ein rot-schwarzer Baum ist ein spezifischerer Binärbaum, aber das Abrufen von Daten und anderen Informationen wie der Größe kann identisch behandelt werden. Natürlich sollte eine gute Bibliothek dies bereits implementiert haben, aber manchmal müssen Sie bestimmte Datentypen für Ihr Problem hinzufügen.
Ich entwickle gerade eine Anwendung, die Matrizen für die Benutzer berechnet. Der Benutzer kann Einstellungen vornehmen, um die Berechnung zu beeinflussen. Es gibt verschiedene Arten von Matrizen, die berechnet werden können, aber es gibt eine deutliche Ähnlichkeit, insbesondere in Bezug auf die Konfigurierbarkeit: Matrix A kann alle Einstellungen von Matrix B verwenden, verfügt jedoch über zusätzliche Parameter, die verwendet werden können. In diesem Fall habe ich von ConfigObjectB für mein ConfigObjectA geerbt und es funktioniert ziemlich gut.
quelle
Im Allgemeinen ist es besser, von einer abstrakten Klasse zu erben als von einer konkreten Klasse. Eine konkrete Klasse muss eine Definition für ihre Datendarstellung bereitstellen, und einige Unterklassen benötigen eine andere Darstellung. Da eine abstrakte Klasse keine Datendarstellung bereitstellen muss, können zukünftige Unterklassen jede Darstellung verwenden, ohne befürchten zu müssen, mit der von ihnen geerbten in Konflikt zu geraten. Selbst ich habe nie eine Situation gefunden, in der ich der Meinung war, dass konkrete Vererbung notwendig ist. Es kann jedoch Situationen für eine konkrete Vererbung geben, insbesondere wenn Sie Abwärtskompatibilität für Ihre Software bereitstellen. In diesem Fall haben Sie sich vielleicht auf a spezialisiert
class A
, möchten aber, dass es konkret ist, da Ihre ältere Anwendung es möglicherweise verwendet.quelle
Ihre Bedenken spiegeln sich aus den von Ihnen genannten Gründen auch im klassischen Prinzip " Komposition gegenüber Vererbung bevorzugen " wider. Ich kann mich nicht erinnern, wann ich das letzte Mal von einer konkreten Klasse geerbt habe. Jeder allgemeine Code, der von untergeordneten Klassen wiederverwendet werden muss, muss fast immer abstrakte Schnittstellen für diese Klassen deklarieren. In dieser Reihenfolge versuche ich folgende Strategien zu bevorzugen:
Das Erben von einer konkreten Klasse ist wirklich nie eine gute Idee.
[BEARBEITEN] Ich werde diese Aussage qualifizieren, indem ich sage, dass ich keinen guten Anwendungsfall dafür sehe, wenn Sie die Kontrolle über die Architektur haben. Was tun, wenn Sie eine API verwenden, die dies erwartet? Aber ich verstehe die Designentscheidungen dieser APIs nicht. Die aufrufende Klasse sollte immer in der Lage sein, eine Abstraktion gemäß dem Prinzip der Abhängigkeitsinversion zu deklarieren und zu verwenden . Wenn eine untergeordnete Klasse über zusätzliche Schnittstellen verfügt, die verwendet werden müssen, müssen Sie entweder gegen DIP verstoßen oder ein hässliches Casting durchführen, um an diese Schnittstellen zu gelangen.
quelle
aus dem gdata-Projekt :
com.google.gdata.client.Service dient als Basisklasse, die für bestimmte Arten von GData-Diensten angepasst werden kann.
Service Javadoc:
Die Serviceklasse repräsentiert eine Clientverbindung zu einem GData-Service. Es kapselt alle Interaktionen auf Protokollebene mit dem GData-Server und fungiert als Hilfsklasse für übergeordnete Entitäten (Feeds, Einträge usw.), die Vorgänge auf dem Server aufrufen und deren Ergebnisse verarbeiten.
Diese Klasse bietet die allgemeinen Funktionen auf Basisebene, die für den Zugriff auf einen GData-Dienst erforderlich sind. Es dient auch als Basisklasse, die für bestimmte Arten von GData-Diensten angepasst werden kann. Beispiele für unterstützte Anpassungen sind:
Authentifizierung - Implementierung eines benutzerdefinierten Authentifizierungsmechanismus für Dienste, die eine Authentifizierung erfordern und etwas anderes als die HTTP-Basis- oder Digest-Authentifizierung verwenden.
Erweiterungen - Definieren Sie erwartete Erweiterungen für Feed, Eintrag und andere Typen, die dem Service zugeordnet sind.
Formate - Definieren Sie zusätzliche benutzerdefinierte Ressourcendarstellungen, die von den Service- und clientseitigen Parsern und Generatoren verwendet oder erstellt werden können, um diese zu verarbeiten.
quelle
Ich finde die Java-Sammlungsklassen als sehr gutes Beispiel. Sie haben also eine AbstractCollection mit untergeordneten Elementen wie AbstractList, AbstractSet, AbstractQueue ... Ich denke, diese Hierarchie wurde gut entworfen. Um sicherzustellen, dass es keine Explosion gibt, gibt es die Collections-Klasse mit all ihren inneren statischen Klassen.
quelle
Dies tun Sie beispielsweise in GUI-Bibliotheken. Es macht wenig Sinn, von einer bloßen Komponente zu erben und an ein Panel zu delegieren. Es ist wahrscheinlich viel einfacher, direkt vom Panel zu erben.
quelle
Nur ein allgemeiner Gedanke. In abstrakten Klassen fehlt etwas. Es ist sinnvoll, wenn dies, was fehlt, in jeder abgeleiteten Klasse unterschiedlich ist. Möglicherweise haben Sie jedoch einen Fall, in dem Sie eine Klasse nicht ändern, sondern nur etwas hinzufügen möchten. Um Doppelarbeit zu vermeiden, würden Sie erben. Und wenn Sie beide Klassen benötigen, wäre dies eine Vererbung von einer konkreten Klasse.
Meine Antwort wäre also: In allen Fällen, in denen Sie wirklich nur etwas hinzufügen möchten. Vielleicht passiert das einfach nicht sehr oft.
quelle