Gibt es eine Sprache oder ein Entwurfsmuster, das das * Entfernen * von Objektverhalten oder -eigenschaften in einer Klassenhierarchie ermöglicht?


28

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.


3
"Ohne überall einige schreckliche Hacks verwenden zu müssen": Behinderung eines Verhaltens IST ein schrecklicher Hack: Es würde bedeuten, dass function save_yourself_from_crashing_airplane(Bird b) { f.fly() }das viel komplizierter wird. (wie Peter Török sagte, es verletzt LSP)
keppla

Eine Kombination aus Strategiemuster und Vererbung könnte es Ihnen ermöglichen, das vererbte Verhalten für bestimmte Supertypen zu "überlagern". Wenn Sie sagen: " 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?
StuperUser

1
Man könnte natürlich werfen nur NotSupportedExceptionaus Penguin.fly().
Felix Dombek

In Bezug auf Sprachen können Sie die Implementierung einer Methode in einer untergeordneten Klasse auf jeden Fall aufheben. Zum Beispiel in Ruby: class Penguin < Bird; undef fly; end;. Ob Sie sollten, ist eine andere Frage.
Nathan Long

Dies würde das Liskov-Prinzip brechen und wohl den ganzen Sinn von OOP ausmachen.
Deadalnix

Antworten:


17

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:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

In diesem speziellen Fall Penguinwird die ererbte Bird.flyMethode aktiv gespiegelt, indem eine flyEigenschaft mit einem Wert undefinedin das Objekt geschrieben wird.

Jetzt können Sie sagen, dass Penguindies nicht mehr als normal behandelt werden Birdkann. Aber wie gesagt, in der realen Welt kann es einfach nicht. Weil wir Birdals fliegende Einheit modellieren .

Die Alternative besteht darin, nicht die allgemeine Annahme zu treffen, dass Birds fliegen können. Es wäre vernünftig, eine BirdAbstraktion 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:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

Wenn Sie neugierig sind, habe ich eine Implementierung vonObject.make

Zusatz:

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.

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.


10
"In der realen Welt kann es einfach nicht." Ja, kann es. Ein Pinguin ist ein Vogel. Die Fähigkeit zu fliegen ist keine Eigenschaft eines Vogels, sondern lediglich eine zufällige Eigenschaft der meisten Vogelarten. Eigenschaften, die Vögel definieren, sind "gefiedert, geflügelt, zweibeinig, endotherm, eierlegend, Wirbeltiere" (Wikipedia) - nichts über das Fliegen dort.
pdr

2
@pdr wieder hängt es von deiner Definition des Vogels ab. Da ich den Begriff "Vogel" verwendete, meinte ich die Klassenabstraktion, die wir zur Darstellung von Vögeln einschließlich der Methode fly verwenden. Ich erwähnte auch, dass Sie Ihre Klassenabstraktion weniger spezifisch gestalten können. Auch ein Pinguin ist nicht gefiedert.
Raynos

2
@ Raynos: Pinguine sind in der Tat gefiedert. Ihre Federn sind natürlich ziemlich kurz und dicht.
Jon Purdy

@ JonPurdy fair genug, ich stelle mir immer vor, sie hatten Fell.
Raynos

+1 im Allgemeinen und im Besonderen für das "Mammut". LOL!
Sebastien Diot

28

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.


1
Der LSP ist für Typen gedacht , nicht für Klassen .
Jörg W Mittag

2
@ PéterTörök: Diese Frage gäbe es sonst nicht :-) Ich kann mir zwei Beispiele von Ruby vorstellen. Classist eine Unterklasse von Module, obwohl ClassIS-NOT-A Module. Es ist aber trotzdem sinnvoll, eine Unterklasse zu sein, da ein Großteil des Codes wiederverwendet wird. OTOH, StringIOIS-A IO, aber die beiden haben keine Vererbungsbeziehung (abgesehen davon, dass beide Objectnatürlich von erben ), da sie keinen gemeinsamen Code haben. Klassen dienen zur gemeinsamen Nutzung von Code, Typen zur Beschreibung von Protokollen. IOund StringIOhaben das gleiche Protokoll, daher den gleichen Typ, aber ihre Klassen hängen nicht zusammen.
Jörg W Mittag

1
@ JörgWMittag, OK, jetzt verstehe ich besser was du meinst. Für mich klingt Ihr erstes Beispiel jedoch eher nach einem Missbrauch der Vererbung als nach dem Ausdruck eines grundsätzlichen Problems, das Sie vermuten. Öffentliche Vererbung IMO sollte nicht zur Wiederverwendung der Implementierung verwendet werden, sondern nur zum Ausdrücken von Subtyp-Beziehungen (is-a). Und die Tatsache, dass es missbraucht werden kann, disqualifiziert es nicht - ich kann mir kein brauchbares Tool aus einer Domain vorstellen, die nicht missbraucht werden kann.
Péter Török,

