Comment éviter le downcasting?

12

Ma question concerne un cas particulier de la super classe Animal.

  1. Ma Animalboîte moveForward()et eat().
  2. Seals'étend Animal.
  3. Dogs'étend Animal.
  4. Et il y a une créature spéciale qui s'étend également Animalappelée Human.
  5. Humanimplémente également une méthode speak()(non implémentée par Animal).

Dans une implémentation d'une méthode abstraite qui accepte, Animalje voudrais utiliser la speak()méthode. Cela ne semble pas possible sans faire un abattage. Jeremy Miller a écrit dans son article qu'une odeur baissée sent.

Quelle serait une solution pour éviter la rétrogradation dans cette situation?

Bart Weber
la source
6
Réparez votre modèle. Utiliser la hiérarchie taxonomique existante pour modéliser une hiérarchie de classes est généralement une mauvaise idée. Vous devez extraire vos abstractions du code existant, ne pas créer d'abstractions, puis essayer de lui adapter le code.
Euphoric
2
Si vous voulez que les animaux parlent, alors la capacité de parler fait partie de ce qui fait de quelque chose un animal: artima.com/interfacedesign/PreferPoly.html
JeffO
8
Avance? et les crabes?
Fabio Marcolini
De quel élément s'agit-il dans Effective Java, BTW?
goldilocks
1
Certaines personnes ne peuvent pas parler, donc une exception est levée si vous essayez.
Tulains Córdova

Réponses:

12

Si vous avez une méthode qui doit savoir si la classe spécifique est de type Humanafin de faire quelque chose, alors vous brisez certains principes SOLIDES , en particulier:

  • Principe ouvert / fermé - si, à l'avenir, vous devez ajouter un nouveau type d'animal qui peut parler (par exemple, un perroquet), ou faire quelque chose de spécifique pour ce type, votre code existant devra changer
  • Principe de ségrégation des interfaces - il semble que vous généralisiez trop. Un animal peut couvrir un large éventail d'espèces.

À mon avis, si votre méthode attend un type de classe particulier, pour appeler sa méthode particulière, changez cette méthode pour n'accepter que cette classe, et non son interface.

Quelque chose comme ça :

public void MakeItSpeak( Human obj );

et pas comme ça:

public void SpeakIfHuman( Animal obj );
BЈовић
la source
On pourrait également créer une méthode abstraite sur Animalappelée canSpeaket chaque implémentation concrète doit définir si elle peut ou non "parler".
Brandon
Mais j'aime mieux votre réponse. Beaucoup de complexité vient d'essayer de traiter quelque chose comme quelque chose qu'il n'est pas.
Brandon
@Brandon Je suppose que dans ce cas, s'il ne peut pas "parler", alors lancez une exception. Ou tout simplement ne rien faire.
BЈовић
Donc, vous obtenez une surcharge comme ça: public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}etpublic void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Bart Weber
1
Allez jusqu'au bout - makeItSpeak (ISpeakingAnimal animal) - vous pouvez alors avoir ISpeakingAnimal implémente Animal. Vous pouvez également avoir makeItSpeak (Animal animal) {parler si instanceof ISpeakingAnimal}, mais qui a une faible odeur.
ptyx
6

Le problème n'est pas que vous abaissez - c'est que vous abattez Human. À la place, créez une interface:

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
    }
    //....
}

De cette façon, la condition n'est pas que l'animal soit Human- la condition est qu'il puisse parler. Cela signifie qu'il mysteriousMethodpeut fonctionner avec d'autres sous-classes non humaines Animaltant qu'elles sont implémentées CanSpeak.

