Switch语句有问题?

45

我最近从Robert Martin的《Clean Code》(第37-39页)中了解到,在面向对象编程中,switch语句是不好的。

但考虑以下场景:我正在编写一个游戏服务器,接收来自客户端的消息,其中包含一个整数,指示玩家的动作,如移动、攻击、拾取物品等,将有30多种不同的动作。当我编写处理这些消息的代码时,无论我想出什么解决方案,都需要在某个地方使用switch。如果不使用switch语句,我应该使用什么设计模式?


12
“Considered Harmful” Considered Harmful。(该标题)表明“被认为有害”的文章类型本身也是有害的。 - user395760
2
@Aaron,通常添加一个类可以最小化需要更改的现有代码量。如果您的代码取决于已经存在的特定类,则这些类很可能是switch语句(或多分支if / then / else),每个都需要更改。 - The Archetypal Paul
1
@Aaron,当你在你的switch语句中添加一个case时,你需要进行回归测试,并为同一类开发更多的测试用例。而添加另一个类不需要开发那么多的测试用例,因为它是一个“新功能”,并且你没有触及任何现有的源码,这取决于你如何加载新类(也称为反射),否则对于实例化代码也必须进行回归测试。 - Yanick Rochon
2
@Aaron - 当然,如果应用程序很小或不太可能发生变化,那么使用switch可能更清晰。有时候goto也可以使用。我只是想指出人们说添加类与添加开关的质量差异很大的好理由。 - The Archetypal Paul
显示剩余3条评论
8个回答

41

一个开关就像其它控制结构一样。在某些情况下,它是最好/最清晰的解决方案,在许多情况下,它完全不合适。它只是被滥用比其他控制结构更多。

在面向对象设计中,通常认为在像你这样的情况下使用不同的消息类型/类继承自一个公共的消息类,然后使用重载方法来“自动”区分不同类型是更可取的。

在你这种情况下,你可以使用映射到你的操作代码的枚举,然后附加到每个枚举值的属性,这将使你能够使用泛型或类型构建来构建不同的Action子类对象,以便重载方法能够正常工作。

但那真的很麻烦。

评估是否有一个设计选项,如枚举,在你的解决方案中是可行的。如果没有,就使用开关。


3
我猜你的意思是动态多态而不是静态多态(重载方法)。 - Geek
@Toby,你能否举个例子详细说明这句话的意思:“然后将属性附加到每个枚举值,以便您可以使用泛型或类型构建来构建不同的Action子类对象,以使重载方法起作用。”? - beinghuman
是的,我可能只会创建一个PlayerAction枚举,其中包含一个public static PlayerAction forActionId(int actionId)方法,该枚举将具有公共的doAction类型方法。不需要使用switch,只需使用PlayerAction.forActionId(number).doAction(args);即可。现在,您只需添加另一个操作就可以添加枚举值并在其上实现doAction方法。注意:我也不喜欢使用“ordinal”,因为很容易按字母顺序重新排序枚举值并破坏使用旧数字的任何内容。 - Shadow Man

22

'坏'的switch语句通常是那些在对象类型上(或者在另一种设计中可能是对象类型的东西)进行切换的语句。换句话说,将某些最好由多态性处理的东西硬编码。其他类型的switch语句可能是可以接受的。

你需要一个switch语句,但只有一个。当你收到消息时,调用一个工厂对象来返回适当的Message子类对象(Move、Attack等),然后调用message->doit()方法来执行工作。

这意味着如果您添加更多消息类型,只有工厂对象需要更改。


做像 Map<Class<?>, Thing> 这样的事情怎么样?这非常类似于“在类上进行切换”,但是这被认为是一个好的实践吗? - YoTengoUnLCD

20

我想到了策略模式。

策略模式旨在提供一种定义算法族的方法,将每个算法封装为一个对象,并使它们可以互换。策略模式使得算法可以独立于使用它们的客户端而变化。

在这种情况下,“算法族”是您的不同操作。


关于switch语句 - 在《Clean Code》一书中,Robert Martin说他尽量每种类型只使用一个switch语句。并不是完全消除它们。

原因是switch语句不符合OCP原则。


5
我会将消息放入一个数组中,然后匹配项目与解决方案关键字以显示消息。

不是所有的东西都必须是面向对象编程。我真的很喜欢这个答案。 - grasshopper

5

我不相信这些面向对象编程(OOP)的狂热者拥有具备无限RAM和惊人性能的机器。显然,如果你拥有无限RAM,就不必担心连续创建和销毁小型辅助类时会出现的RAM碎片化和性能影响。引用《编写优美代码》一书中的一句话来概括 - “计算机科学中的每个问题都可以通过另一个抽象层次来解决”。

如果需要,可以使用switch语句。编译器在生成代码方面表现得非常好。


5
你应该先重构再进行优化。反其道而行之是没有意义的。 - Eva

4

从设计模式的角度来看,您可以在给定的场景中使用命令模式 (请参阅 http://en.wikipedia.org/wiki/Command_pattern)。

如果您发现自己在 OOP 范例中反复使用 switch 语句,则说明您的类可能设计得不好。假设您有超类和子类的适当设计以及相当数量的多态性。应由子类处理 switch 语句背后的逻辑。

有关如何消除这些 switch 语句并引入适当的子类的更多信息,我建议您阅读 Martin Fowler 的《重构》第一章。或者,您可以在此处找到类似的幻灯片http://www1.informatik.uni-wuerzburg.de/database/courses/pi2_ss03_dir/RefactoringExampleSlides.pdf(第44页)。


4
我认为switch语句并不是“坏”的,但如果可能的话应该避免使用。一种解决方案是使用Map,其中键是命令,值是带有execute()方法的Command对象。或者,如果您的命令是数字且没有间隙,则可以使用List
然而,通常情况下,在实现设计模式时会使用switch语句;一个例子是使用责任链模式来处理给定任何命令“id”或“value”的命令。(还提到了策略模式。)但是,在您的情况下,您还可以查看命令模式。
基本上,在面向对象编程中,您将尝试使用其他解决方案,而不是依赖于使用过程性编程范例的switch块。但是,何时以及如何使用取决于您的决定。我个人在使用工厂模式等时经常使用switch块。
代码组织的定义是:
  • 是具有一致API的类组(例如许多框架中的Collection API)
  • 是一组相关功能(例如Math类...)
  • 方法是一个功能;它应该只做一件事情。 (例如,在列表中添加项目可能需要扩大该列表,在这种情况下,add方法将依赖于其他方法来执行此操作,并且不会执行该操作本身,因为它不是它的契约。)
因此,如果您的switch语句执行不同类型的操作,则“违反”该定义;而使用设计模式则不会,因为每个操作都在其自己的类中定义(其自己的一组功能)。

2

使用命令。将操作包装在对象中,让多态性为您执行切换。在C++中(shared_ptr只是一个指针,或者从Java术语来说是一个引用。它允许进行动态调度):

void GameServer::perform_action(shared_ptr<Action> op) {
    op->execute();
}

客户端选择要执行的操作,一旦他们这样做,就会将该操作本身发送到服务器,因此服务器无需进行任何解析:
void BlueClient::play() {
    shared_ptr<Action> a;
    if( should_move() ) a = new Move(this, NORTHWEST);
    else if( should_attack() ) a = new Attack(this, EAST);
    else a = Wait(this);
    server.perform_action(a);
}

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