2
Für diejenigen, die diese Antwort positiv bewerten: Beachten Sie, dass dies die Frage nicht wirklich beantwortet, insbesondere nach der bearbeiteten Klarstellung. Ich denke nicht, dass diese Antwort eine Ablehnung verdient, denn was er sagt, ist sehr wahr und wichtig zu wissen, aber er hat die Frage nicht wirklich beantwortet.
jhocking

1
Stellen Sie sich ein Java vor, in dem nur Interfaces Typen sind, Klassen nicht, und Unterklassen in der Lage sind, Interfaces ihrer Oberklasse zu "desimplementieren", und Sie haben eine ungefähre Vorstellung, denke ich.
Jörg W Mittag

15

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, FlightlessBirddie von einer Factory das richtige Verhalten injizieren, die relevanten Subtypen z. B. Penguin : FlightlessBirdautomatisch abrufen und alles andere, was wirklich spezifisch ist, wird von der Factory selbstverständlich behandelt.


1
Ich habe das Decorator-Muster in meiner Antwort erwähnt, aber das Strategy-Muster funktioniert auch ziemlich gut.
schockierend

1
+1 für "Komposition vor Vererbung bevorzugen" Die Notwendigkeit spezieller Entwurfsmuster zur Implementierung der Komposition in statisch typisierten Sprachen verstärkt jedoch meine Neigung zu dynamischen Sprachen wie Ruby.
Roy Tinker

11

Ist das eigentliche Problem, von dem Sie ausgehen, nicht Birdeine FlyMethode? Warum nicht:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

Das offensichtliche Problem ist nun die Mehrfachvererbung ( Duck). Was Sie also wirklich brauchen, sind Schnittstellen:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}

3
Das Problem ist, dass Evolution nicht dem Liskov-Substitutionsprinzip folgt und Vererbung mit Entfernen von Features durchführt.
Donal Fellows

7

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:

  • Bird ist eine Klasse mit einer fly () -Methode
  • Pinguin muss von Bird erben
  • Pinguin kann nicht fliegen ()
  • Es ist mir egal, ob es ein gutes Design ist oder ob es der realen Welt entspricht, wie es das Beispiel in dieser Frage ist.

Du sagtest :

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

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):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

Ü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):

var bird:Bird = new Penguin();
bird.fly();

wird eine Laufzeitausnahme auslösen.


"Lassen Sie Ihren Pinguin einfach eine Ausnahme auslösen oder erben Sie von einer NonFlyingBird-Klasse, die eine Ausnahme auslöst." Das ist immer noch ein Verstoß gegen LSP. Es deutet immer noch darauf hin, dass ein Pinguin fliegen kann, auch wenn die Implementierung von fly scheitern soll. Es sollte niemals eine Fliegenmethode auf Penguin geben.
pdr

@pdr: Es bedeutet nicht, dass ein Pinguin fliegen kann , sondern dass er fliegen sollte (es ist ein Vertrag). Die Ausnahme sagt Ihnen, dass dies nicht möglich ist . Übrigens behaupte ich nicht, dass es eine gute OOP-Praxis ist, ich gebe nur eine Antwort auf einen Teil der Frage
David

Punkt ist, dass ein Pinguin nicht erwartet werden sollte zu fliegen, nur weil es ein Vogel ist. Wenn ich Code schreiben möchte, der besagt: "Wenn x fliegen kann, mach das, sonst mach das." Ich muss in Ihrer Version einen Versuch / Fang verwenden, bei dem ich das Objekt nur fragen kann, ob es fliegen kann (Casting- oder Prüfmethode vorhanden). Vielleicht liegt es nur am Wortlaut, aber Ihre Antwort impliziert, dass das Auslösen einer Ausnahme mit LSP kompatibel ist.
pdr

@pdr "Ich muss einen Versuch / Fang in Ihrer Version verwenden" -> das ist der springende Punkt, um Verzeihung anstatt um Erlaubnis zu bitten (weil sogar eine Ente ihre Flügel gebrochen haben könnte und nicht fliegen kann). Ich werde den Wortlaut korrigieren.
David

