如何避免在 MVC 架构中使用 instanceof

How to avoid using instanceof in MVC Architecture

我正在 Java 开发一款像 Pacman 这样的游戏,我目前正面临这些我不喜欢的代码味道。

让我解释一下我的想法:我的游戏是建立在MVC架构上的。在视图模块上,我寻找模型上的每个电源,并将其添加到要在 GUI 上绘制的元素列表中。问题是我有 3 种使用界面的道具,所以当我添加道具时,我需要检查它们是什么类型,然后添加相应的视图。让我展示一些代码,这样我会更清楚:

for (PowerUp powerUp : level.getPowerUps()) {
   if (powerUp instanceof Invincibility) elements.add(new InvincibilityView(powerUp.getPosition()));
   if (powerUp instanceof Freeze) elements.add(new FreezeView(powerUp.getPosition()));
   if (powerUp instanceof Fright) elements.add(new FrightView(powerUp.getPosition()));
}

第二种气味与鬼魂有关,我的游戏有一个状态模式,如果状态改变,它应该改变鬼魂的颜色。例如,如果状态是 Frightened 我希望鬼魂是橙色的,如果状态是 Frozen 我希望鬼魂是蓝色的,等等

因此,在创建 Ghost View 时,我通过参数传递状态,并检查(再次使用 instanceof)当前处于什么状态。让我再展示一些代码:

public void draw(graphics) {
    String color = "#FF0000";
    if (state instanceof Invincible) color = "#585858";
    if (state instanceof Frozen) color = "#00FFFF";
    if (state instanceof Frightened) color = "#FF7F50";
    // draw ghost
}

我的问题是如何在不更改模型模块的情况下避免使用 instanceof

注意安全!

看来您要找的是抽象工厂模式。有几种方法可以实现它;这可能是您需要的最佳匹配:

interface ViewFactory<V extends View> {
    boolean canHandlePowerUp(PowerUp target);
    V createView(PowerUp target);
}

class FreezeViewFactory extends ViewFactory<FreezeView> { ... }

...

List<ViewFactory<? extends V>> factories;

View v = factories.stream()
    .filter(f -> f.canHandlePowerUp(p))
    .findFirst()
    .map(f -> createView(p))
    .orElseThrow(() -> new IllegalStateException("no ViewFactory found for " + p);

当您旨在隔离关注点时,这实际上是一个需要解决的非常普遍的问题。例如,如果渲染没有与模型分离,那么您将有一个 PowerUp.render() 方法,这样他们就可以渲染自己并结束工作。

但是,您会遇到另一个问题,即各种问题最终会集中在一个 class 中。这是一种权衡,您要么直接在对象上实现操作,要么提取这些行为,但随后必须以某种方式执行类型匹配。

抽象工厂在这里可能很有趣,因为它减少了您必须做出的基于类型的决定的数量,但是如果您需要做出其他基于类型的决定(例如不同的开机声音),您将必须再次进行类型匹配,没有办法。

也就是说,您可以使用 Visitor Pattern.

实现可重用的类型安全机制来进行类型匹配

例如(为简洁起见省略了访问修饰符)

interface PowerUpVisitor {
    visit(Invincibility powerUp);
    visit(Freeze powerUp);
    visit(Fright powerUp);
}

interface PowerUp {
    accept(PowerUpVisitor visitor);
    ...
}

class Freeze implements PowerUp {
    accept(PowerUpVisitor visitor) { visitor.visit(this); }
}

class PlayPowerUpSound implements PowerUpVisitor {
    visit(Invincibility powerUp) { playInvicibilitySound(); }
    ...
}

class RenderPowerUp implements PowerUpVisitor {
    visit(Invincibility powerUp) { renderInvisibility(); }
    ...
}

//In practice you would most likely reuse the same visitor instances
somePowerUp.accept(new PlayPowerUpSound()); //to play sound
somePowerUp.accept(new RenderPowerUp()); //to render

这种模式的主要优点是现在编译器会告诉您是否忘记处理特定类型的加电,这与 instanceof 检查不同。

与其他提议的抽象工厂解决方案一样,使用基于动态类型的解析,您必须使用反射实现基于运行时的验证,以确保所有类型都存在一个工厂。

访问者模式通常会自然而然地出现在抽象语法树等层次结构中,但它在这里同样有用。

如果您同意降低隔离的严格程度,还有其他方法。例如,您可以为 PowerUp 可能具有的各种行为引入专用接口,例如 IPlaySoundIHaveAView 等,并直接在通电中实现这些操作。边界现在完全由界面绘制,但它们仍然存在。

最后,您还可以要求它们各自的渲染器或声音播放器的通电,而不是直接由对象执行操作。我想我更喜欢这里的访客,但是有很多方法可以剥猫皮,具体取决于您期望的复杂程度等等。这完全是权衡取舍!