28 votes

Quand est-il acceptable d'utiliser instanceof ?

Je suis en train de concevoir un jeu. Dans le jeu, les différents objets du jeu étendent différentes interfaces (et une classe abstraite) en fonction de ce qu'ils doivent faire, et sont transmis à des gestionnaires qui s'occupent des éléments avec une interface spécifique à des intervalles définis (ils répartissent en fait tout leur travail d'une manière soignée pour s'assurer que les entrées/vidéos/etc sont toujours traitées).

Quoi qu'il en soit, certains de ces objets étendent la classe abstraite Collider et sont transmis à un CollisionHandler. La classe Collider et le gestionnaire s'occupent de tout ce qui est techniquement impliqué dans la collision, et demandent simplement qu'un objet implémente une fonction collidesWith(Collider c), et se modifie en fonction de ce avec quoi il est entré en collision.

Des objets de nombreuses classes différentes entreront en collision les uns avec les autres et se comporteront très différemment selon le type d'objet avec lequel ils sont entrés en collision et ses attributs spécifiques.

La solution parfaite semble être d'utiliser instanceof comme ceci :

class SomeNPC extends Collider{
    collidesWith(Collider c){
        if(c instanceof enemy){
            Fight it or run away depending on your attributes and theirs.
        }
        else if(c instanceof food){
            Eat it, but only if it's yellow.
        }
        else if(c instanceof BeamOfLight){
            Try to move towards its source.
        }
    }
}

Ça semble être un endroit légitime pour une instance. J'ai juste ce mauvais pressentiment. Comme si un goto avait un sens dans une situation particulière. Quelqu'un a-t-il l'impression que le design est fondamentalement différent ? Si oui, que recommanderiez-vous de faire pour obtenir le même comportement ?

15voto

alf Points 5130

La réponse traditionnelle est d'utiliser un Visiteur modèle. Vous ajoutez une nouvelle interface,

interface Visitor {
     void visit(Enemy e);
     void visit(Food f);
     void visit(BeanOfLight bol);
}

et une méthode,

public void visit(Visitor v) {
    visitor.visit(this);
}

Chaque objet de votre jeu implémente un visit et chaque action dont vous avez besoin implémente une méthode Visitor l'interface. Ainsi, dès que l'action visits un objet, il est obligé d'effectuer une action associée à cet objet.

Vous pouvez bien sûr être plus détaillé et ne pas vous appuyer sur le mécanisme de répartition des méthodes.

Mise à jour : pour en revenir à l'en-tête de la question, il est toujours acceptable d'utiliser instanceof . C'est votre code, c'est votre langage à utiliser. Le problème est que, s'il y a de nombreux endroits dans votre code où vous utilisez instanceof vous en manquerez inévitablement une tôt ou tard, de sorte que votre code échouera silencieusement sans que le compilateur soit là pour vous aider. Le visiteur vous rendra la vie plus pénible pendant le codage, car il vous obligera à implémenter l'interface chaque fois que vous la modifiez, partout. Mais d'un autre côté, vous ne manquerez aucun cas de cette façon.

Mise à jour 2 : Veuillez lire la discussion ci-dessous. Visitor va bien sûr vous lier et vous vous sentirez contraint par lui dès que vous aurez plus d'une douzaine de types. De plus, si vous avez besoin de distribuer des événements, par exemple des collisions, en vous basant sur les types de deux ou plus objets, aucun visiteur ne vous aidera (aucun instanceof soit) : vous devrez mettre en œuvre votre propre système de gestion de l'information. tableau des conséquences de la collision qui mettrait en correspondance vos combinaisons de types avec un objet (je dirais que Strategy mais je crains que la discussion ne soit décuplée) qui sauraient comment traiter avec les cette collision particulière.

Une citation obligatoire de Stroustrup : "Il n'y a pas de substitut à : l'intelligence, l'expérience, le goût et le travail."

7voto

DJClayworth Points 11288

La classe des visiteurs est souvent recommandée. Avec Visitor, vous implémentez une méthode de visite :

interface Visitor {
 void visit(Enemy e);
 void visit(Food f);
 void visit(BeanOfLight bol);
}

Mais ceci est en fait équivalent à :