"Das ist der springende Punkt, eher um Vergebung als um Erlaubnis zu bitten." Ja, mit der Ausnahme, dass das Framework für jede fehlende Methode denselben Ausnahmetyp auslösen kann. Daher ist Pythons "try: except AttributeError:" genau gleichbedeutend mit "if (X is Y) {} else {}" und sofort erkennbar so wie. Wenn Sie jedoch absichtlich eine CannotFlyException ausgelöst haben, um die Standardfunktion fly () in Bird zu überschreiben, wird sie weniger erkennbar.
pdr

7

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.


Ich stimme zu, Fly sollte eine Schnittstelle sein, die Bird implementieren kann . Es könnte auch als Methode implementiert werden, wobei das Standardverhalten überschrieben werden kann. Ein saubererer Ansatz ist jedoch die Verwendung einer Schnittstelle.
Jon Raynor

6

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:

class Bird {
public:
   virtual void fly()=0;
};

Sie sollten so etwas haben:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

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.


1
Ihre Antwort gefällt mir sehr gut. Ich denke, es verdient mehr Stimmen.
Sebastien Diot

2
Hervorragende Antwort. Das Problem ist, dass die Frage nur mit den Händen wedelt, was fly () tatsächlich tut. Jede echte Implementierung von fly hätte zumindest ein Ziel - fly (Koordinatenziel), das im Fall des Pinguins überschrieben werden könnte, um {return currentPosition)} zu implementieren
Chris Cudmore

4

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.


3

Peter hat das Liskov-Substitutionsprinzip erwähnt, aber ich denke, das muss erklärt werden.

Sei q (x) eine Eigenschaft, die für Objekte x vom Typ T beweisbar ist. Dann sollte q (y) für Objekte y vom Typ S beweisbar sein, wobei S ein Subtyp von T ist.

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.


2

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.


0

Die typische Art, mit einer solchen Situation umzugehen, besteht darin, so etwas wie UnsupportedOperationException(Java) bzw. NotImplementedException(C #).


Solange Sie diese Möglichkeit in Bird dokumentieren.
DJClayworth

0

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).


0

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.

class eagle : bird, IFly
class penguin : bird

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.


-1

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:

  • nichts
  • wirft eine Ausnahme
  • Ruft stattdessen die Penguin :: swim () -Methode auf
  • behauptet, dass der Pinguin unter Wasser ist (sie "fliegen" durch das Wasser)

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.


2
-1 für den Vorschlag, Penguin :: swim () aufzurufen. Das verstößt gegen den Grundsatz des geringsten Erstaunens und wird Wartungsprogrammierer überall dazu veranlassen, Ihren Namen zu verfluchen.
DJClayworth

1
@DJClayworth Da das Beispiel an erster Stelle lächerlich war, scheint das Abstimmen für Verstöße gegen das abgeleitete Verhalten von fly () und swim () ein bisschen zu viel. Aber wenn Sie das wirklich ernsthaft betrachten möchten, würde ich zustimmen, dass es wahrscheinlicher ist, dass Sie den umgekehrten Weg gehen und swim () in Bezug auf fly () implementieren. Enten schwimmen, indem sie mit den Füßen paddeln; Pinguine schwimmen, indem sie mit den Flügeln schlagen.
Caleb

1
Ich stimme zu, dass die Frage albern war, aber das Problem ist, dass ich Leute gesehen habe, die dies im wirklichen Leben taten - verwenden Sie vorhandene Aufrufe, die "nichts wirklich tun", um seltene Funktionen zu implementieren. Es bringt den Code wirklich durcheinander und endet normalerweise damit, schreiben zu müssen "if (! (MyBird instanceof Penguin)) fly ();" in der Hoffnung, dass niemand eine Straußenklasse schafft, gibt es viele Orte.
DJClayworth

Die Behauptung ist noch schlimmer. Wenn ich eine Reihe von Vögeln habe, die alle die fly () -Methode haben, möchte ich keinen Assertionsfehler, wenn ich fly () für sie aufrufe.
DJClayworth

1
Ich habe die Pinguin- Dokumentation nicht gelesen , weil mir eine Reihe von Vögeln ausgehändigt wurde und ich nicht wusste, dass ein Pinguin in der Reihe sein würde. Ich habe die Bird- Dokumentation gelesen, in der steht , dass der Vogel fliegt, wenn ich fly () rufe. Wenn in dieser Dokumentation klar angegeben worden wäre, dass eine Ausnahme ausgelöst werden könnte, wenn der Vogel flugunfähig wäre, hätte ich dies zugelassen. Wenn gesagt worden wäre, dass das Aufrufen von fly () es manchmal zum Schwimmen gebracht hätte, hätte ich zu einer anderen Klassenbibliothek gewechselt. Oder für einen sehr großen Drink gegangen.
DJClayworth
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.