避免使用“instanceof”的方法

10

我在努力想方设法避免在我的代码中使用 instanceof()。 这个虚构的例子有点能够体现这个问题。

Class Meat extends Food;

Class Plant extends Food;

Class Animal;

Class Herbivore extends Animal
{
    void eat( Plant food);
}

Class Carnivore extends Animal
{
    void eat( Meat food);
}

Class Omnivore extends Animal
{
    void eat(Food food);
}

Class Zoo
{
    List<Animals> animals;

    void receiveFood( Food food)
    {
        // only feed Plants to Herbivores and Meat to Carnivores
        // feed either to Omnivores
    }
}

草食动物只对植物感兴趣,肉食动物只对肉感兴趣,而杂食动物则同时对两者感兴趣。当动物园收到食物时,仅尝试将食物喂给吃这种类型的食物的动物是有意义的。

我想到了几个解决方案,但似乎都要在某个地方使用 instanceof(),我的各种重构似乎只是将其移动到别处。

(1) 我可以在 Animal 中实现 eat( Food food),每个子类都可以选择忽略它不吃的食物,但这样效率低下,每个 Animal 子类都需要使用 instanceof() 来测试食物的类型。

(2) 我可以根据它们所吃食物的类型在动物园中保留三个动物集合,但仍然需要使用 instanceOf() 来测试食物的类型,以查看要将其喂给哪个集合。至少这样会更有效率,因为我不会将食物喂给不吃它的动物。

我想到了其他一些方法,但它们似乎只是将 instanceof() 传递给别的地方。

有什么建议吗?或者(至少)这种使用 instanceof() 的方法可行吗?


也许你应该研究一下通用方法?这样,你可以指定一个动物有一个吃(T extends 食物)的方法,而草食动物将被归类为植物?大致就是这样。 - Thorn G
1
可能与此问题 Is This Use of the "instanceof" Operator Considered Bad Design? 重复。 - Tomasz Nurkiewicz
访问者模式的理想候选者 - bluesman
嗯...访问者模式的解决方案是否会像上面(1)一样有类似的问题?当收到食物时,即使动物不吃这种类型的食物,也必须访问动物?一个访问者模式的例子会很棒 - 我从来不确定在某些情况下是否完全理解该模式... - HolySamosa
超类Animal的接口是什么?它是否处理<T extends Food>?如果是,那么整个动物和食物的抽象不会违反Liskov替换原则吗?也许面向对象编程并不总是自然分类的最佳抽象... - nansen
显示剩余3条评论
5个回答

12

访问者设计模式可以解决你的问题。以下是代码:

public abstract class Animal {
  public abstract void accept(AnimalVisitor visitor);
}

public interface AnimalVisitor {
  public void visit(Omnivore omnivore);
  public void visit(Herbivore herbivore);
  public void visit(Carnivore carnivore);
}

public class Carnivore extends Animal {
  @Override
  public void accept(AnimalVisitor visitor) {
    visitor.visit(this);
  }

  public void eat(Meat meat) {
    System.out.println("Carnivore eating Meat...");
  }
}

public class Herbivore extends Animal {
  @Override
  public void accept(AnimalVisitor visitor) {
    visitor.visit(this);
  }

  public void eat(Plant plant) {
    System.out.println("Herbivore eating Plant...");
  }
}

public class Omnivore extends Animal {
  @Override
  public void accept(AnimalVisitor visitor) {
    visitor.visit(this);
  }

  public void eat(Food food) {
    System.out.println("Omnivore eating " + food.getClass().getSimpleName() + "...");
  }
}

public abstract class Food implements AnimalVisitor {
  public void visit(Omnivore omnivore) {
    omnivore.eat(this);
  }
}

public class Meat extends Food {
  @Override
  public void visit(Carnivore carnivore) {
    carnivore.eat(this);
  }

   @Override
  public void visit(Herbivore herbivore) {
    // do nothing
  }
}

public class Plant extends Food {
   @Override
  public void visit(Carnivore carnivore) {
    // do nothing
  }

   @Override
  public void visit(Herbivore herbivore) {
    herbivore.eat(this);
  }
}

public class Zoo {
  private List<Animal> animals = new ArrayList<Animal>();

  public void addAnimal(Animal animal) {
    animals.add(animal);
  }

  public void receiveFood(Food food) {
    for (Animal animal : animals) {
      animal.accept(food);
    }
  }

  public static void main(String[] args) {
    Zoo zoo = new Zoo();
    zoo.addAnimal(new Herbivore());
    zoo.addAnimal(new Carnivore());
    zoo.addAnimal(new Omnivore());

    zoo.receiveFood(new Plant());
    zoo.receiveFood(new Meat());
  }
}

运行 Zoo 演示会打印

Herbivore eating Plant...
Omnivore eating Plant...
Carnivore eating Meat...
Omnivore eating Meat...

1
通过从类Food中提取接口AnimalVisitor来重构代码。 - chris
3
虽然我内心的书呆子喜欢看到Visitor模式的代码实现(太棒了!),但该模式是为了在不破坏Visited类的情况下添加新的访问者,而不是避免使用instanceof(这正是问题所在)。属性解决方案(如isMeat()等)较少复杂,更加朴素。 - Fuhrmanator

5

在您的情况下,如果对象的使用者必须了解该对象的某些信息(例如它是否是肉类),请在基类中包含一个属性isMeat(),并让具体的子类覆盖基类方法的实现以返回适当的值。

将该知识保留在类本身中,而不是在类的使用者中。


那么,在基类中放置isMeat()方法比使用instanceof更好的原因是,对于后者,关于(可能的)实现的知识正在传递给客户端类?我想这将归结为权衡向客户端传输的知识和在基对象中添加isXXX方法的混乱。我可以看到不同的情况可能会倾向于任何一方。 - HolySamosa

4

当使用多个自定义类相互交互时,一个简单的解决方案是创建isFood()、isAnimal()、isCarnivore()等方法,根据它们所在的类返回布尔值。虽然不太美观,但百分之百能完成工作。


你们基本上同时给出了相同的答案! - HolySamosa

1

针对我的评论,我会尝试使用泛型来帮助我解决这个问题:

interface Animal<T extends Food> {
    void eat(T food);
}

class Herbivore extends Animal<Plant> {
    void eat(Plant food) { foo(); }
}

class Carnivore extends Animal<Meat> {
    void eat(Meat food) { bar(); }
}

请注意,这仍然无法解决遍历 FoodAnimal 列表并仅向每个动物发送适当食物的问题 - 我没有看到一种方法可以在不进行显式的 instanceof 风格检查的情况下完成。但是,它确实允许您更具体地指定子类接受什么。

请参考以下链接:https://dev59.com/T2HVa4cB1Zd3GeqPqMzz#9807170此问题在上面被提及,但它确实讨论了一些泛型使用中潜在的缺陷。尽管如此,我之前也一直在考虑这样的解决方案。 - HolySamosa

0
另一种解决方案是维护两个列表:一个是草食动物,一个是肉食动物。

这是我想到的一件事,但似乎我仍然需要使用instanceof()来测试食物的类型,以决定将食物传递给哪个列表。 - HolySamosa
好的,你还需要两个渠道来获取食物:一个只提供植物,另一个只提供肉类。 - Puce

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接