Wie gehe ich mit den Methoden um, die im Kontext des Polymorphismus für Subtypen hinzugefügt wurden?

14

Wenn Sie das Konzept des Polymorphismus verwenden, erstellen Sie eine Klassenhierarchie und rufen mit Hilfe der Elternreferenz die Schnittstellenfunktionen auf, ohne zu wissen, über welchen bestimmten Typ das Objekt verfügt. Das ist toll. Beispiel:

Sie haben eine Sammlung von Tieren und Sie rufen alle Tierfunktionen auf eatund es ist Ihnen egal, ob es sich um einen Hund oder eine Katze handelt. Aber in der gleichen Klassenhierarchie haben Sie Tiere , die zusätzlichen haben - anders als geerbt und von Klasse implementiert Animal, zum Beispiel makeEggs, getBackFromTheFreezedStateund so weiter. In einigen Fällen möchten Sie in Ihrer Funktion möglicherweise den spezifischen Typ kennen, um zusätzliche Verhaltensweisen aufzurufen.

Zum Beispiel, wenn es Morgen ist und wenn es nur ein Tier eatist, dann rufst du an , sonst, wenn es ein Mensch ist, rufst du zuerst washHandsan getDressedund dann erst an eat. Wie gehe ich mit diesen Fällen um? Polymorphismus stirbt. Sie müssen den Typ des Objekts herausfinden, der sich wie ein Codegeruch anhört. Gibt es einen gemeinsamen Ansatz zur Behandlung dieser Fälle?

Narek
quelle
7
Die von Ihnen beschriebene Art des Polymorphismus wird als Subtyp-Polymorphismus bezeichnet , ist jedoch nicht die einzige Art (siehe Polymorphismus ). Sie müssen keine Klassenhierarchie erstellen, um Polymorphismus auszuführen (und ich würde sogar argumentieren, dass Vererbung nicht die häufigste Methode ist, um Polymorphismus mit Untertypen zu erzielen, da die Implementierung einer Schnittstelle viel häufiger ist).
Vincent Savard
24
Wenn Sie eine EaterSchnittstelle mit der eat()Methode definieren, kümmert Humanes Sie als Client nicht, dass eine Implementierung diese zuerst aufrufen muss, washHands()und getDressed()es handelt sich um Implementierungsdetails dieser Klasse. Wenn Ihnen als Kunde diese Tatsache am Herzen liegt, verwenden Sie höchstwahrscheinlich nicht das richtige Tool für den Job.
Vincent Savard
3
Sie müssen auch berücksichtigen, dass ein Mensch morgens möglicherweise etwas getDressedvor sich haben muss eat, was beim Mittagessen nicht der Fall ist. Je nach Ihren Umständen ist dies washHands();if !dressed then getDressed();[code to actually eat]möglicherweise der beste Weg, um dies für einen Menschen umzusetzen. Eine andere Möglichkeit ist was, wenn andere Dinge das erfordern washHandsund / oder getDressedgenannt werden? Angenommen, Sie haben leaveForWork? Möglicherweise müssen Sie den Programmfluss so strukturieren, dass er ohnehin schon lange vorher aufgerufen wird.
Duncan X Simpson
1
Denken Sie daran, dass das Abgleichen des genauen Typs in OOP ein Codegeruch sein kann, in FP jedoch eine weit verbreitete Praxis ist (dh, verwenden Sie Pattern Matching, um den Typ einer diskriminierten Union zu bestimmen, und handeln Sie danach).
Theodoros Chatzigiannakis
3
Hüten Sie sich vor Beispielen von OO-Hierarchien wie Tieren im Schulzimmer. Echte Programme haben fast nie so saubere Taxonomien. ZB ericlippert.com/2015/04/27/wizards-and-warriors-part-one . Oder wenn Sie das ganze Paradigma hinterfragen wollen: Objektorientierte Programmierung ist schlecht .
jpmc26

Antworten:

18

