经常使用 instanceof 是一种好的实践吗?

16
场景:我正在编写与游戏相关的代码。 在该游戏中,一个“Player”(也是一个类)拥有一个“Item”的列表。 还有其他类型的物品从“Item”继承,例如“ContainerItem”,“DurableItem”或“WeaponItem”。
显然,对于我来说,只拥有“List ”非常方便。 但是,当我获取玩家物品时,唯一区分物品类型的方法是使用“instanceof”关键字。 我确信已经读到过依赖它是不好的做法。
在这种情况下使用它是否可以? 还是应该重新考虑我的整个结构?

如果Item定义了一个抽象方法并强制每个子类实现自己的方法,那么您可以避免使用instanceof的需要。 - NickJ
可能需要重新思考你的结构。你所描述的模式实际上是一种反模式。 - MadConan
通常情况下,使用 instanceof 是设计不良的症状。诀窍是使用双重分派或访问者模式。在网上搜索一下。 - Jean-Baptiste Yunès
如果您能提供一些关于如何使用instanceof的细节,那么给您举一些例子会更容易些。 - MadConan
可能是使用instanceof的正确决定时间?的重复问题。 - dimo414
6个回答

22

假设我正在编写一些库存代码:

public void showInventory(List<Item> items) {
    for (Item item : items) {
        if (item instanceof ContainerItem) {
            // container display logic here
        }
        else if (item instanceof WeaponItem) {
            // weapon display logic here
        }
        // etc etc
    }
}

那段代码可以编译并正常运行。但它遗漏了面向对象设计的关键思想:你可以定义父类来进行一般有用的事情,并且让子类填写特定而重要的细节。

上述方法的替代方案:

abstract class Item {
    // insert methods that act exactly the same for all items here

    // now define one that subclasses must fill in themselves
    public abstract void show()
}
class ContainerItem extends Item {
    @Override public void show() {
        // container display logic here instead
    }
}
class WeaponItem extends Item {
    @Override public void show() {
        // weapon display logic here instead
    }
}

现在我们有一个地方可以查找所有子类的库存显示逻辑,那就是show()方法。我们如何访问它?很容易!

public void showInventory(List<Item> items) {
    for (Item item : items) {
        item.show();
    }
}

我们将所有特定于项目的逻辑都放在特定的项目子类中。这样可以使您的代码库更易于维护和扩展。它减轻了第一个代码示例中长for-each循环的认知负担。并且它使show() 准备好在您尚未设计的地方重用。


2
这是正确的做法,但有时候还不够。以Record类为例 - 我们有两种记录类型 - FileDirectory。通用的东西可以抽象成记录,但是假设File有一个download()方法。因此,从Set<Record>中你不能下载文件,除非你知道它们确实是文件。所以如果你遇到这种情况,多态就不够了,但在大多数情况下它是最好的解决方案,所以给这个好答案加上+1。 - Svetlin Zarev
@SvetlinZarev 你也可以进行抽象化 - 通过实现有意义的方法,可以抛出UnsupportedOperationException(例如java.util.Iterator.remove),提供Null-Object/默认返回或添加查询方法以确定方法是否适用。这只是一个努力的问题,而不是根本上的不可能性。 - Durandal
2
我认为在接口中添加方法,只是为了抛出“UnsupportedOperationException”是不好的设计。而且,空对象模式的目的完全不同。是的,它可能完成工作,但更像是一种hack,而不是真正的解决方案。以我的“记录”示例为例。为什么我要用不适用于每个记录的方法来污染“记录”接口?这样做只会使API更难使用-即客户端将不得不处理愚蠢的异常或无用的返回值。 - Svetlin Zarev
@svetlin zarev,您会如何在您的示例中实现下载方法? - markus

8
在我看来,使用instanceof是一种代码异味。简单来说,它会使您的代码程序化,而不是面向对象的。使用访问者模式是面向对象的方式。
访问者模式还允许您轻松构建decoratorchain of responsibility,从而实现关注点分离,得到更短、更清洁、更易于阅读和测试的代码。
此外,您是否真的需要知道确切的类?难道不能利用多态性吗?毕竟Axe就像Sword一样都是Weapon

