Wie vermeide ich Downcasting?


12

Meine Frage betrifft einen Sonderfall der Superklasse Animal.

  1. Meine AnimalDose moveForward()und eat().
  2. Sealerstreckt Animal.
  3. Dogerstreckt Animal.
  4. Und es gibt ein besonderes Wesen , das auch erstreckt Animalgenannt Human.
  5. Humanimplementiert auch eine Methode speak()(nicht implementiert von Animal).

In einer Implementierung einer abstrakten Methode, die akzeptiert, Animalmöchte ich die speak()Methode verwenden. Das scheint ohne Niedergeschlagenheit nicht möglich zu sein. Jeremy Miller schrieb in seinem Artikel, dass ein Niedergeschlagener riecht.

Was wäre eine Lösung, um in dieser Situation einen Sturz zu vermeiden?


6
Repariere dein Modell. Die Verwendung der vorhandenen taxonomischen Hierarchie zum Modellieren einer Klassenhierarchie ist normalerweise eine falsche Idee. Sie sollten Ihre Abstraktionen aus vorhandenem Code ziehen, keine Abstraktionen erstellen und dann versuchen, Code darauf abzustimmen.
Euphoric

2
Wenn Sie möchten, dass Tiere sprechen, ist die Fähigkeit zu sprechen Teil dessen, was ein Tier ausmacht
JeffO

8
vorwärts bewegen? was ist mit Krabben?
Fabio Marcolini

Welche Artikelnummer ist das in Effective Java, übrigens?
Goldlöckchen

1
Einige Leute können nicht sprechen, also wird eine Ausnahme geworfen, wenn Sie es versuchen.
Tulains Córdova

Antworten:


12

Wenn Sie eine Methode haben, die wissen muss, ob die bestimmte Klasse vom Typ ist Human, um etwas zu tun, dann brechen Sie einige SOLID-Prinzipien , insbesondere:

  • Offenes / geschlossenes Prinzip - Wenn Sie in Zukunft einen neuen Tiertyp hinzufügen müssen, der sprechen kann (z. B. einen Papagei), oder etwas spezielles für diesen Typ tun müssen, muss sich Ihr bestehender Code ändern
  • Prinzip der Schnittstellentrennung - es klingt, als würden Sie zu viel verallgemeinern. Ein Tier kann ein breites Artenspektrum abdecken.

Wenn Ihre Methode einen bestimmten Klassentyp erwartet, um diese bestimmte Methode aufzurufen, ändern Sie diese Methode meiner Meinung nach so, dass nur diese Klasse und nicht die Schnittstelle akzeptiert wird.

Etwas wie das :

public void MakeItSpeak( Human obj );

und nicht so:

public void SpeakIfHuman( Animal obj );

Man könnte auch eine abstrakte Methode auf Animalrufen canSpeakund jede konkrete Implementierung muss definieren, ob sie "sprechen" kann oder nicht.
Brandon

Aber ich mag deine Antwort besser. Viel Komplexität entsteht durch den Versuch, etwas wie etwas zu behandeln, das es nicht ist.
Brandon

@Brandon Ich denke, in diesem Fall, wenn es nicht "sprechen" kann, dann werfen Sie eine Ausnahme. Oder einfach nichts tun.
BЈови14

So kommt es zu einer Überlastung: public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}undpublic void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Bart Weber

1
Gehen Sie den ganzen Weg - makeItSpeak (ISpeakingAnimal animal) - Sie können dann ISpeakingAnimal Animal implementieren lassen. Sie könnten auch makeItSpeak (Animal animal) {speak if instance of ISpeakingAnimal} haben, aber das hat einen schwachen Geruch.
Ptyx

6

Das Problem ist nicht, dass du einen Downcast machst - es ist, dass du einen Downcast machst Human. Erstellen Sie stattdessen eine Schnittstelle:

public interface CanSpeak{
    void speak();
}

public abstract class Animal{
    //....
}

public class Human extends Animal implements CanSpeak{
    public void speak(){
        //....
    }
}

public void mysteriousMethod(Animal animal){
    //....
    if(animal instanceof CanSpeak){
        ((CanSpeak)animal).speak();
    }else{
        //Throw exception or something
    }
    //....
}

Auf diese Weise ist die Bedingung nicht, dass das Tier ist Human- die Bedingung ist, dass es sprechen kann. Dies bedeutet mysteriousMethod, dass mit anderen, nicht menschlichen Unterklassen gearbeitet werden kann, Animalsofern diese implementiert sind CanSpeak.


In diesem Fall sollte eine Instanz von CanSpeak anstelle von Animal
Newtopian am

1
@Newtopian Angenommen, diese Methode benötigt nichts von Animalund alle Benutzer dieser Methode enthalten das Objekt, das sie über eine CanSpeakTypreferenz (oder sogar eine Typreferenz) an sie senden möchten Human. Wenn das der Fall wäre, hätte diese Methode Humanin erster Linie verwendet werden können und wir müssten sie nicht vorstellen CanSpeak.
Idan Arye

