有人不同意这个说法吗:“使用switch是不好的面向对象编程风格”?

28

我在stackoverflow的多个帖子/评论中看到过许多人写道使用 switch 是不好的面向对象编程(OOP)风格。就我个人而言,我不同意这种说法。

会有很多情况下你无法向你想要 switch 的 enum 类中添加代码(即方法),因为你无法控制它们,也许它们在一个第三方 jar 文件中。还有一些情况下,将功能放在枚举本身上是一个不好的想法,因为它违反了某些关注点分离的考虑,或者它实际上是其他某些东西的函数以及枚举。

最后,switch语句简洁明了:

boolean investable;
switch (customer.getCategory()) {
    case SUB_PRIME:
    case MID_PRIME:
        investible = customer.getSavingsAccount().getBalance() > 1e6; break;
    case PRIME:
        investible = customer.isCeo(); break;
}

我并不是在为每一个使用switch的情况辩护,也不是在说这总是最好的方法。但像“switch是代码异味”的说法,在我看来是错误的。还有其他人同意吗?


我认为“代码异味”是一个针对任何代码都有攻击性的标签,这很恶心。代码没有气味,程序员才有。 - EnocNRoll - AnandaGopal Pardue
这不是一个真正的问题,而是一种抱怨,附带了一个修辞性问题。如果是一个真正的问题,它应该被表达得完全不同。 - Joachim Sauer
如果代码出现“异味”,那就不是被编写的,而是被倾倒的。 - EnocNRoll - AnandaGopal Pardue
@Bill the Lizard:我只是喜欢用“丑陋”作为比喻,而不是“臭”。 - EnocNRoll - AnandaGopal Pardue
开关是好的,但是一定要把break放在下一个case之前,而不是附加到长语句的末尾。无论是开关还是多态性,任何过度使用都是不好的。 - bruceatk
显示剩余10条评论
22个回答

59

我认为像这样的陈述

使用switch语句是不好的面向对象编程风格。

情况语句几乎总是可以用多态代替。

都过于简单化了。事实上,针对类型进行switch语句的确是糟糕的面向对象编程风格,这些应该用多态来替换。而基于的switch语句则没有问题。


1
我同意,但不要忘记 (x instanceof C) 可以并且经常被类似于 (x.getType() == 123) 的测试所替代,这在技术上是一个值的开关。那么,你能澄清类型/值的区别吗? - eljenso
1
x.getType 只是在“扯技术细节”,这样做很不好。请停止淘气。 - gbjbaanb
@g 那真是一个很不方便的技术细节。 - eljenso
@eljenso: gbjbaanb是对的。使用带有数值的类型代码与根据类型进行切换是相同的。如果该值只表示对象的类型,那么有什么区别呢? - Bill the Lizard
@eljenso: 我更倾向于将其称为术语的不便之处。"类型"可以,但并不总是指类似于面向对象的类的东西。The Lizard说得太对了。 - Dave Sherohman
@Dave 同意。@(all) 不过,最后一次尝试:switch 通常用于基于单个枚举、整数、字符等做出决策,而不是 if-then-else,其中你测试范围和其他更奇特的条件。为什么会这样呢?也许 switch 只是经常用于分支类型的事物? - eljenso

17

针对您的后续提问:

如果这只是客户希望获得商业贷款的“可投资性”逻辑,那么客户在另一种产品上的投资决策可能会完全不同...此外,如果有新产品不断推出,每个产品都有不同的投资决策,我不想每次都更新我的核心客户类,该怎么办?

以及其中一条评论:

我对将逻辑保存在其操作数据附近并不完全确定。现实世界并不像这样运作。当我申请贷款时,银行会决定我是否符合条件。他们不会要求我自己做决定。

就您所说的而言,您是正确的。

boolean investable = customer.isInvestable();

对于你所谈论的灵活性来说,这并不是最优解。但是,原问题并没有提到一个单独的“产品基类”存在。

鉴于现在有了额外的信息,最佳解决方案似乎是

boolean investable = product.isInvestable(customer);
投资决策是由产品根据你的“真实世界”参数进行多态性决策,同时也避免每次添加产品时创建新的客户子类。 产品可以使用任何方法来确定基于客户公共接口的决策。 我仍然会质疑是否有适当的添加可以用于消除开关的需要,但它可能仍然是所有恶魔中最小的一个。
在提供的特定示例中,我倾向于做以下事情:
if (customer.getCategory() < PRIME) {
    investable = customer.getSavingsAccount().getBalance() > 1e6;
} else {
    investable = customer.isCeo();
}

我认为这种方法比在switch语句中列出每个可能的类别更清晰简洁。我猜这种方式更能反映"现实世界"的思维过程("它们是否低于prime?"而不是"它们是sub-prime或mid-prime")并且如果将来添加了一种SUPER_PRIME的称号,它可以避免重新访问此代码。