Idan Arye
la source
auquel cas il doit prendre comme argument une instance de CanSpeak au lieu d'Animal
Newtopian
1
@Newtopian En supposant que cette méthode n'a besoin de rien Animalet que tous les utilisateurs de cette méthode contiendront l'objet qu'ils souhaitent lui envoyer via une CanSpeakréférence de type (ou même Humanune référence de type). Si tel était le cas, cette méthode aurait pu être utilisée Humanen premier lieu et nous n'aurions pas besoin de la présenter CanSpeak.
Idan Arye
Se débarrasser de l'interface n'est pas lié à la spécificité de la signature de méthode, vous pouvez vous débarrasser de l'interface si et seulement s'il n'y a que des Humains et ne sera jamais que des humains qui "CanSpeak".
Newtopian
1
@Newtopian La raison pour laquelle nous avons introduit CanSpeaken premier lieu n'est pas que nous avons quelque chose qui l'implémente ( Human) mais que nous avons quelque chose qui l'utilise (la méthode). Il CanSpeaks'agit de découpler cette méthode de la classe concrète Human. Si nous n'avions pas de méthodes pour traiter les choses CanSpeakdifféremment, il serait inutile de les distinguer CanSpeak. Nous ne créons pas une interface juste parce que nous avons une méthode ...
Idan Arye
2

Vous pouvez ajouter Communiquer à l'animal. Le chien aboie, l'homme parle, le phoque ... euh ... Je ne sais pas ce que fait le phoque.

Mais il semble que votre méthode soit conçue pour (Animal est humain) parler ();

La question que vous voudrez peut-être poser est quelle est l'alternative? C'est difficile de donner une suggestion car je ne sais pas exactement ce que vous voulez réaliser. Il existe des situations théoriques où le downcasting / upcasting est la meilleure approche.

Andrew Hoffman
la source
15
Le joint va ow ow ow , mais le renard est indéfini.
2

Dans ce cas, l'implémentation par défaut de speak()dans la AbstractAnimalclasse serait:

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

À ce stade, vous avez une implémentation par défaut dans la classe Abstract - et elle se comporte correctement.

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

Oui, cela signifie que vous avez des tentatives de captures éparpillées dans le code pour gérer tout speak, mais l'alternative à cela consiste à if(thingy is Human)envelopper tous les discours à la place.

L'avantage de l'exception est que si vous avez un autre type de chose qui parle (un perroquet), vous n'aurez pas besoin de réimplémenter tous vos tests.


la source
1
Je ne pense pas que ce soit une bonne raison d'utiliser l'exception (sauf s'il s'agit de code python).
Bryan Chen
1
Je ne voterai pas contre parce que cela fonctionnerait techniquement, mais je n'aime vraiment pas ce genre de conception. Pourquoi définir une méthode au niveau parent que le parent ne peut pas implémenter? À moins que vous ne sachiez de quel type est l'objet (et si vous l'avez fait - vous n'auriez pas ce problème), vous devez toujours utiliser un try / catch pour rechercher une exception complètement évitable. Vous pouvez même utiliser une canSpeak()méthode pour mieux gérer cela.
Brandon
2
J'ai travaillé sur un projet qui avait une tonne de défauts provenant de la décision de quelqu'un de réécrire un tas de méthodes de travail pour lever une exception car ils utilisent une implémentation obsolète. Il aurait pu corriger l'implémentation, mais a plutôt choisi de lever des exceptions partout. Naturellement, nous sommes entrés dans le contrôle qualité avec des centaines de bugs. Je suis donc partisan de ne pas lancer d'exceptions dans des méthodes qui ne sont pas censées être appelées. S'ils ne sont pas censés être appelés, supprimez-les .
Brandon
2
Une alternative au lancement d'une exception dans ce cas peut ne rien faire. Après tout, ils ne peuvent pas parler :)
BЈовић
@Brandon, il y a une différence entre le concevoir dès le départ, à l'exception de le rénover plus tard. On pourrait également regarder une interface avec une méthode par défaut dans Java8, ou ne pas lever d'exception et se taire. Le point clé est cependant que pour éviter d'avoir à abattre, la fonction doit être définie dans le type transmis.
1