Die Schnittstelle loszuwerden ist nicht an die Spezifität der Methodensignatur gebunden. Sie können die Schnittstelle nur dann loswerden, wenn es nur Menschen gibt und es jemals nur Menschen geben wird, die "CanSpeak" können.
Newtopian

1
@Newtopian Der Grund, den wir zuerst eingeführt CanSpeakhaben, ist nicht, dass wir etwas haben, das es implementiert ( Human), sondern dass wir etwas haben, das es verwendet (die Methode). Es CanSpeakgeht darum, diese Methode von der konkreten Klasse zu entkoppeln Human. Wenn wir keine Methoden hätten, um Dinge so CanSpeakunterschiedlich zu behandeln , hätte es keinen Sinn, Dinge so zu unterscheiden CanSpeak. Wir erstellen keine Schnittstelle, nur weil wir eine Methode haben ...
Idan Arye

2

In diesem Fall wäre die Standardimplementierung von speak()in der AbstractAnimalKlasse:

void speak() throws CantSpeakException {
  throw new CantSpeakException();
}

Zu diesem Zeitpunkt haben Sie eine Standardimplementierung in der Abstract-Klasse - und sie verhält sich korrekt.

try {
  thingy.speak();
} catch (CantSeakException e) {
  System.out.println("You can't talk to the " + thingy.name());
}

Ja, das heißt, Sie haben Versuchsfänge im Code verstreut, um alle zu behandeln speak, aber die Alternative dazu besteht darin, if(thingy is Human)stattdessen alle Speaks zu verpacken.

Der Vorteil der Ausnahme ist, dass Sie nicht alle Tests erneut durchführen müssen, wenn Sie irgendwann eine andere Art von Dingen haben, die sprechen (einen Papagei).


1
Ich denke nicht, dass dies ein guter Grund ist, eine Ausnahme zu verwenden (es sei denn, es handelt sich um Python-Code).
Bryan Chen

1
Ich werde nicht abstimmen, weil dies technisch funktionieren würde, aber ich mag diese Art von Design wirklich nicht. Warum eine Methode auf übergeordneter Ebene definieren, die die übergeordnete Ebene nicht implementieren kann? Wenn Sie nicht wissen, um welchen Typ es sich bei dem Objekt handelt (und wenn ja, hätten Sie dieses Problem nicht), müssen Sie immer einen try / catch-Befehl verwenden, um nach einer vollständig vermeidbaren Ausnahme zu suchen. Sie könnten sogar eine canSpeak()Methode verwenden, um dies besser zu handhaben.
Brandon

2
Ich habe an einem Projekt gearbeitet, das eine Menge Fehler aufwies, die auf die Entscheidung zurückzuführen waren, eine Reihe von Arbeitsmethoden neu zu schreiben, um eine Ausnahme auszulösen, da sie eine veraltete Implementierung verwenden. Er hätte die Implementierung korrigieren können, entschied sich jedoch dafür, überall Ausnahmen zu werfen. Natürlich haben wir die Qualitätssicherung mit Hunderten von Fehlern gestartet. Daher bin ich voreingenommen, keine Ausnahmen in Methoden zu werfen, die nicht aufgerufen werden sollen. Wenn sie nicht aufgerufen werden sollen, löschen Sie sie .
Brandon

2
Alternative zum Auslösen einer Ausnahme in diesem Fall kann nichts tun. Immerhin können sie nicht sprechen :)
BЈовић

@Brandon Es gibt einen Unterschied zwischen dem Design von Anfang an mit der Ausnahme und dem späteren Nachrüsten. Man könnte auch eine Schnittstelle mit einer Standardmethode in Java8 betrachten oder keine Ausnahme auslösen und einfach still sein. Der entscheidende Punkt ist jedoch, dass die Funktion in dem übergebenen Typ definiert werden muss, damit kein Downcast erforderlich ist.

1

Sie könnten Communicate to Animal hinzufügen. Hund bellt, Mensch spricht, Robbe ... äh ... Ich weiß nicht, was Robbe tut.

Aber es hört sich so an, als ob Ihre Methode so konzipiert ist, dass (Tier ist Mensch) Sprechen ();

Die Frage, die Sie möglicherweise stellen möchten, lautet: Was ist die Alternative? Es ist schwierig, einen Vorschlag zu machen, da ich nicht genau weiß, was Sie erreichen möchten. Es gibt theoretische Situationen, in denen Downcasting / Upcasting der beste Ansatz ist.


15
Das Siegel geht durcheinander , aber der Fuchs ist undefiniert.

1