这是一个非常优秀的解决方案。唯一需要注意的是,如果决策需要某种协作服务类,则该类需要从每个产品实例中访问。假设决策基于(也是)某些必须独立于客户/产品查找的事实。 - oxbow_lakes
@Dave 你只是把 switch 重写成了 if-then-else,通常情况下这并不能消除原来的问题(要不要使用 switch)。 - eljenso
1
@eljenso:在两个标准中选择一个是一个问题吗?是的,这可以仅使用多态性来解决,但代价是需要为每种可能的产品和客户类型组合创建一个新的子类。那将比原始情况更糟糕。 - Dave Sherohman
@Chris:即使需要来自第三方的其他信息,我认为决策仍然是每个产品政策的问题,因此 Policy 类(层次结构)很可能仍然是该方法的正确位置。 - Dave Sherohman
如果我控制枚举,我会使用接口进行双重分派;如果我不能控制枚举,我会使用包装器。 - Ran Biron
这真是太有趣了,是我目前在编程中唯一喜欢的事情。不幸的是,我曾经在博客上读到过面向对象并不能真正提高生产效率的说法。但愿这不是真的! - Dan Rosenstark

16

在纯面向对象的代码中,使用switch语句是代码异味(smell)。这并不意味着它们本质上是错误的,只是你需要仔细考虑是否使用。要特别小心。

我的定义中,switch还包括那些可以轻松重写为switch语句的if-then-else语句。

Switch语句可能表明您没有在操作数据的地方定义行为,并且没有充分利用子类型多态性。

当使用面向对象语言时,您不必强制以面向对象的方式编程。因此,如果选择使用更功能性或基于对象的编程风格(例如,使用仅包含数据而不包含行为的DTO,而不是更丰富的领域模型),则使用switch语句没有问题。

最后,在编写面向对象程序时,switch语句非常方便,可以用于在外部非面向对象世界进入您的面向对象模型时将外部实体转换为面向对象概念。最好尽早进行这种转换。例如:从数据库中获取的int可以使用switch语句转换为对象。

int dbValue = ...;

switch (dbValue)
{
  case 0: return new DogBehaviour();
  case 1: return new CatBehaviour();
  ...
  default: throw new IllegalArgumentException("cannot convert into behaviour:" + dbValue);  
}

编辑 阅读了一些回答之后。

Customer.isInvestable:很好,多态性。但是现在你将这个逻辑与客户联系起来,你需要为每种类型的客户都实现不同的行为而创建一个子类。据我所知,继承不应该被用于这种方式。你希望客户类型是 Customer 的属性,或者有一个函数可以决定客户的类型。

双重分派:两次多态性。但是你的访问者类本质上仍然是一个大开关,它有一些与上面解释的相同的问题。

此外,根据 OP 的示例,多态性应该基于客户的类别,而不是基于 Customer 本身。

切换值很好:好吧,但是切换语句在大多数情况下是用于测试单个 intcharenum 等值,而不是 if-then-else,在那里可以测试范围和更奇特的条件。但是如果我们在此单个值上调度,并且它不在我们的 OO 模型边缘,如上面所述,那么似乎经常使用 switch 来调度类型,而不是值。或者:如果您不能使用 switch 替换 if-then-else 的条件逻辑,那么您可能没有问题,否则您可能有问题。因此,我认为 OOP 中的 switch 是代码异味,而语句

  

在类型上切换是不好的 OOP 风格,   在值上切换很好。

本身就过于简单化了。

回到起点:一个 switch 不是坏的,它只是不总是非常 OO。您不必使用 OO 来解决问题。如果您确实使用 OOP,则需要特别注意切换。


我并不完全确定将逻辑与其操作的数据紧密结合的做法是否可行。现实世界并非如此运作。当我申请贷款时,银行会决定我是否有资格,而不是让我自己决定。 - oxbow_lakes
现实世界并不像这样运作,面向对象编程是这样的。也许您不想要一个面向对象的设计?好吧。也许您有一个面向对象的设计,但它是错误的。 - eljenso
我同意eljenso的观点。但是,请不要再谈论代码气味了。代码要么美丽,要么丑陋,或者介于两者之间。代码没有气味。 - EnocNRoll - AnandaGopal Pardue
1
@oxbow_lakes,现实世界并不像那样,因为客户不能信任他或她是否有资格获得贷款。此外,他或她不知道银行的规则,也没有办法“给”他们(而且信任是一个问题,再次强调)。这就是为什么OO建模(像所有开发一样)是一个创造性的过程。某些模型比其他模型更好(允许您使用更少/更清晰/更高效的代码完成更多工作)。Switch并不是坏事,但当您在代码中多次切换相同的内容时,这不是DRY,您应该考虑OO建模。 - Dan Rosenstark

15

这是不好的面向对象编程风格。

并非所有问题都适合使用面向对象编程。有些问题需要模式匹配,而 switch 语句则是较为简陋的版本。


13

如果说有什么东西让我感到不耐烦的话,那就是人们把这种编程风格描述为面向对象编程——在“低悬”的类型(客户端、账户、银行)中添加一堆getter,并将有用的代码散布在系统中的“控制器”、“帮助器”和“实用工具”类中。像这样的代码在面向对象系统中是一种异味,你应该问为什么而不是感到生气。