Hängt davon ab. Leider gibt es keine generische Lösung. Denken Sie über Ihre Anforderungen nach und versuchen Sie herauszufinden, was diese Dinge tun sollten.

Sie sagten zum Beispiel morgens, dass verschiedene Tiere verschiedene Dinge tun. Wie wäre es, wenn Sie eine Methode getUp()oder prepareForDay()ähnliches einführen? Dann können Sie mit dem Polymorphismus fortfahren und jedes Tier seine Morgenroutine ausführen lassen.

Wenn Sie zwischen Tieren unterscheiden wollen, sollten Sie sie nicht wahllos in einer Liste ablegen.

Wenn nichts anderes funktioniert, können Sie das Besuchermuster ausprobieren , das eine Art Hack darstellt , um eine Art dynamisches Dispatching zu ermöglichen, bei dem Sie einen Besucher einreichen können, der typgenaue Rückrufe von Tieren erhält. Ich möchte jedoch betonen, dass dies ein letzter Ausweg sein sollte, wenn alles andere fehlschlägt.

Robert Bräutigam
quelle
33

Dies ist eine gute Frage und es ist die Art von Ärger, die viele Leute haben, wenn sie versuchen, zu verstehen, wie man OO benutzt. Ich denke, die meisten Entwickler haben damit zu kämpfen. Ich wünschte, ich könnte sagen, dass die meisten daran vorbeikommen, aber ich bin mir nicht sicher, ob das der Fall ist. Meiner Erfahrung nach verwenden die meisten Entwickler Pseudo-OO-Eigenschaftensäcke .

Lassen Sie mich zunächst klar sein. Das ist nicht deine Schuld. Die Art und Weise, wie OO gelehrt wird, ist in der Regel sehr fehlerhaft. Das AnimalBeispiel ist der Haupttäter, IMO. Grundsätzlich sagen wir, lassen Sie uns über Objekte sprechen, was können sie tun. Eine AnimalDose eat()und es kann speak(). Super. Erstellen Sie nun einige Tiere und kodieren Sie, wie sie essen und sprechen. Jetzt weißt du OO, richtig?

Das Problem ist, dass dies bei OO aus der falschen Richtung kommt. Warum gibt es Tiere in diesem Programm und warum müssen sie sprechen und essen?

Es fällt mir schwer, über eine wirkliche Verwendung für einen AnimalTyp nachzudenken . Ich bin mir sicher, dass es das gibt, aber lassen Sie uns etwas diskutieren, von dem ich denke, dass es einfacher ist, darüber nachzudenken: eine Verkehrssimulation. Angenommen, wir möchten den Verkehr in verschiedenen Szenarien modellieren. Hier sind einige grundlegende Dinge, die wir brauchen, um das zu können.

Vehicle
Road
Signal

Wir können mit allen möglichen Dingen, wie Fußgängern und Zügen, tiefer gehen, aber wir werden es einfach halten.

Lassen Sie uns überlegen Vehicle. Welche Fähigkeiten benötigt das Fahrzeug? Es muss auf einer Straße fahren. Es muss in der Lage sein, bei Signalen anzuhalten. Es muss in der Lage sein, Kreuzungen zu navigieren.

interface Vehicle {
  move(Road road);
  navigate(Road... intersection);
}

Das ist wahrscheinlich zu einfach, aber es ist ein Anfang. Jetzt. Was ist mit all den anderen Dingen, die ein Fahrzeug tun könnte? Sie können von einer Straße in einen Graben abbiegen. Gehört das zur Simulation? Nein, brauche es nicht. Einige Autos und Busse verfügen über eine Hydraulik, mit der sie springen oder knien können. Gehört das zur Simulation? Nein, brauche es nicht. Die meisten Autos verbrennen Benzin. Manche nicht. Ist das Kraftwerk Teil der Simulation? Nein, brauche es nicht. Radgröße? Brauche es nicht GPS Navigation? Infotainmentsystem? Ich brauche sie nicht.

