摆脱 `instanceof`

8
在我正在编写的基于精灵的游戏中,2D网格中的每个字段都包含一堆精灵。大多数情况下,顶部的那个是最重要的。
在游戏的规则模块中,我有很多类似这样的代码:
public boolean isGameWon(Board board) {
    for (Point point : board.getTargetPoints())
        if(!(board.getTopSpriteAt(point) instanceof Box))
            return false;
    return true;
}

更新:如果每个“Target”上面都有一个“Box”,则//Do something将计数。我不明白如何只通过在Sprite中添加doSomething()来完成这项任务,除非doSomething()返回1(如果精灵是箱子);否则返回0(这与instanceof完全相同)。
我知道instanceof被认为是有害的,因为它破坏了面向对象编程的思想。
然而,在我的情况下,我不确定如何修复代码。以下是我想到的一些想法:
- 我不认为只是在“Sprite”接口中简单添加一个isABox()方法会使问题变得更好。 - 如果“Box”是一个接口,那么其他类是否可以获得相同的特权? - 我是否应该尝试像访问者模式那样使用模式匹配/双重调度之类的花哨技巧? - 规则模块是否可以与类型密切合作,因为它本来就应该了解它们的语义? - 规则模块策略模式的整个思想是否有缺陷? - 将规则构建到Sprites中是没有意义的,因为当添加新类型时,所有Sprites都必须更改。
希望你已经尝试过类似的事情,并能指导我正确的方向。

你有没有考虑在接口Sprite中添加一个doSomething()方法,并让每个实现类提供其实现? - A4L
1
考虑到在此处使用基类引用的网格不合适,因为您想特别处理特定的派生类。基本上,您正在违反LSP - Oliver Charlesworth
@OliCharlesworth 我认为你说得很对,我只是还没有找到其他的方法? - Thomas Ahle
也许这只是一个 https://sites.google.com/site/steveyegge2/when-polymorphism-fails 的例子,我应该庆幸这种味道被隔离在Rules类中... - Thomas Ahle
1
@TomasNarros 但是假设我有10种类型:“Box,Cat,Bottle,Human,Target...”它们都以某种方式进行交互。那么我将添加110个新的isType方法。 - Thomas Ahle
显示剩余3条评论
11个回答

8
使用多态性
class Sprite {
    ..
    someMethod(){
    //do sprite
    }
    ..
}

class Box extends Sprite {
    ..
    @Overrides
    someMethod(){
    //do box
    }
    ..
}

所以,在你的示例中,只需要调用sprite.someMethod()。

现在怎么样?我不明白 someMethod() 怎么可能不是 int isABox() - Thomas Ahle
cnt += sprite.size();getCount()getSize() 怎么样?对于大多数精灵,size 返回 0,而对于 Box,返回 1。 - Peter Lawrey
你可以将与cnt变量相关的逻辑封装到某个类中,并将该类的实例传递给someMethod()。 - Artem
@PeterLawrey,你会在那个方法的javadoc中写什么?“如果对象是一个盒子,则getSize()返回1,否则返回0”。 - Thomas Ahle
@Artem:如果你的意思是我已经将cnt和逻辑封装到Rules类中,那么是的,我已经这样做了。 - Thomas Ahle
显示剩余4条评论

5

Instanceof: 几乎总是有害的

我查看了您帖子中的所有答案,并尝试理解您在做什么。最终我得出结论: instanceof 恰好是您想要的,而您原始的代码示例也是正确的。

您澄清了:

  • You are not violating the Liskov substitution principle since none of the Box code invalidates the Sprite code.

  • You are not forking the code with the response to instanceof. This is why people say instanceof is bad; because people do this:

    if(shape instanceof Circle) {
        area = Circle(shape).circleArea();
    } else if(shape instanceof Square) {
        area = Square(shape).squareArea();
    } else if(shape instanceof Triangle) {
        area = Triangle(shape).triangleArea();
    }
    

    This is the reason why people avoid instanceof. But this is not what you are doing.

  • There is a one-to-one relationship between Box and winning the game (no other Sprites can win the game). So you are not in need of an additional "winner" sprite abstraction (because Boxes == Winners).

  • You are simply checking the board to make sure that each top item is a Box. This is exactly what instanceof is designed to do.

每个人的答案(包括我的答案)都为检查Sprite是否为Box添加了一个额外的机制。但是它们没有增加任何健壮性。实际上,您正在使用语言已经提供的功能,并在自己的代码中重新实现它们。
Tomas Narros认为,您应在代码中区分“语义类型”和“java类型”。我不同意。您已经确定了一个java类型Box,它是Sprite的子类。因此,您已经拥有了所有需要的信息。
在我看来,拥有第二个独立机制,也报告“I am a Box”,违反了DRY(不要重复自己)原则。这意味着不要为相同的信息维护两个独立的来源。您现在必须维护一个枚举和一个类结构。
所谓的“好处”是能够在完全满足目的的关键字周围旋转,并且在使用更有害的方式时是有害的。
黄金法则是“用你的头脑”。不要将规则视为硬性事实。质疑它们,了解它们存在的原因,并在适当时弯曲它们。