7
当然,switch语句不是面向对象的好方法;在函数中间插入return语句也是不好的做法;魔法值是不好的;引用永远不应该为null;条件语句必须放在{括号}中。但这些只是指导方针,不应该被过分地奉为圭臬。可维护性、可重构性和易理解性都非常重要,但实际完成工作才是最重要的。有时我们没有时间成为一个编程理想主义者。
如果认为某个程序员是有能力的,那么应该假设他能遵循指导方针并谨慎使用可用的工具,并且应该接受他不总是会做出最好的决定。他可能会选择一条次优路径或者犯错并遇到难以调试的问题,因为他选择了switch语句,或者传递了太多的null指针。这就是生活,他从错误中学习,因为他是有能力的。
我不会盲目地遵循编程教条。我会在自己作为程序员的背景下考虑指导方针,并根据情况加以应用。除非它们对手头问题至关重要,否则我们不应该过分强调这些编程实践。如果您想要表达自己对于良好的编程实践的看法,最好在博客或适当的论坛(比如这里)上发表。

6

罗伯特·马丁在他的文章中提供了另一个视角,关于开闭原则

软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。

在您的代码示例中,您实际上是根据客户“类别类型”进行切换。

boolean investible ;
switch (customer.getCategory()) {
    case SUB_PRIME:
    case MID_PRIME:
        investible = customer.getSavingsAccount().getBalance() > 1e6; break;
    case PRIME:
        investible = customer.isCeo(); break;
}

在当前的环境下,可能会出现新的客户类别 ;-). 这意味着需要打开这个类,并不断地修改它。如果你只有一个单独的 switch 语句,那么这可能还可以接受,但是如果你想在其他地方使用类似的逻辑,会怎样呢。
与其他建议不同的是,在 Customer 上创建一个 isInvestible 方法,我认为 Category 应该成为一个完整的类,并用于做出这些决策:
boolean investible ;
CustomerCategory category = customer.getCategory();
investible = category.isInvestible(customer);

class PrimeCustomerCategory extends CustomerCategory {
    public boolean isInvestible(Customer customer) {
        return customer.isCeo();
    }
}

5

我认为根据类型进行开关是一种代码异味。然而,我理解您对于代码中的关注点分离的担忧。但是,这些问题可以通过许多方法来解决,允许您仍然使用多态性,例如访问者模式或类似的方法。请阅读“四人帮”的“设计模式”

如果您的核心对象(如客户)大部分时间保持不变,但操作经常发生变化,则可以将操作定义为对象。

    interface Operation {
      void handlePrimeCustomer(PrimeCustomer customer);
      void  handleMidPrimeCustomer(MidPrimeCustomer customer);
      void  handleSubPrimeCustomer(SubPrimeCustomer customer);    
    };

    class InvestibleOperation : public Operation {
      void  handlePrimeCustomer(PrimeCustomer customer) {
        bool investible = customer.isCeo();
      }

      void  handleMidPrimeCustomer(MidPrimeCustomer customer) {
        handleSubPrimeCustomer(customer);
      }

      void  handleSubPrimeCustomer(SubPrimeCustomer customer) {
        bool investible = customer.getSavingsAccount().getBalance() > 1e6;    
      }
    };

    class SubPrimeCustomer : public Customer {
      void  doOperation(Operation op) {
        op.handleSubPrimeCustomer(this);
      }
    };

   class PrimeCustomer : public Customer {
      void  doOperation(Operation op) {
        op.handlePrimeCustomer(this);
      }
    };

这可能看起来有些过度,但当您需要处理操作集合时,它可以轻松地为您节省大量编码。例如,在列表中显示所有操作并让用户选择其中一个。如果将操作定义为函数,则很容易出现大量硬编码的switch-case逻辑,每次添加另一个操作时都需要更新多个位置,或者如我在此处看到的所述,产品


为了使用访问者模式,您需要控制代码库。此外,这是一个个人的看法,我发现在使用访问者时控制流更难以跟踪。 - oxbow_lakes
当使用访问者模式时,流程确实更难跟踪,但尽量不要像处理过程代码一样跟踪它,而是以交互和接口契约为思考方式。例如,如果一个方法表示它会执行某个操作,不要去跟踪它,只需假设它可以正常工作(通过单元测试来确保)。 - Chii

4

有时候你需要根据几个选项做出决策,而多态性可能过于复杂(YAGNI)。在这种情况下,switch是可以接受的。Switch只是一个工具,可以像其他工具一样轻易地使用或滥用。

这取决于你想要做什么。然而,重要的是,在使用switch时要三思而行,因为它可能表明设计不良。


3

我认为使用switch语句可以比if/else块更易读。如果你能将逻辑简化为可整体评估的结构,那么代码很可能提供了OOP所需的封装级别。

在实际程序中,必须编写真正(混乱)的逻辑。Java和C#不是严格的OOP语言,因为它们继承自C。如果您想强制执行严格的OOP代码,则需要使用不提供违反该思维方式的习语的语言。我认为Java和C#都旨在具有灵活性。

奇怪的是,VB6之所以如此成功,是因为它是基于对象而不是面向对象的。因此,我会说务实的程序员必然会结合概念。只要已经编程良好的封装性较好,switch也可以导致更可管理的代码。


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