class SomeNPC extends Collider {
  public void collidesWith( Enemy enemy )
  public void collidesWith( Food food )
  public void collidesWith( Bullet bullet )
}

Les deux présentent des inconvénients. 1) Vous devez toutes les implémenter, même si la réponse de votre objet est la même dans chaque cas. 2) Si vous ajoutez un nouveau type d'objet avec lequel entrer en collision, vous devez écrire une méthode pour implémenter la collision avec cet objet. chaque objet . 3) Si un objet dans votre système réagit différemment à 27 types de collisionneurs, mais que tout le reste réagit de la même manière, vous il faut toujours écrire 27 méthodes de visiteurs pour chaque classe .

Parfois, le moyen le plus simple est de faire :

collidesWith(Object o) {
  if (o instanceof Balloon) {
    // bounce
  } else {
    //splat
  }

Cela a l'avantage de conserver la connaissance de la façon dont un objet réagit aux choses qu'il touche avec cet objet. Cela signifie également que si Balloon a des sous-classes RedBalloon, BlueBalloon etc. nous n'avons pas à en tenir compte, comme nous le ferions avec le modèle visiteur.

L'argument traditionnel pour ne pas utiliser instanceof est que ce n'est pas OO, et que vous devriez utiliser le polymorphisme. Cependant, cet article pourrait vous intéresser : Quand le polymorphisme échoue, par Steve Yegge dont explique pourquoi instanceof est parfois la bonne réponse .

5voto

Rotsor Points 6987

Il est étrange que personne n'ait encore publié une implémentation du modèle de visiteur "non cassé". Et par "pas cassé" je veux dire ne pas dépendre des effets secondaires du visiteur. Pour ce faire, nous avons besoin que nos visiteurs retournent un résultat (appelons-le R ) :

interface ColliderVisitor<R> {
     R visit(Enemy e);
     R visit(Food f);
     R visit(BeanOfLight bol);
     R visit(SomeNpc npc);
}

Ensuite, nous modifions accept pour accepter les nouveaux visiteurs :

interface Collider {
    <R> R accept(ColliderVisitor<R> visitor);
}

les implémentations concrètes de collider devront appeler la fonction appropriée visit comme ceci (je suppose que la méthode Food implements Collider mais ce n'est pas nécessaire) :

class Food implements Collider {
    @Override
    <R> R accept(ColliderVisitor<R> visitor) {
        return visitor.visit(this);
    }
}

Maintenant, pour mettre en œuvre les collisions, nous pouvons faire quelque chose comme ceci :

class SomeNpcCollisionVisitor implements ColliderVisitor<Action> {
    SomeNpcCollisionVisitor(SomeNpc me) { this.me = me; }
    SomeNpc me;
    @Override
    Action visit(Enemy they) { 
        return fightItOrRunAway(me.attributes(), they.attributes());
    }
    @Override
    Action visit(Food f) {
        return f.colour()==YELLOW ? eat(f) : doNothing;
    }
    @Override
    Action visit(BeamOfLight l) {
        return moveTowards(l.source());
    }
    @Override
    Action visit(SomeNpc otherNpc) {
       // What to do here? You did not say! The compiler will catch this thankfully.
    }
}

class CollisionVisitor implements 
        ColliderVisitor<ColliderVisitor<Action>> { // currying anyone?

    @Override
    Action visit(Enemy they) { 
        return new EnemyCollisionVisitor(they); // what to do here?
    }
    @Override
    Action visit(Food f) {
        return new FoodCollisionVisitor(f); // what to do here?
    }
    @Override
    Action visit(BeamOfLight l) {
        return new BeamOfLightCollisionVisitor(l); // what to do here?
    }
    @Override
    Action visit(SomeNpc otherNpc) {
       return new SomeNpcCollisionVisitor(otherNpc);
    }
}

Action collide(Collider a, Collider b) {
    return b.accept(a.accept(new CollisionVisitor()));
}

Vous pouvez voir que le compilateur peut vous aider à trouver tous les endroits où vous avez oublié de spécifier le comportement. Ce n'est pas un handicap comme certains le prétendent, mais un avantage car vous pouvez toujours le désactiver en utilisant une implémentation par défaut :

class ColliderVisitorWithDefault<R> implements ColliderVisitor {
    final R def;
    ColliderVisitorWithDefault(R def) { this.def = def; }
    R visit(Enemy e) { return def; }
    R visit(Food f) { return def; }
    R visit(BeanOfLight bol) { return def; }
    R visit(SomeNpc npc) { return def; }
}

Vous devrez également trouver un moyen de réutiliser le code pour les collisions de (Food, SomeNpc) et (SomeNpc, Food), mais cela sort du cadre de cette question.

Si vous pensez que c'est trop verbeux, c'est parce que ça l'est. Dans les langages qui proposent le filtrage de motifs, cela peut être fait en plusieurs lignes (exemple Haskell) :

data Collider = 
    Enemy <fields of enemy>
  | Food <fields of food>
  | BeanOfLight <fields>
  | SomeNpc <fields>

collide (SomeNpc npc) (Food f) = if colour f == YELLOW then eat npc f else doNothing
collide (SomeNpc npc) (Enemy e) = fightOrRunAway npc (npcAttributes npc) (enemyAttributes e)
collide (SomeNpc npc) (BeamOfLight bol) = moveTowards (bolSource bol)
collide _ _ = undefined -- here you can put some default behaviour

2voto

chubbsondubs Points 16075

Vous pouvez utiliser ici un modèle visiteur dans lequel les sous-classes de Collider implémentent une méthode distincte pour chaque type de collision qu'elles peuvent rencontrer. Ainsi, votre méthode pourrait se transformer en :

class SomeNPC extends Collider {

    public void collidesWith( Enemy enemy ) {}

    public void collidesWith( Food food ) {}

    public void collidesWith( Bullet bullet ) {}

    public void doCollision( Collider c ) {
        if( c.overlaps( this ) ) {
            c.collidesWith( this );
        }
    }
}

Vous voyez le genre. Ce qui est étrange dans votre modèle, c'est qu'une classe de base Collider doit connaître toutes les sous-classes potentielles afin de définir une méthode pour ce type. Cela a en partie à voir avec le problème du pattern visitor, mais c'est aussi parce que Collider est combiné dans Visitor. Je suggérerais de chercher une séparation entre le visiteur et le collisionneur afin de pouvoir définir comment vous voulez vous comporter lorsque des collisions se produisent. Ce que cela signifie pour vos colliders, c'est qu'ils peuvent changer leur comportement lors d'une collision en fonction de leur état interne. Disons qu'ils sont invulnérables par rapport au mode normal, cachés ou morts. En regardant le code client, cela pourrait être :

collider1.getCollisionVisitor().doCollision( collider2 );
collider2.getCollisionVisitor().doCollision( collider1 );

2voto

Russell Zahniser Points 11176

À mon avis, ce que vous avez esquissé ci-dessus est une utilisation légitime de instanceof, et peut être plus lisible que l'utilisation d'un système Visitor, si chaque classe n'interagit qu'avec quelques autres classes comme vu ci-dessus.

Le problème est qu'il a le potentiel pour se transformer en pages de else-if pour chacun des vingt types d'ennemis. Mais avec instanceof, vous pouvez éviter cela avec une utilisation standard du polymorphisme (vérifier une Enemy et de traiter tous les ennemis de la même manière, même s'ils sont Orc ou Dalek ou autre).

Le modèle Visiteur rend la chose beaucoup plus difficile. La solution la plus viable serait d'avoir une classe de niveau supérieur dont tous les objets du jeu dérivent, et de définir des méthodes collideWith() dans cette classe pour toutes ses sous-classes - mais de faire en sorte que l'implémentation par défaut de chacune d'entre elles appelle simplement la méthode collideWith() de la classe supérieure :

class GameObject {
   void collideWith(Orc orc) {
      collideWith((Enemy)orc);
   }

   void collideWith(Enemy enemy) {
      collideWith((GameObject)enemy);
   }

   ...

   void collideWith(GameObject object) { }
}

class SomeNPC extends GameObject {
   void collideWith(Orc orc) {
      // Handle special case of colliding with an orc
   }

   // No need to implement all the other handlers,
   // since the default behavior works fine.
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X