Ein bekannter Mangel der traditionellen Klassenhierarchien ist, dass sie schlecht sind, wenn es darum geht, die reale Welt zu modellieren. Als Beispiel soll versucht werden, Tierarten mit Klassen darzustellen. Dabei gibt es tatsächlich mehrere Probleme, aber eines, für das ich nie eine Lösung gesehen habe, ist, wenn eine Unterklasse ein Verhalten oder eine Eigenschaft "verliert", die in einer Superklasse definiert wurde, wie ein Pinguin, der nicht fliegen kann (dorthin) sind wahrscheinlich bessere Beispiele, aber das ist das erste, das mir in den Sinn kommt).
Einerseits möchten Sie nicht für jede Eigenschaft und jedes Verhalten ein Flag definieren, das angibt, ob es überhaupt vorhanden ist, und es jedes Mal überprüfen, bevor Sie auf dieses Verhalten oder diese Eigenschaft zugreifen. Sie möchten nur sagen, dass Vögel in der Vogelklasse einfach und klar fliegen können. Aber dann wäre es schön, wenn man hinterher "Ausnahmen" definieren könnte, ohne überall ein paar schreckliche Hacks verwenden zu müssen. Dies ist häufig der Fall, wenn ein System längere Zeit produktiv war. Sie finden plötzlich eine "Ausnahme", die überhaupt nicht in das ursprüngliche Design passt, und Sie möchten nicht einen großen Teil Ihres Codes ändern, um ihn aufzunehmen.
Gibt es also einige Sprach- oder Entwurfsmuster, die dieses Problem sauber handhaben können, ohne dass größere Änderungen an der "Superklasse" und dem gesamten Code, der sie verwendet, erforderlich sind? Selbst wenn eine Lösung nur einen bestimmten Fall behandelt, können mehrere Lösungen zusammen eine vollständige Strategie bilden.
Nachdem ich mehr nachgedacht habe, wird mir klar, dass ich das Liskov-Substitutionsprinzip vergessen habe. Deshalb kannst du es nicht tun. Angenommen, Sie definieren "Merkmale / Schnittstellen" für alle wichtigen "Merkmalsgruppen", können Sie Merkmale in verschiedenen Zweigen der Hierarchie frei implementieren, so wie das Flugmerkmal von Vögeln und einigen besonderen Arten von Eichhörnchen und Fischen implementiert werden könnte.
Meine Frage könnte also lauten: "Wie kann ich ein Merkmal deaktivieren?" Wenn Ihre Superklasse eine Java-serialisierbare Klasse ist, müssen Sie auch eine sein, auch wenn Sie Ihren Status nicht serialisieren können, z. B. wenn Sie einen "Socket" enthalten haben.
Eine Möglichkeit, dies zu tun, besteht darin, alle Ihre Merkmale von Anfang an paarweise zu definieren: Fliegen und NotFlying (was eine UnsupportedOperationException auslösen würde, wenn nicht dagegen geprüft). Das Merkmal Nicht definiert keine neue Schnittstelle und kann einfach überprüft werden. Klingt nach einer "billigen" Lösung, insbesondere wenn sie von Anfang an verwendet wird.
quelle
function save_yourself_from_crashing_airplane(Bird b) { f.fly() }
das viel komplizierter wird. (wie Peter Török sagte, es verletzt LSP)" it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"
Halten Sie eine Factory-Methode zur Verhaltenskontrolle für hacky?NotSupportedException
ausPenguin.fly()
.class Penguin < Bird; undef fly; end;
. Ob Sie sollten, ist eine andere Frage.Antworten:
Wie bereits erwähnt, müsste man gegen LSP vorgehen.
Es kann jedoch argumentiert werden, dass eine Unterklasse lediglich eine willkürliche Erweiterung einer Superklasse ist. Es ist ein neues Objekt für sich und die einzige Beziehung zur Superklasse besteht darin, dass es als Fundament verwendet wird.
Dies kann logischen Sinn machen, eher dann sagen Penguin ist ein Vogel. Ihr Spruch Pinguin erbt eine Untergruppe von Verhaltensweisen von Bird.
Im Allgemeinen können Sie dies in dynamischen Sprachen leicht ausdrücken. Ein Beispiel mit JavaScript finden Sie unten:
In diesem speziellen Fall
Penguin
wird die ererbteBird.fly
Methode aktiv gespiegelt, indem einefly
Eigenschaft mit einem Wertundefined
in das Objekt geschrieben wird.Jetzt können Sie sagen, dass
Penguin
dies nicht mehr als normal behandelt werdenBird
kann. Aber wie gesagt, in der realen Welt kann es einfach nicht. Weil wirBird
als fliegende Einheit modellieren .Die Alternative besteht darin, nicht die allgemeine Annahme zu treffen, dass Birds fliegen können. Es wäre vernünftig, eine
Bird
Abstraktion zu haben, die es allen Vögeln erlaubt, ohne Misserfolg davon zu erben. Dies bedeutet, dass nur Annahmen getroffen werden, die für alle Unterklassen gelten können.Generell trifft die Idee von Mixin hier gut zu. Haben Sie eine sehr dünne Basisklasse und mischen Sie alle anderen Verhaltensweisen ein.
Beispiel:
Wenn Sie neugierig sind, habe ich eine Implementierung von
Object.make
Zusatz:
Sie können ein Merkmal nicht "deaktivieren". Sie reparieren einfach Ihre Vererbungshierarchie. Entweder können Sie Ihren Superklassenvertrag erfüllen oder Sie sollten nicht so tun, als wären Sie von diesem Typ.
Hier leuchtet die Objektkomposition.
Im Übrigen bedeutet Serializable nicht, dass alles serialisiert werden sollte, sondern nur, dass der Status, den Sie interessieren, serialisiert werden sollte.
Sie sollten keine "NotX" -Eigenschaft verwenden. Das ist einfach grauenhafter Code. Wenn eine Funktion ein fliegendes Objekt erwartet, sollte es abstürzen und brennen, wenn Sie ihm ein Mammut geben.
quelle
AFAIK Alle vererbungsbasierten Sprachen basieren auf dem Liskov-Substitutionsprinzip . Das Entfernen / Deaktivieren einer Basisklasseneigenschaft in einer Unterklasse würde eindeutig gegen LSP verstoßen, daher denke ich, dass eine solche Möglichkeit nirgendwo implementiert ist. Die reale Welt ist in der Tat chaotisch und kann durch mathematische Abstraktionen nicht präzise modelliert werden.
Einige Sprachen bieten Merkmale oder Mixins, um auf solche Probleme flexibler reagieren zu können.
quelle
Class
ist eine Unterklasse vonModule
, obwohlClass
IS-NOT-AModule
. Es ist aber trotzdem sinnvoll, eine Unterklasse zu sein, da ein Großteil des Codes wiederverwendet wird. OTOH,StringIO
IS-AIO
, aber die beiden haben keine Vererbungsbeziehung (abgesehen davon, dass beideObject
natürlich von erben ), da sie keinen gemeinsamen Code haben. Klassen dienen zur gemeinsamen Nutzung von Code, Typen zur Beschreibung von Protokollen.IO
undStringIO
haben das gleiche Protokoll, daher den gleichen Typ, aber ihre Klassen hängen nicht zusammen.Fly()
befindet sich im ersten Beispiel in: Erste Entwurfsmuster für das Strategiemuster , und dies ist eine gute Situation, warum Sie "Komposition gegenüber Vererbung bevorzugen" sollten . .Sie könnten Komposition und Vererbung mischen, indem Sie Supertypen von erstellen
FlyingBird
,FlightlessBird
die von einer Factory das richtige Verhalten injizieren, die relevanten Subtypen z. B.Penguin : FlightlessBird
automatisch abrufen und alles andere, was wirklich spezifisch ist, wird von der Factory selbstverständlich behandelt.quelle
Ist das eigentliche Problem, von dem Sie ausgehen, nicht
Bird
eineFly
Methode? Warum nicht:Das offensichtliche Problem ist nun die Mehrfachvererbung (
Duck
). Was Sie also wirklich brauchen, sind Schnittstellen:quelle
Erstens, JA, jede Sprache, die eine einfache dynamische Änderung von Objekten ermöglicht, würde dies ermöglichen. In Ruby können Sie beispielsweise eine Methode einfach entfernen.
Aber wie Péter Török sagte, würde es gegen LSP verstoßen .
In diesem Teil werde ich LSP vergessen und davon ausgehen, dass:
Du sagtest :
Es sieht aus wie das, was Sie wollen , Python ist „ um Vergebung bitten , anstatt Erlaubnis “
Lassen Sie Ihren Pinguin einfach eine Ausnahme auslösen oder erben Sie eine NonFlyingBird-Klasse, die eine Ausnahme auslöst (Pseudocode):
Übrigens, was auch immer Sie wählen: Auslösen einer Ausnahme oder Entfernen einer Methode, am Ende der folgende Code (vorausgesetzt, Ihre Sprache unterstützt das Entfernen von Methoden):
wird eine Laufzeitausnahme auslösen.
quelle
Wie jemand oben in den Kommentaren ausgeführt hat, sind Pinguine Vögel, Pinguine fliegen nicht, ergo können nicht alle Vögel fliegen.
Bird.fly () sollte also nicht existieren oder nicht funktionieren dürfen. Ich bevorzuge das erstere.
FlyingBird extended zu haben, Bird eine .fly () Methode zu haben, wäre natürlich richtig.
quelle
Das eigentliche Problem mit dem fly () - Beispiel besteht darin, dass die Eingabe und die Ausgabe der Operation nicht richtig definiert sind. Was braucht ein Vogel, um zu fliegen? Und was passiert nach dem erfolgreichen Fliegen? Die Parametertypen und Rückgabetypen für die fly () -Funktion müssen diese Informationen enthalten. Ansonsten hängt Ihr Design von zufälligen Nebenwirkungen ab und alles kann passieren. Der Teil alles ist, was das ganze Problem verursacht, die Schnittstelle ist nicht richtig definiert und alle Arten der Implementierung sind erlaubt.
Also stattdessen:
Sie sollten so etwas haben:
Jetzt definiert es explizit die Grenzen der Funktionalität - Ihr Flugverhalten hat nur einen Schwimmer zu entscheiden - die Entfernung zum Boden, wenn die Position angegeben wird. Jetzt löst sich das ganze Problem von selbst. Ein Vogel, der nicht fliegen kann, gibt nur 0.0 von dieser Funktion zurück, er verlässt nie den Boden. Das ist das richtige Verhalten, und sobald Sie sich für einen Float entschieden haben, wissen Sie, dass Sie die Schnittstelle vollständig implementiert haben.
Es kann schwierig sein, echtes Verhalten in die Typen zu codieren, aber nur so können Sie Ihre Schnittstellen richtig angeben.
Edit: Ich möchte einen Aspekt klarstellen. Diese float-> float-Version der fly () -Funktion ist auch deshalb wichtig, weil sie einen Pfad definiert. Diese Version bedeutet, dass der eine Vogel sich während des Fluges nicht magisch duplizieren kann. Deshalb ist der Parameter single float - es ist die Position auf dem Weg, den der Vogel nimmt. Wenn Sie komplexere Pfade wünschen, verwenden Sie Point2d posinpath (float x). welches das gleiche x wie die fly () Funktion benutzt.
quelle
Technisch kann man das in so ziemlich jeder dynamischen Sprache (JavaScript, Ruby, Lua usw.) machen, aber es ist fast immer eine wirklich schlechte Idee. Das Entfernen von Methoden aus einer Klasse ist ein Wartungs-Albtraum, ähnlich der Verwendung globaler Variablen (dh Sie können in einem Modul nicht feststellen, dass der globale Status an keiner anderen Stelle geändert wurde).
Gute Muster für das von Ihnen beschriebene Problem sind Dekorateur oder Strategie, die eine Komponentenarchitektur entwerfen. Anstatt unnötige Verhaltensweisen aus Unterklassen zu entfernen, erstellen Sie Objekte, indem Sie die erforderlichen Verhaltensweisen hinzufügen. Um die meisten Vögel zu bauen, fügen Sie die fliegende Komponente hinzu, aber fügen Sie diese Komponente nicht Ihren Pinguinen hinzu.
quelle
Peter hat das Liskov-Substitutionsprinzip erwähnt, aber ich denke, das muss erklärt werden.
Wenn also ein Vogel (Objekt x vom Typ T) fliegen kann (q (x)), dann kann per Definition ein Pinguin (Objekt y vom Typ S) fliegen (q (y)). Das ist aber eindeutig nicht der Fall. Es gibt auch andere Kreaturen, die fliegen können, aber nicht vom Typ Vogel sind.
Wie Sie damit umgehen, hängt von der Sprache ab. Wenn eine Sprache Mehrfachvererbung unterstützt, sollten Sie eine abstrakte Klasse für fliegende Kreaturen verwenden. Wenn eine Sprache Schnittstellen bevorzugt, ist dies die Lösung (und die Implementierung von fly sollte gekapselt und nicht vererbt werden). oder, wenn eine Sprache Duck Typing unterstützt (kein Wortspiel beabsichtigt), können Sie einfach eine fly-Methode für die Klassen implementieren, die dies können, und sie aufrufen, wenn sie vorhanden sind.
Jede Eigenschaft einer Oberklasse sollte jedoch für alle Unterklassen gelten.
[Als Antwort auf die Bearbeitung]
Es ist nicht besser, ein "Merkmal" von CanFly auf Bird anzuwenden. Dem Aufruf von Code wird weiterhin nahegelegt, dass alle Vögel fliegen können.
Ein Merkmal in den von Ihnen definierten Begriffen ist genau das, was Liskov meinte, als sie "Eigentum" sagte.
quelle
Lassen Sie mich zunächst (wie alle anderen auch) das Liskov-Substitutionsprinzip erwähnen, das erklärt, warum Sie dies nicht tun sollten. Die Frage, was Sie tun sollten, ist jedoch eine Frage des Designs. In einigen Fällen ist es möglicherweise nicht wichtig, dass Pinguin nicht fliegen kann. Möglicherweise kann Penguin InsquateWingsException auslösen, wenn Sie zum Fliegen aufgefordert werden, sofern in der Dokumentation von Bird :: fly () klar ist, dass es das für Vögel auslösen kann, die nicht fliegen können. Habe einen Test, um zu sehen, ob es wirklich fliegen kann, obwohl das die Schnittstelle aufbläst.
Die Alternative ist die Umstrukturierung Ihrer Klassen. Lassen Sie uns die Klasse "FlyingCreature" erstellen (oder besser eine Benutzeroberfläche, wenn Sie sich mit der Sprache befassen, die dies zulässt). "Bird" erbt nicht von FlyingCreature, aber Sie können "FlyingBird" erstellen, das funktioniert. Lark, Vulture und Eagle erben allesamt von FlyingBird. Pinguin nicht. Es erbt nur von Bird.
Es ist etwas komplizierter als die naive Struktur, hat aber den Vorteil, genau zu sein. Sie werden feststellen, dass alle erwarteten Klassen vorhanden sind (Bird) und der Benutzer normalerweise die "erfundenen" (FlyingCreature) ignorieren kann, wenn es nicht wichtig ist, ob Ihre Kreatur fliegen kann oder nicht.
quelle
Die typische Art, mit einer solchen Situation umzugehen, besteht darin, so etwas wie
UnsupportedOperationException
(Java) bzw.NotImplementedException
(C #).quelle
Viele gute Antworten mit vielen Kommentaren, aber sie stimmen nicht alle überein, und ich kann nur einen auswählen, daher fasse ich hier alle Ansichten zusammen, denen ich zustimme.
0) Nehmen Sie nicht an, dass Sie "statisch tippen" (das habe ich getan, als ich gefragt habe, weil ich fast ausschließlich Java mache). Grundsätzlich hängt das Problem stark von der Art der verwendeten Sprache ab.
1) Man sollte die Typ-Hierarchie von der Code-Wiederverwendungs-Hierarchie im Design und im Kopf trennen, auch wenn sie sich größtenteils überschneiden. Verwenden Sie im Allgemeinen Klassen zur Wiederverwendung und Interfaces für Typen.
2) Der Grund, warum Bird IS-A Fly normalerweise so ist, dass die meisten Vögel fliegen können. Aus der Sicht der Code-Wiederverwendung ist es also praktisch, zu sagen, dass Bird IS-A Fly tatsächlich falsch ist, da es mindestens eine Ausnahme gibt (Pinguin).
3) Sowohl in statischen als auch in dynamischen Sprachen können Sie einfach eine Ausnahme auslösen. Dies sollte jedoch nur verwendet werden, wenn dies ausdrücklich im "Vertrag" der Klasse / Schnittstelle deklariert ist, die die Funktionalität deklariert, andernfalls handelt es sich um eine "Vertragsverletzung". Das bedeutet auch, dass Sie jetzt darauf vorbereitet sein müssen, die Ausnahme überall abzufangen, damit Sie mehr Code auf der Aufrufseite schreiben und es ist hässlicher Code.
4) In einigen dynamischen Sprachen ist es tatsächlich möglich, die Funktionalität einer Superklasse zu "entfernen / verbergen". Wenn Sie in dieser Sprache nach "IS-A" suchen, um festzustellen, ob die Funktionalität vorhanden ist, ist dies eine angemessene und vernünftige Lösung. Wenn auf der anderen Seite die "IS-A" -Operation noch etwas anderes ist, das besagt, dass Ihr Objekt die jetzt fehlende Funktionalität implementieren sollte, dann geht Ihr aufrufender Code davon aus, dass die Funktionalität vorhanden ist, und ruft sie auf und stürzt ab es kommt einer Ausnahme gleich.
5) Die bessere Alternative besteht darin, das Fly-Merkmal vom Bird-Merkmal zu trennen. Ein fliegender Vogel muss also sowohl Bird als auch Fly / Flying explizit erweitern / implementieren. Dies ist wahrscheinlich das sauberste Design, da Sie nichts "entfernen" müssen. Der einzige Nachteil ist nun, dass fast jeder Vogel sowohl Bird als auch Fly implementieren muss, sodass Sie mehr Code schreiben. Der Weg dahin besteht darin, eine Zwischenklasse FlyingBird zu haben, die sowohl Bird als auch Fly implementiert und den allgemeinen Fall darstellt. Diese Umgehung kann jedoch ohne Mehrfachvererbung von begrenztem Nutzen sein.
6) Eine andere Alternative, die keine Mehrfachvererbung erfordert, ist die Verwendung von Komposition anstelle von Vererbungen. Jeder Aspekt eines Tieres wird von einer unabhängigen Klasse modelliert, und ein konkreter Vogel ist eine Komposition aus Vogel und möglicherweise Fliegen oder Schwimmen die Flugfunktion, wenn Sie eine Referenz eines konkreten Vogels haben. Außerdem funktionieren die natürlichen Sprachen "object IS-A Fly" und "object AS-A (cast) Fly" nicht mehr. Sie müssen sich also eine eigene Syntax ausdenken (einige dynamische Sprachen könnten sich anders verhalten). Dies könnte Ihren Code umständlicher machen.
7) Definieren Sie Ihr Flugmerkmal so, dass es einen eindeutigen Ausweg für etwas bietet, das nicht fliegen kann. Fly.getNumberOfWings () könnte 0 zurückgeben. Wenn Fly.fly (direction, currentPotinion) die neue Position nach dem Flug zurückgeben sollte, könnte Penguin.fly () einfach die aktuelle Position zurückgeben, ohne sie zu ändern. Möglicherweise haben Sie Code, der technisch funktioniert, aber es gibt einige Einschränkungen. Erstens weist ein Code möglicherweise kein offensichtliches Verhalten "Nichts tun" auf. Auch wenn jemand x.fly () aufruft, würde er erwarten, dass es etwas tut , auch wenn der Kommentar sagt, dass fly () nichts tun darf . Schließlich würde Pinguin IS-A Flying immer noch true zurückgeben, was für den Programmierer verwirrend werden könnte.
8) Gehen Sie wie 5) vor, verwenden Sie jedoch die Komposition, um Fälle zu umgehen, die eine Mehrfachvererbung erfordern würden. Dies ist die Option, die ich für eine statische Sprache vorziehen würde, da 6) umständlicher erscheint (und wahrscheinlich mehr Speicherplatz benötigt, weil wir mehr Objekte haben). Eine dynamische Sprache könnte 6) weniger umständlich machen, aber ich bezweifle, dass sie weniger umständlich wird als 5).
quelle
Definieren Sie ein Standardverhalten (markieren Sie es als virtuell) in der Basisklasse und überschreiben Sie es als erforderlich. So kann jeder Vogel "fliegen".
Sogar Pinguine fliegen, es gleitet über das Eis in null Höhe!
Das Flugverhalten kann bei Bedarf überschrieben werden.
Eine andere Möglichkeit ist ein Fly Interface. Nicht alle Vögel werden diese Schnittstelle implementieren.
Eigenschaften können nicht entfernt werden, deshalb ist es wichtig zu wissen, welche Eigenschaften bei allen Vögeln gleich sind. Ich denke, dass es eher ein Entwurfsproblem ist, sicherzustellen, dass gemeinsame Eigenschaften auf der Basisebene implementiert werden.
quelle
Ich denke, das Muster, das Sie suchen, ist ein guter alter Polymorphismus. In einigen Sprachen ist es zwar möglich, eine Schnittstelle aus einer Klasse zu entfernen, aus den von Péter Török angegebenen Gründen ist dies jedoch wahrscheinlich keine gute Idee. In jeder OO-Sprache können Sie jedoch eine Methode außer Kraft setzen, um ihr Verhalten zu ändern, und dazu gehört auch, nichts zu tun. Um Ihr Beispiel auszuleihen, können Sie eine Penguin :: fly () -Methode bereitstellen, die eine der folgenden Aktionen ausführt:
Das Hinzufügen und Entfernen von Eigenschaften kann etwas einfacher sein, wenn Sie im Voraus planen. Sie können Eigenschaften in einer Karte / einem Wörterbuch / einem assoziativen Array speichern, anstatt Instanzvariablen zu verwenden. Sie können das Factory-Muster verwenden, um Standardinstanzen solcher Strukturen zu erstellen, sodass ein Bird, der aus der BirdFactory stammt, immer mit denselben Eigenschaften beginnt. Das Key Value Coding von Objective-C ist ein gutes Beispiel dafür.
Hinweis: Aus den nachstehenden Kommentaren geht hervor, dass das Überschreiben zum Entfernen eines Verhaltens zwar funktionieren kann, aber nicht immer die beste Lösung ist. Wenn Sie feststellen, dass Sie dies auf eine signifikante Weise tun müssen, sollten Sie dies als starkes Signal dafür ansehen, dass Ihr Vererbungsdiagramm fehlerhaft ist. Es ist nicht immer möglich, die Klassen umzugestalten, von denen Sie geerbt haben, aber wenn dies der Fall ist, ist dies oft die bessere Lösung.
Wenn Sie Ihr Pinguin-Beispiel verwenden, besteht eine Möglichkeit zur Umgestaltung darin, die Flugfähigkeit von der Vogelklasse zu trennen. Da nicht alle Vögel fliegen können, war eine fly () -Methode in Bird unangemessen und führte direkt zu dem Problem, nach dem Sie fragen. Verschieben Sie die fly () -Methode (und möglicherweise takeoff () und land ()) in eine Aviator-Klasse oder -Schnittstelle (je nach Sprache). Auf diese Weise können Sie eine FlyingBird-Klasse erstellen, die sowohl von Bird als auch von Aviator erbt (oder von Bird erbt und Aviator implementiert). Pinguin kann weiterhin direkt von Bird, aber nicht von Aviator erben, wodurch das Problem vermieden wird. Eine solche Anordnung könnte es auch einfach machen, Klassen für andere Flugobjekte zu erstellen: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect und so weiter.
quelle