1
非常好的答案。我真的很感激。我要做的是尝试语法/语义分离,看看是否有足够的差异出现,以免违反DRY原则。或者如果我发现自己一遍又一遍地困惑于使用哪种类型。 - Thomas Ahle
+1 感谢您的回答 - 我遇到了类似的问题,您的解释帮了我很多。 - AgentKnopf

3

基本的重载是这里应该采取的方法。Sprite类层次结构应该知道如何做什么,如:

interface Sprite {
    boolean isCountable();
}


class MyOtherSprite implements Sprite {
    boolean isCountable() {
        return false;
    }
 }

 class Box implements Sprite {
    boolean isCountable() {
        return true;
    }
}

int count = 0;
for (Point point : board.getTargetPoints()) {
    Sprite sprite = board.getTopSpriteAt(point);
    count += sprite.isCountable() ? 1 : 0;
}

编辑: 你对问题的编辑并没有从根本上改变问题。你现在拥有的是一些只适用于Box的逻辑。同样地,将特定的逻辑封装在Box实例中(见上文)。你还可以进一步创建一个通用的超类来定义精灵的默认值isCountable()(注意,该方法与isBox类似,但从设计角度来看实际上更好,因为圆形没有isBox方法是有道理的 - Box也应该包含isCircle方法吗?)。


3

这里是我的建议。考虑定义一个枚举,其中包含不同的 Sprite 类型:

class Sprite {
    public enum SpriteType {
         BOX, CAT, BOTTLE, HUMAN, TARGET, /* ... */, SIMPLE;
    }

    public SpriteType getSpriteType(){
       return SIMPLE;
    }
}


class Box extends Sprite {
    @Override
    public SpriteType getSpriteType(){
       return Sprite.SpriteType.BOX;
    }
}

最后一点:
public boolean isGameWon(Board board) {
    for (Point point : board.getTargetPoints())
        if(board.getTopSpriteAt(point).getSpriteType()!=SpriteType.BOX)
            return false;
    return true;
}

这样,您就可以解决在Sprite中为每种类型X创建isATypeX()方法的问题。如果您需要一种新类型,只需将其添加到枚举中,只有需要检查此类型的规则才需要知道它。


2
不错的尝试。这很好,因为枚举在Rules眼中扮演语义标识符的角色,从某种程度上将语义与类型分离开来。但是从可维护性的角度来看,它真的给代码带来了什么吗?我需要再思考一下这个问题 :) - Thomas Ahle
我认为会的,因为创建一个新类型只会影响枚举、类型本身和适用规则。 - Tomas Narros
2
这仍然比仅使用类类型作为语义类型多一个 ;) - Thomas Ahle
2
我认为这违反了DRY原则。信息已经在类结构中了,不需要在枚举中重复。 - Chris Burt-Brown
2
嗯,与类相比,枚举系统的劣势在于更难以进行层次结构/分组。 - Thomas Ahle
显示剩余2条评论

1

基本上,不是

if (sprite instanceof Box)
    // Do something

使用

sprite.doSomething()

doSomething()Sprite 类中定义并在 Box 中被重写。

如果你想将这些规则与 Sprite 类层次结构分离,可以将它们移动到单独的 Rules 类(或接口)中,在其中 Sprite 有一个 getRules() 方法,子类返回不同的实现。这将进一步增加灵活性,因为它允许相同的 Sprite 子类对象具有不同的行为。


你如何使用精灵内部的规则来实现测试“每个目标上是否有一个盒子”的功能? - Thomas Ahle
@Thomas Ahle: cnt += sprite.getTargetCount()。其中getTargetCount()在“Box”中返回1,在“Sprite”中返回0。可能会有更简洁的解决方案,但这是我立即想到的。并且,这与在执行计数的代码中进行instanceof测试完全不同。 - Michael Borgwardt
我知道你想去哪里,但你如何在文档中描述该方法呢?“如果您是一个Box,则返回1,否则返回0?”这可能与“instanceof”不同,但它有什么优势呢? - Thomas Ahle
1
@Thomas Ahle:不,它绝对不应该被称为那个。它应该适用于(并以其名称命名)Boxes的特定行为。重要的是,现在检查具有特定于域的名称,并位于类型本身中(或特定于该类型的规则类中)。如果您添加一个表现像Box但不应计入Target顶部时的新类,则该类可以具有该方法的不同实现。 - Michael Borgwardt
@Thomas Ahle:“如果此对象位于目标顶部,则返回1以计入??;否则返回0。” - Michael Borgwardt
显示剩余4条评论

1
这是一个通用计数器的示例,没有针对每种要计数的类型的isAnX()方法。假设您想要计算板上X类型的数量。
public int count(Class type) {
    int count = 0;
    for (Point point : board.getTargetPoints())
        if(type.isAssignable(board.getTopSpriteAt(point)))
            count++;
    return count;
}

