【问题标题】:Switch statements are bad? [closed]switch 语句不好? [关闭]
【发布时间】:2011-05-23 22:57:45
【问题描述】:

我最近了解到 switch 语句在 OOP 中很糟糕,特别是来自 Robert Martin 的“Clean Code”(p37-39)。

但是考虑一下这个场景:我正在编写一个游戏服务器,接收来自客户端的消息,其中包含一个表示玩家动作的整数,例如移动、攻击、拾取物品......等等,会有超过 30 种不同的行动。当我编写代码来处理这些消息时,无论我想到什么解决方案,它都必须在某个地方使用 switch。 if not switch 语句应该使用什么模式?

【问题讨论】:

  • 参见 [Large Switch 语句:糟糕的 OOP? ](*.com/questions/505454/…)
  • '“被认为有害”被认为有害'。
  • @Aaron,经常添加一个类可以最大限度地减少必须更改的现有代码量。如果您的代码依赖于已经存在的特定类,那么这些可能是 switch 语句(或多分支 if/then/else),其中每一个都需要更改
  • @Aaron,在您的交换机上添加case 时,您需要进行回归测试并为同一类开发更多测试用例。而添加另一个类不需要开发那么多测试,因为它是一个“新功能”,并且您不会触及任何现有源,这取决于您如何加载新类(也称为反射),否则回归测试会有实例化代码也要这样做
  • @Aaron - 当然,如果应用程序很小或者不太可能发生太大变化,那么切换可能会更清晰。有时“goto”也可以。我只是指出,人们说添加一个类与添加一个开关在质量上是不同的,这是有充分理由的

标签: oop design-patterns coding-style switch-statement


【解决方案1】:

开关类似于任何其他控制结构。有些地方它是最好/最干净的解决方案,还有更多地方完全不合适。它只是比其他控制结构更容易被滥用。

在 OO 设计中,在像您这样的情况下,通常认为最好使用从公共消息类继承的不同消息类型/类,然后使用重载方法“自动”区分不同类型。

在像您这样的情况下,您可以使用映射到您的操作代码的枚举,然后将一个属性附加到每个枚举值,这将允许您使用泛型或类型构建来构建不同的 Action 子类对象,以便重载方法将起作用。

但这真的很痛苦。

评估在您的解决方案中是否存在可行的设计选项,例如枚举。如果没有,请使用开关。

【讨论】:

  • 我猜你的意思是动态多态而不是静态多态(重载方法
  • @Toby 你能否用例子详细说明这个语句:“然后为每个枚举值附加一个属性,让你使用泛型或类型构建来构建不同的 Action 子类对象,以便重载方法会起作用。”?
  • 是的,我可能只是用public static PlayerAction forActionId(int actionId) 制作一个PlayerAction 枚举,并且该枚举将有一个公共doAction 类型方法。无需开关,只需PlayerAction.forActionId(number).doAction(args);。现在,添加另一个动作所需要做的就是添加一个枚举值并在其上实现doAction 方法。注意:我也不喜欢使用“序数”,因为它很容易按字母顺序重新排列您的枚举值并使用旧数字破坏任何内容。
【解决方案2】:

'Bad' switch 语句通常是那些打开对象类型(或其他设计中可能是对象类型)的语句。换句话说,对多态性可能更好地处理的东西进行硬编码。其他类型的 switch 语句可能没问题

您将需要一个 switch 语句,但只有一个。收到消息后,调用 Factory 对象返回相应 Message 子类(Move、Attack 等)的对象,然后调用 message->doit() 方法完成工作。

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

【讨论】:

  • Map<Class<?>, Thing> 这样的操作与“打开课程”非常相似,但是,这是一种好的做法吗?
【解决方案3】:

想到了Strategy 模式。

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

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


至于 switch 语句 - 在“清洁代码”中,Robert Martin 说他试图将自己限制为每种类型的 一个 switch 语句。不要完全消除它们。

原因是 switch 语句不遵守OCP

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 并非一切都必须是 OOP。真的很喜欢这个答案。
    【解决方案5】:

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

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

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

    【讨论】:

      【解决方案6】:

      IMO switch 语句不是,但应尽可能避免。一种解决方案是使用Map,其中键是命令,值Command 对象使用execute() 方法。或者List,如果你的命令是数字的并且没有间隔。

      但是,通常,您在实现设计模式时会使用switch 语句;一个例子是使用Chain of responsibility 模式来处理给定任何命令“id”或“value”的命令。 (还提到了Strategy 模式。)但是,在您的情况下,您也可以查看Command 模式。

      基本上,在 OOP 中,您将尝试使用其他解决方案,而不是依赖于使用过程编程范式的 switch 块。但是,何时以及如何使用这两种方法在某种程度上取决于您的决定。我个人在使用Factory 模式等时经常使用switch 块。


      代码组织的定义是:

      • 一个包是一组具有一致 API 的类(例如:Collection 在许多框架中的 API)
      • 是一组连贯的功能(例如:Math 类...
      • 方法一个功能;它应该只做一件事和一件事。 (例如:在列表中添加项目可能需要扩大该列表,在这种情况下,add 方法将依赖其他方法来执行此操作,并且不会自行执行该操作,因为它不是合同。)李>

      因此,如果您的 switch 语句执行不同类型的操作,则您“违反”该定义;而使用设计模式并不是因为每个操作都在它自己的类中定义(它自己的一组功能)。

      【讨论】:

        【解决方案7】:

        我不买。这些 OOP *者似乎拥有拥有无限 RAM 和惊人性能的机器。显然,使用无限 RAM,您不必担心 RAM 碎片以及不断创建和销毁小型帮助程序类时对性能的影响。套用“Beautiful Code”一书中的一句话——“计算机科学中的每一个问题都可以通过另一个抽象层次来解决”

        如果需要,请使用开关。编译器非常擅长为它们生成代码。

        【讨论】:

        • 你应该重构然后优化。反过来做是没有任何意义的。
        【解决方案8】:

        使用命令。将动作包装在一个对象中,让多态性为您完成切换。在 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);
        }
        

        【讨论】: