什么时候可以使用instanceof?

时间:2012-01-05 20:26:47

标签: java polymorphism instanceof flawed-concept

我正在设计一款游戏。在游戏中,各种游戏对象根据他们需要做的事情扩展不同的接口(和一个抽象类),并传递给处理程序,处理程序按照定义的时间间隔处理具有特定接口的项目(它们实际上将所有工作分散开来以一种简洁的方式确保始终处理输入/视频/等。)

无论如何,其中一些对象扩展了抽象类Collider并传递给CollisionHandler。 Collider类和处理程序负责碰撞中涉及的所有技术,并且只要求对象实现collidesWith(Collider c)函数,并根据它碰撞的内容进行修改。

许多不同类的对象将相互碰撞,并且根据它们碰撞的对象类型及其特定属性将采取非常不同的行为。

完美的解决方案似乎是像这样使用instanceof:

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

这实际上似乎是一个合法的地方。 I just get this bad feeling. Like how if a goto made sense in some particular situation.设计是否对任何人都有根本感觉?如果是这样,你会建议做什么来实现相同的行为。

8 个答案:

答案 0 :(得分:15)

传统的答案是,使用访问者模式。您添加了一个新界面

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

和方法,

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

游戏中的每个对象都实现了visit方法,您需要的每个操作都会实现Visitor接口。因此,只要操作visits一个对象,就会强制执行与该对象关联的操作。

您当然可以更详细,而不是依赖于方法调度机制。

更新:返回问题标题,使用instanceof始终可以接受。这是你的代码,它是你的语言。问题是,如果你的代码中有很多地方使用instanceof,你迟早会不可避免地错过一个地方,这样你的代码就会在没有编译器的情况下无声地失败来帮助你。访问者会在编码过程中让您的生活更加痛苦,因为它会迫使您在每次更改界面时实施界面。但从好的方面来说,你不会错过这种情况。

更新2:请阅读以下讨论。访客当然会束缚你,只要你有十几种类型,你就会感到受到约束。此外,如果您需要发送事件,例如冲突,基于两个或更多对象的类型,没有访问者会帮助您(也不是instanceof):您需要实现自己的碰撞后果表会将你的类型组合映射到一个对象(我会说Strategy,但恐怕讨论会增长十倍),这会知道如何处理这个特殊的碰撞。

强制性的Stroustrup引用:“没有替代品:智力;经验;品味;努力工作。”

答案 1 :(得分:7)

通常建议访客班级。使用访问者,您可以实施访问方法:

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

但实际上这相当于:

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

这两个都有缺点。

  1. 您必须实现所有这些,即使您的对象的响应在每种情况下都相同
  2. 如果添加一种新类型的对象与之碰撞,则必须编写一种方法来实现与每个对象的碰撞。
  3. 如果系统中的一个对象对27种碰撞器的反应不同,但其他一切反应方式相同,那么仍然需要为每个类编写27个访问者方法
  4. 有时最简单的方法是:

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

    它的优势在于它能够了解对象如何对与该对象相关的事物作出反应。它还意味着如果Balloon有子类RedBalloon,BlueBalloon等,我们不必考虑这一点,就像访问模式一样。

    不使用instanceof的传统观点是它不是OO,你应该使用多态。不过您可能对这篇文章感兴趣:When Polymorphism Fails, by Steve Yegge 解释了为什么instanceof有时候是正确的答案

答案 2 :(得分:4)

奇怪的是,没有人发布过“未破坏”的访客模式实现。 并且没有破坏我的意思是不依赖于访客的副作用。要做到这一点,我们需要访问者返回一些结果(让我们称之为R):

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

接下来,我们修改accept以接受新访问者:

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

对撞机的具体实现必须调用正确的visit方法,就像这样(我假设Food implements Collider,但这不是必需的):

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

现在要实现碰撞,我们可以这样做:

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()));
}

您可以看到编译器可以帮助您找到忘记指定行为的所有位置。这不像一些人声称的那样是一种责任,但是因为你总是可以通过使用默认实现来禁用它,所以这是有利的:

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; }
}

您还需要某种方法来重用(Food,SomeNpc)和(SomeNpc,Food)冲突的代码,但这超出了这个问题的范围。

如果你认为这太冗长了 - 那是因为它是。在具有模式匹配的语言中,这可以在几行中完成(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

答案 3 :(得分:2)

您可以在此处使用访问者模式,其中Collider的子类将为其可能遇到的每种类型的冲突实现单独的方法。所以你的方法可能变成:

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 );
        }
    }
}

你明白了。模型中的奇怪之处在于Collider基类必须知道所有潜在的子类才能为该类型定义方法。部分原因与访客模式的问题有关,但也是因为Collider被合并到访客中。我建议寻找访客和对撞机的分离,这样您就可以定义碰撞发生时的行为方式。这对你的对手来说意味着他们可以根据内部状态改变他们对碰撞的行为方式。比如普通模式,隐藏或死亡,它们都是无懈可击的。查看客户端代码可能是:

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

答案 4 :(得分:2)

在我看来,上面描述的内容是instanceof的合法使用,并且可能比使用Visitor系统更具可读性,如果每个类仅与上面列出的其他几个类交互。

问题在于, potential 可以为20种敌人中的每一种变成else-if s的页面。但是使用instanceof,你可以通过一些标准的多态性来避免这种情况(检查一个Enemy类并对待所有敌人,即使他们是OrcDalek s或什么)。

访客模式使得这样做变得更加困难。最可行的解决方案是拥有一个所有游戏对象派生的顶级类,并在该类中为其所有子类定义collideWith()方法 - 但是每个类的默认实现只调用collideWith()超类型:

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

答案 5 :(得分:0)

这是一个使用大多数人都会感到畏缩的例子。这是一个有用的链接,可以为您提供一些见解:http://www.javapractices.com/topic/TopicAction.do?Id=31

相反,Collider应该有一些每个子类都会覆盖的collide()方法。

答案 6 :(得分:0)

有几种方法可以解决这个问题:

  • 对撞机类型的枚举,略显丑陋但无法防范
  • 使用处理程序进行类调度:即Map<Class, CollisionHandler>之类的东西,你从传递的对撞机类(或枚举类型)中选择CollisionHandler并调用processCollision(source)。每个Colllider类都有自己的处理程序映射。
  • 基于上述内容,您还可以创建碰撞器类型和编写处理程序的组合,对于创建新处理程序所需的每种新碰撞类型,类似Map<Pair<ColliderType>, CollisionHandler>。优点是这样的声明可以是外部的(依赖注入),因此新的NPC / Object可以与Collision处理程序一起添加。

无论如何,首先要确保它以您感觉最舒适的方式工作,然后您可以重构它。

答案 7 :(得分:0)

这可能需要考虑。使用instanceof可能很有用,但考虑访问者想法的另一种方法是构建一个Factory类,该类检查相互字段以确定访问者的类型。

您仍然需要为每个实现构建新类,但它们在一个定义方法的抽象类中路由

 public abstract class Qualifier{
 public abstract String type(); //...
 }

/**
 * QualifierFactory method.
 */
public static Qualifier getQualifier(Item sp, Config cf) {
    String type = cf.type();
    if (XQualifier.type().equals(type))
        return new XQualifier(sp, cf);
    else if (NQualifier.type().equals(type))
        return new NQualifier(sp, cf);
    else if (Tools.isNone(type) || NoneQualifier.type().equals(type))
        return new NoneQualifier(sp);
    else if (CSQualifier.type().equals(type))
        return new CSQualifier(sp, cf);//...
 }

返回的对象可以是操作。