我猜你真正想要的是

public boolean isAllBoxes() {
    for (Point point : board.getTargetPoints())
        if(!board.getTopSpriteAt(point).isABox())
            return false;
    return true;
}

啊,是的,这样就干净多了 :) 但它仍然没有解决为每个我创建的 X 类型在 Sprite 中创建一个 isATypeX() 方法(并且规则需要知道)的问题。 - Thomas Ahle
也许我给出的计数示例避免了需要大量使用“isAnXxx()”方法。- @ThomasAhle - Peter Lawrey
那么在我的规则中,我会有boolean isGameWon(Board board){return count(Box.class) == board.getTargetPoints();}吗? - Thomas Ahle
你可以这样做,但效率会降低,因为它将始终计算每个点,即使第一个不是一个盒子。 - Peter Lawrey

1

这里实际上要测试的是:

玩家是否能够在棋盘顶部使用此 Sprite 赢得游戏?

因此,我建议使用以下名称:

public boolean isGameWon(Board board) {
    for (Point point : board.getTargetPoints())
        if(!board.getTopSpriteAt(point).isWinningSprite())
            return false;
    return true;
}

拥有一个isBox函数绝对没有任何意义。完全没有。你可以使用instanceof

但是如果BoxBottleTarget都是获胜的方块,那么你可以让它们全部返回。

class Box {
    public override bool isWinningSprite() { return true; }
}

你可以添加另一种“获胜”精灵,而无需更改isGameWon函数。


1
PS:我对 isWinningSprite 的命名不是百分之百确定,这取决于你正在制作的游戏。 - Chris Burt-Brown
我同意你的观点,但实际上这也是Tomas Narros的答案所涉及的。枚举类型“Box”也可以称为“获胜精灵”。它的意义只存在于规则的眼中。重要的是区分类类型和规则类型。 - Thomas Ahle
@Thomas:如果除了Boxes之外没有其他精灵可以成为获胜精灵,那么我认为最干净、最易于维护的解决方案可能是使用instanceof - Chris Burt-Brown

1

您考虑使用访问者模式。

每个点都继承了acceptBoxDetection方法:

boolean acceptBoxDetection(Visitor boxDetector){
           return boxDetector.visit(this);
}

and then Visitor does:

boolean visit(Box box){
        return true;
}

boolean visit(OtherStuff other){
        return false;
}

0

关于面向对象设计/重构的一般性陈述很难给出我的意见,因为“最佳”操作在很大程度上取决于上下文。

您应该尝试将“Do something”移动到Sprite的虚拟方法中,该方法不执行任何操作。此方法可以从您的循环中调用。

然后Box可以覆盖它并执行“something”。


0

我认为人们给你的建议是正确的。你的doSomething()可以长成这个样子:

class Sprite {
    public int doSomething(int cnt){
       return cnt;
    }
}

class Box extends Sprite {
    @Override
    public int doSomething(int cnt){
       return cnt + 1;
    }
}

所以在你的原始代码中,你可以这样做:

int cnt = 0;
for (Point point : board.getTargetPoints()) {
    Sprite sprite = board.getTopSpriteAt(point)
    cnt = sprite.doSomething(cnt);
}

或者,您也可以通过您提出的方法实现相同的目标,但这可能会导致每个循环额外的计算成本。

class Sprite {
    public boolean isBox() {
        return false;
    }
}

class Box extends Sprite {
    @Override
    public boolean isBox(){
       return true;
    }
}

但这与在Sprite中拥有boolean isBox()完全相同,这与拥有instanceof Box完全相同。我想问题在于前提条件,我想知道一些关于Boxes和Targets的事情。这是语法而不是语义,并违反了Liskov原则。我只是不知道还能做什么? - Thomas Ahle
好的,这并不完全相同。如果您在Box中覆盖了doSomething方法,当您从任何作为Box的Sprite对象执行该方法时,它将触发Box类内部的方法。您不需要进行额外的isBox()instanceof计算。此外,如果您覆盖doSomething方法,则不会违反Liskov规则。 - Mr.J4mes
Liskov替换原则指出:“如果S是T的子类型,则可以用S类型的对象替换T类型的对象,而不会改变该程序的任何期望属性(正确性、执行任务等)”。您的程序的“期望属性”是仅在“Sprite”为“Box”时增加“cnt”。因此,如果您用其子类替换所有“Sprite”,并且仍然获得相同的“期望属性”,则不违反Liskov原则。 - Mr.J4mes
好的,我明白这可能解决了依赖特定实现的问题。但这意味着对于我创建的每种新的Sprite类型,我都需要在Sprite和其他每个实现中添加一个isNewType()方法。 - Thomas Ahle
@ThomasAhle 是的。因此,我认为如果你覆盖 doSomething 的话可能会更好。 - Mr.J4mes
显示剩余2条评论

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