La descente est parfois nécessaire et appropriée. En particulier, cela est souvent approprié dans les cas où l'on a des objets qui peuvent ou non avoir une certaine capacité, et on souhaite utiliser cette capacité lorsqu'elle existe tout en manipulant des objets sans cette capacité d'une manière par défaut. À titre d'exemple simple, supposons que l' Stringon demande à a s'il est égal à un autre objet arbitraire. Pour que l'un soit Stringégal à un autre String, il doit examiner la longueur et le tableau de caractères de support de l'autre chaîne. Si Stringon demande à a s'il est égal à a Dog, cependant, il ne peut pas accéder à la longueur de Dog, mais il ne devrait pas avoir à le faire; au lieu de cela, si l'objet auquel a Stringest censé se comparer n'est pas unString, la comparaison doit utiliser un comportement par défaut (signalant que l'autre objet n'est pas égal).

Le moment où la descente doit être considéré comme le plus douteux est celui où l'objet projeté est "connu" pour être du type approprié. En général, si un objet est connu pour être un Cat, il faut utiliser une variable de type Cat, plutôt qu'une variable de type Animal, pour s'y référer. Il y a des moments où cela ne fonctionne pas toujours, cependant. Par exemple, une Zoocollection peut contenir des paires d'objets dans des emplacements de tableau pairs / impairs, en s'attendant à ce que les objets de chaque paire puissent agir les uns sur les autres, même s'ils ne peuvent pas agir sur les objets des autres paires. Dans un tel cas, les objets de chaque paire devraient toujours accepter un type de paramètre non spécifique de sorte qu'ils pourraient, syntaxiquement , transmettre les objets de toute autre paire. Ainsi, même si Catl »playWith(Animal other)La méthode ne fonctionnerait que lorsque otherétait un Cat, le Zoodevrait avoir la possibilité de lui passer un élément d'un Animal[], donc son type de paramètre devrait être Animalplutôt que Cat.

Dans les cas où la descente est légitimement inévitable, il faut l'utiliser sans scrupule. La question clé est de déterminer quand on peut sensiblement éviter la descente, et de l'éviter quand c'est raisonnablement possible.

supercat
la source
Dans le cas des chaînes, vous devriez plutôt avoir une méthode Object.equalToString(String string). Ensuite, vous n'avez boolean String.equal(Object object) { return object.equalStoString(this); }donc pas besoin de downcast: vous pouvez utiliser la répartition dynamique.
Giorgio
@Giorgio: La répartition dynamique a ses utilités, mais elle est généralement encore pire que le downcasting.
supercat
façon répartition dynamique généralement pire que la descente? je pense que c'est l'inverse
Bryan Chen
@BryanChen: Cela dépend de votre terminologie. Je ne pense pas Objectavoir de equalStoStringméthode virtuelle, et je dois admettre que je ne sais pas comment l'exemple cité fonctionnerait même en Java, mais en C #, la répartition dynamique (par opposition à la répartition virtuelle) signifierait que le compilateur a essentiellement pour effectuer une recherche de nom basée sur la réflexion la première fois qu'une méthode est utilisée sur une classe, qui est distincte de la répartition virtuelle (qui effectue simplement un appel via un emplacement dans la table de méthode virtuelle qui doit contenir une adresse de méthode valide).
supercat
Du point de vue de la modélisation, je préférerais la répartition dynamique en général. C'est également la manière orientée objet de sélectionner une procédure en fonction du type de ses paramètres d'entrée.
Giorgio
1

Dans une implémentation d'une méthode abstraite qui accepte les animaux, je voudrais utiliser la méthode speak ().

Vous avez quelques choix:

  • Utilisez la réflexion pour appeler speaksi elle existe. Avantage: pas de dépendance Human. Inconvénient: il existe désormais une dépendance cachée sur le nom "speak".

  • Introduisez une nouvelle interface Speakeret abaissez l'interface. Ceci est plus flexible qu'en fonction d'un type de béton spécifique. Il présente l'inconvénient que vous devez modifier Humanpour l'implémenter Speaker. Cela ne fonctionnera pas si vous ne pouvez pas modifierHuman

  • Descendu à Human. Cela présente l'inconvénient que vous devrez modifier le code chaque fois que vous voulez qu'une autre sous-classe parle. Idéalement, vous souhaitez étendre les applications en ajoutant du code sans revenir en arrière et modifier l'ancien code.

Kevin Cline
la source