1
我不同意,访问者模式允许您在其基础上轻松构建“装饰器”和“责任链”,以实现“关注点分离”,从而使您的代码更简单、更易于阅读。它还强制执行类型安全性——您无法添加T的新子类而不会遇到编译时错误,而使用“instanceoff”,您将在运行时发现问题。 - Svetlin Zarev
我的评论并不是认真的,但我已经尝试过多次理解访问者模式,却从未成功过。我曾经不得不维护一个使用访问者模式的代码,发现它非常令人沮丧。 - NickJ
我需要进行一些深入的谷歌搜索才能理解这个模式...但是如果我错了,请纠正我,这不是和@Matt Spephenson的帖子非常相似吗? - Justas S
1
不,不是这样的。而且Matt在我之后发布了它。他向您展示如何利用语言的多态特性。基本上,他的方法是解决问题的最佳方案,但有时候还不够 - 请看我在他的帖子下面的评论。我的帖子是关于面向对象的instanceof方式 - 这是一种称为“双重分派”的技术,通过访问者模式实现。但别误会了。我的答案与他的不同也不相反。它是互补的。如果您没有正确实现类层次结构,则无法实现访问者。 - Svetlin Zarev
我并没有说任何冒犯的话。对于访客模式,它对我来说似乎很复杂,而我所看到的教程与他的代码非常相似。 - Justas S
使用访问者模式仍会导致空的访问函数,这样做没问题吗? - Gopikrishna K S

3
您需要重新思考您的结构,非元代码中的instanceof几乎总是反模式的标志。请尝试在Item类/接口中定义所有Item共有的行为(例如具有图片、描述以及单击它们时会发生某些事情),如果合适的话,请使用abstract关键字,并利用多态性来实现具体内容。

3

1

如果你觉得容易理解,那就没问题。

将分支逻辑从自然归属的位置移动到子类中并不一定是一个好主意。这可能会创建错误的依赖关系,导致过于庞大的类,并且可能难以导航和理解。

最终,这一切都关乎如何在一维空间中物理组织我们的代码,同时考虑多个方面的问题。这不是一个微不足道的问题,它是主观的,并且没有万灵药。

特别是,我们的行业继承了许多经验规则,这些规则是基于上个世纪的技术和经济约束制定的。例如,工具非常有限;程序员费用高昂;应用演变缓慢。

这些规则中的一些在今天可能已经不再适用。


-1

我并不认为instanceof对于那些知道如何使用它来避免编写更复杂代码的程序员是坏事。万物皆有用,也有误用。

话虽如此,你提供的描述并不需要使用instanceof。有各种方法可以实现这一点,而且(最重要的是)替代方案必须比使用instanceof更好。不要仅仅因为听说instanceof不好就选择非instanceof解决方案。

我认为,在你的情况下,非instanceof解决方案有助于使解决方案更易于扩展。

for (Item i: items) {
    if (ItemTypes.WEAPON_ITEM.equals(i.getType)) {
        processWeaponType(i);
    }

}

或者

for (Item i: items) {
    if (WeaponItem.class.equals(i.getType)) {
        processWeaponType(i);
    }

}

好的,但是你的解决方案并不比 instanceof 更易读。就性能而言,instanceof 真的很快;它绝对比比较我们自己的 type 字段要快;甚至可能比将 Class== 进行比较还要快。 - ZhongYu
1
也许,但是在这个问题上,速度是期望的结果还是软件设计和架构?我的意思是,如果你真的认为性能差异值得考虑,我们可以完全摆脱类,只需将所有内容放入一个长方法中。由于JIT编译器会重新组织您的代码,很难确定哪个更快。 - Jose Martinez
这并不比instanceof更好,它只是instanceof的伪装。两种建议都没有解决添加新的子类型时需要检查/更新所有客户端代码的问题。 - Durandal

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