Sie müssen nur das Verhalten definieren, das Sie verwenden möchten. Aus diesem Grund denke ich, ist es oft besser, OO-Schnittstellen aus dem Code zu erstellen, der mit ihnen interagiert. Sie beginnen mit einer leeren Schnittstelle und schreiben dann den Code, der die nicht vorhandenen Methoden aufruft. So wissen Sie, welche Methoden Sie für Ihre Benutzeroberfläche benötigen. Anschließend definieren Sie Klassen, die diese Verhaltensweisen implementieren. Verhaltensweisen, die nicht verwendet werden, sind irrelevant und müssen nicht definiert werden.

Der springende Punkt bei OO ist, dass Sie später neue Implementierungen dieser Schnittstellen hinzufügen können, ohne den aufrufenden Code zu ändern. Das funktioniert nur, wenn die Anforderungen des aufrufenden Codes bestimmen, was in der Schnittstelle abläuft. Es gibt keine Möglichkeit, alle Verhaltensweisen aller möglichen Dinge zu definieren, die später erdacht werden könnten.

JimmyJames
quelle
13
Das ist eine gute Antwort. "Aus diesem Grund denke ich, ist es oft besser, OO-Schnittstellen aus dem Code zu erstellen, der mit ihnen interagiert." Absolut, und ich würde behaupten, es ist der einzige Weg. Sie können den öffentlichen Auftrag einer Schnittstelle nicht nur durch die Implementierung kennen, sondern immer aus der Perspektive ihrer Kunden definieren. (Und als Randnotiz ist dies, worum es bei TDD eigentlich geht.)
Vincent Savard
@VincentSavard "Ich würde behaupten, es ist der einzige Weg." Du hast recht. Ich denke, der Grund, warum ich es nicht absolut gemacht habe, ist, dass man, sobald man die Idee hat, die Oberfläche irgendwie ausarbeiten und sie dann auf diese Weise verfeinern kann. Letztendlich ist es das Einzige, was zählt, wenn es um Messing geht.
JimmyJames
@ jpmc26 Vielleicht ein bisschen stark formuliert. Ich bin nicht sicher, ob ich damit einverstanden bin, dass dies nur selten umgesetzt wird. Ich bin mir nicht sicher, wie Schnittstellen nützlich sein können, wenn Sie sie nicht auf diese Weise verwenden, abgesehen von Markierungsschnittstellen, die ich für eine schreckliche Idee halte.
JimmyJames
9

TL; DR:

Stellen Sie sich eine Abstraktion und Methoden vor, die für alle Unterklassen gelten, und decken Sie alles ab, was Sie benötigen.

Bleiben wir zunächst bei Ihrem eat()Beispiel.

Es ist eine Eigenschaft des Menschseins, dass Menschen als Voraussetzung für das Essen ihre Hände waschen und sich vor dem Essen anziehen wollen. Wenn Sie möchten, dass jemand zu Ihnen kommt, um mit Ihnen zu frühstücken, sagen Sie ihm nicht , er solle sich die Hände waschen und anziehen, er tut es auf eigene Faust, wenn Sie ihn einladen, oder er antwortet: "Nein, ich kann nicht kommen Ich habe meine Hände nicht gewaschen und bin noch nicht angezogen ".

Zurück zur Software:

Als HumanBeispiel wird , ohne dass die Voraussetzungen nicht essen, würde ich das hat Humans‘ eat()Methode zu tun washHands()und getDressed()wenn das nicht geschehen ist. Es sollte nicht Ihre Aufgabe als eat()Anrufer sein, über diese Besonderheit Bescheid zu wissen. Die Alternative für hartnäckige Menschen wäre, eine Ausnahme zu machen ("Ich bin nicht bereit zu essen!"), Wenn die Voraussetzungen nicht erfüllt sind, was Sie frustriert, aber zumindest darüber informiert, dass das Essen nicht funktioniert.

Was ist makeEggs()?