Downcasting ist manchmal notwendig und angemessen. Insbesondere ist es häufig in Fällen angebracht, in denen Objekte vorhanden sind, die möglicherweise über eine bestimmte Fähigkeit verfügen oder nicht, und diese Fähigkeit möchte man verwenden, wenn sie vorhanden ist, während Objekte ohne diese Fähigkeit auf eine bestimmte Standardweise behandelt werden. Als einfaches Beispiel wird angenommen, dass a Stringgefragt wird, ob es einem anderen beliebigen Objekt entspricht. Damit eins Stringdem anderen entspricht String, muss es die Länge und das Hintergrundzeichenarray der anderen Zeichenfolge untersuchen. Wenn a Stringgefragt wird, ob es gleich a Dogist, kann es nicht auf die Länge von zugreifen Dog, aber es sollte nicht müssen; stattdessen, wenn das Objekt, mit dem sich a Stringvergleichen soll, nicht a istStringsollte der Vergleich ein Standardverhalten verwenden (Meldung, dass das andere Objekt nicht gleich ist).

Die Zeit, in der das Herabwerfen als am zweifelhaftesten angesehen werden sollte, ist die Zeit, in der "bekannt" ist, dass das zu gießende Objekt vom richtigen Typ ist. Wenn bekannt ist, dass ein Objekt ein Objekt ist Cat, sollte im Allgemeinen eine Variable vom Typ Catanstelle einer Variablen vom Typ verwendet werden Animal, um auf dieses Objekt zu verweisen. Es gibt Zeiten, in denen dies jedoch nicht immer funktioniert. Beispielsweise kann eine ZooSammlung Paare von Objekten in Slots für gerade / ungerade Arrays enthalten, mit der Erwartung, dass die Objekte in jedem Paar aufeinander einwirken können, auch wenn sie nicht auf die Objekte in anderen Paaren einwirken können. In einem solchen Fall müssten die Objekte in jedem Paar immer noch einen unspezifischen Parametertyp akzeptieren, sodass sie syntaktisch die Objekte von jedem anderen Paar übergeben . Also, auch wenn Cat's gehtplayWith(Animal other)Verfahren funktionieren würde nur dann , wenn othereine war Cat, die Zooin der Lage sein müssen , wäre es ein Element eines zu übergeben Animal[], so dass ihr Parametertyp sein müsste, Animalals vielmehr Cat.

In Fällen, in denen Downcasting legitimerweise unvermeidbar ist, sollte man es ohne Bedenken anwenden. Die entscheidende Frage ist, wann man Downcasting sinnvoll vermeiden und wann es sinnvoll möglich ist.


Im String-Fall sollten Sie lieber eine Methode haben Object.equalToString(String string). Dann haben Sie boolean String.equal(Object object) { return object.equalStoString(this); }also keinen Downcast nötig: Sie können dynamisches Dispatching nutzen.
Giorgio

@ Giorgio: Dynamisches Disponieren hat seine Verwendung, ist aber im Allgemeinen noch schlimmer als Downcasting.
Supercat

Weg dynamisches Disponieren generell noch schlimmer als Downcasting? Ich denke, es ist anders herum
Bryan Chen

@BryanChen: Das hängt von Ihrer Terminologie ab. Ich glaube nicht, dass Objectes eine equalStoStringvirtuelle Methode gibt, und ich gebe zu, dass ich nicht weiß, wie das angeführte Beispiel überhaupt in Java funktionieren würde, aber in C # würde dynamisches Versenden (anders als virtuelles Versenden) bedeuten, dass der Compiler im Wesentlichen über Folgendes verfügt Reflection-based Name-Lookup ausführen, wenn eine Methode zum ersten Mal für eine Klasse verwendet wird, die sich vom virtuellen Versand unterscheidet (der einfach über einen Steckplatz in der virtuellen Methodentabelle aufruft, der eine gültige Methodenadresse enthalten muss).
Supercat

Aus modelltechnischer Sicht würde ich generell dynamisches Dispatching bevorzugen. Es ist auch die objektorientierte Art, eine Prozedur basierend auf der Art ihrer Eingabeparameter auszuwählen.
Giorgio

1

In einer Implementierung einer abstrakten Methode, die Animals akzeptiert, möchte ich die speak () -Methode verwenden.

Sie haben ein paar Möglichkeiten:

  • Verwenden Sie Reflection, um anzurufen, speakfalls vorhanden. Vorteil: keine Abhängigkeit von Human. Nachteil: Es gibt jetzt eine versteckte Abhängigkeit vom Namen "speak".

  • Eine neue Schnittstelle einführen Speakerund auf die Schnittstelle herunterschalten. Dies ist flexibler als abhängig von einem bestimmten Betontyp. Dies hat den Nachteil, dass Sie Änderungen vornehmen müssen, um sie Humanzu implementieren Speaker. Dies funktioniert nicht, wenn Sie keine Änderungen vornehmen könnenHuman

  • Niedergeschlagen auf Human. Dies hat den Nachteil, dass Sie den Code immer dann ändern müssen, wenn eine andere Unterklasse sprechen soll. Idealerweise möchten Sie Anwendungen erweitern, indem Sie Code hinzufügen, ohne immer wieder zurück zu gehen und alten Code zu ändern.

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.