Ich würde empfehlen, Ihre Denkweise zu ändern. Sie möchten wahrscheinlich die geplanten Morgenaufgaben aller Wesen ausführen. Auch hier sollte es nicht Ihre Aufgabe sein, als Anrufer zu wissen, welche Aufgaben sie haben. Daher würde ich eine doMorningDuties()Methode empfehlen , die von allen Klassen implementiert wird.

Ralf Kleberhoff
quelle
Ich stimme dieser Antwort zu. Narek hat Recht mit dem Code-Geruch. Es ist das Design der Benutzeroberfläche, das stinkt.
Jonathan van de Veen
Was in dieser Antwort beschrieben wird, wird normalerweise als Liskov-Substitutionsprinzip bezeichnet .
Philipp
2

Die Antwort ist ganz einfach.

Wie gehe ich mit Objekten um, die mehr können als erwartet?

Sie müssen nicht damit umgehen, weil es keinen Zweck erfüllen würde. Eine Schnittstelle wird normalerweise in Abhängigkeit von ihrer Verwendung entworfen. Wenn Ihre Benutzeroberfläche kein Händewaschen definiert, ist es Ihnen als Aufrufer der Benutzeroberfläche egal. wenn du es getan hättest, hättest du es anders gestaltet.

Wenn es beispielsweise Morgen ist und es sich nur um ein Tier handelt, rufen Sie eat auf. Wenn es sich um einen Menschen handelt, rufen Sie zuerst washHands, getDressed und dann eat auf. Wie gehe ich mit diesen Fällen um?

Zum Beispiel im Pseudocode:

interface IEater { void Eat(); }
interface IMorningRoutinePerformer { void DoMorningRoutine(); }
interface IAnimal : IEater, IMorningPerformer;
interface IHuman : IEater, IMorningPerformer; 
{
  void WashHands();
  void GetDressed();
}

void MorningTime()
{
   IList<IMorningRoutinePerformer> items = Service.GetMorningPerformers();
   foreach(item in items) { item.DoMorningRoutine(); }
}

Jetzt implementieren Sie, IMorningPerformerum Animalnur zu essen, und Humanimplementieren es, um Hände zu waschen und sich anzuziehen. Der Aufrufer Ihrer MorningTime-Methode könnte sich weniger darum kümmern, ob es sich um einen Menschen oder ein Tier handelt. Alles, was es will, ist die morgendliche Routine, die jedes Objekt dank OO bewundernswert macht.

Polymorphismus stirbt.

Oder doch?

Sie müssen den Typ des Objekts herausfinden

Warum nehmen wir das an? Ich denke, das könnte eine falsche Annahme sein.

Gibt es einen gemeinsamen Ansatz zur Behandlung dieser Fälle?

Ja, normalerweise wird es mit einer sorgfältig entworfenen Klassen- oder Schnittstellenhierarchie gelöst. Beachten Sie, dass es im obigen Beispiel nichts gibt, was Ihrem Beispiel in der von Ihnen angegebenen Form widerspricht. Sie werden sich jedoch wahrscheinlich unzufrieden fühlen, da Sie weitere Annahmen getroffen haben, die Sie in der Frage zum Zeitpunkt des Schreibens nicht geschrieben haben , und diese Annahmen sind wahrscheinlich verletzt.

Es ist möglich, zu einem Kaninchenbau zu gehen, indem Sie Ihre Annahmen verschärfen und ich die Antwort ändere, um sie noch zu befriedigen, aber ich halte das nicht für nützlich.

Das Entwerfen guter Klassenhierarchien ist schwierig und erfordert viel Einblick in Ihre Geschäftsdomäne. Bei komplexen Domänen werden zwei, drei oder sogar mehr Iterationen durchlaufen, während das Verständnis der Interaktion zwischen verschiedenen Entitäten in der Unternehmensdomäne verfeinert wird, bis ein geeignetes Modell gefunden wird.

Hier fehlen vereinfachte Tierbeispiele. Wir wollen einfach lehren, aber das Problem, das wir zu lösen versuchen, ist nicht offensichtlich, bis Sie tiefer gehen, dh komplexere Überlegungen und Bereiche haben.

Andrew Savinykh
quelle