27

我已经看到它在stackoverflow的多个线程/评论中编写,使用switch只是糟糕的OOP风格。我个人不同意这一点。

在很多情况下,您无法将代码(即方法)添加到enum要打开的类,因为您无法控制它们,也许它们位于第 3 方 jar 文件中。在其他情况下,将功能放在 enum 本身是一个坏主意,因为它违反了一些关注点分离的考虑,或者它实际上是其他东西的函数以及 enumeration

最后,开关简洁明了:

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 是一种代码味道”这样的说法是错误的。还有人同意吗?

4

22 回答 22

57

我认为这样的陈述

使用 switch 语句是不好的 OOP 风格。

案例语句几乎总是可以用多态性代替。

过于简单化了。事实是,打开类型的case 语句是糟糕的 OOP 风格。这些是您想要用多态性替换的那些。打开一个很好。

于 2009-02-15T16:06:36.733 回答
17

采取后续行动:

如果这只是希望获得商业贷款的客户的“可投资性”逻辑怎么办?也许客户对另一种产品的可投资性决定真的很不同......另外,如果一直有新产品出现,每个产品都有不同的可投资性决定,我不想每次都更新我的核心客户类这种情况发生的时间?

和你的评论之一:

我不完全确定是否将逻辑靠近它所操作的数据。现实世界不是这样运作的。当我申请贷款时,银行会决定我是否符合条件。他们不要求我自己做决定。

就这一点而言,你是对的。

boolean investable = customer.isInvestable();

不是您所说的灵活性的最佳解决方案。但是,原始问题没有提到存在单独的 Product 基类。

鉴于现在可用的附加信息,最好的解决方案似乎是

boolean investable = product.isInvestable(customer);

可投资性决策由产品根据您的“真实世界”论点做出(多态性!),并且它还避免了每次添加产品时都必须创建新的客户子类。产品可以根据客户的公共接口使用它想要做出该决定的任何方法。我仍然会质疑是否可以对客户界面进行适当的添加以消除切换的需要,但它可能仍然是所有弊端中最小的。

但是,在提供的特定示例中,我很想这样做:

if (customer.getCategory() < PRIME) {
    investable = customer.getSavingsAccount().getBalance() > 1e6;
} else {
    investable = customer.isCeo();
}

我发现这比在 switch 中列出所有可能的类别更清晰、更清晰,我怀疑它更有可能反映“现实世界”的思维过程(“它们是否低于质数?”与“它们是次质数还是中质数” ?"),并且如果在某些时候添加了 SUPER_PRIME 名称,它就不必重新访问此代码。

于 2009-02-15T16:01:16.247 回答
16

在纯 OO 代码中使用时,开关是一种代码味道。这并不意味着它们在定义上是错误的,只是你需要三思而后行。要格外小心。

我在这里对 switch 的定义还包括 if-then-else 语句,这些语句可以很容易地重写为 switch 语句。

开关可能表明您没有定义接近其操作的数据的行为,并且没有利用例如子类型多态性。

使用 OO 语言时,您不会被迫以 OO 方式进行编程。因此,如果您选择使用更具功能性或基于对象的编程风格(例如,使用仅包含数据但不包含行为的 DTO,而不是更丰富的域模型),使用开关没有任何问题。

最后,在编写 OO 程序时,开关在您的 OO 模型的“边缘”非常方便,当某些东西从非 OO 外部世界进入您的 OO 模型并且您需要将此外部实体转换为 OO 概念时。你最好尽早这样做。例如:数据库中的 int 可以使用开关转换为对象。

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其自身上。

切换一个值很好:好的,但是 switch 语句在大多数情况下用于测试单个int, char, enum, ... 值,而不是 if-then-else 可以测试范围和更奇特的条件。但是如果我们在这个单一的值上进行调度,并且它不像上面解释的那样位于我们的 OO 模型的边缘,那么似乎开关经常被用来在类型上调度,而不是在一个值上。或者:如果你不能用 switch 替换 if-then-else 的条件逻辑,那么你可能没问题,否则你可能不行。因此,我认为 OOP 中的开关是代码异味,而声明

打开类型是不好的 OOP 风格,打开一个值就可以了。

本身就过于简单化了。

回到起点:aswitch还不错,只是并不总是非常 OO。您不必使用 OO 来解决您的问题。如果您确实使用 OOP,那么您需要特别注意开关。

于 2009-02-15T15:05:32.540 回答
14

这是糟糕的 OOP 风格。

并不是所有的问题都最好用 OO 来解决。有些你想要模式匹配,哪个开关是穷人的版本。

于 2009-02-15T14:09:32.880 回答
12

如果有的话,我受够了人们描述这种编程风格——其中一堆getter被添加到“低挂”类型(客户、账户、银行)中,有用的代码被喷洒在“控制器”中的系统周围"、"helpers" 和 "utility" 类——作为面向对象的。像这样的代码在 OO 系统中一种气味,你应该问为什么而不是被冒犯。

于 2009-02-15T15:32:46.393 回答
7

当然开关是糟糕的 OO,你不应该在函数中间放一个 return,魔术值是不好的,引用永远不应该是 null,条件语句必须放在 {braces} 中,但这些是指导方针。他们不应该被虔诚地追随。可维护性、可重构性和可理解性都非常重要,但都仅次于实际完成工作。有时我们没有时间成为一个编程理想主义者。

如果任何程序员被认为是有能力的,那么应该假设他可以遵循指导方针并谨慎使用可用的工具,并且应该接受他不会总是做出最好的决定。他可能会选择一条不太理想的路线,或者犯错误并遇到难以调试的问题,因为他选择了一个开关,而他可能不应该拥有或传递太多的空指针。这就是生活,他从错误中吸取教训,因为他有能力。

我不虔诚地遵循编程教条。我在自己作为程序员的背景下考虑指导方针,并以合理的方式应用它们。除非它们是手头问题的基础,否则我们不应该对这些类型的编程实践大加赞赏。如果您想就良好的编程实践发表意见,最好在博客或适当的论坛(例如此处)中进行。

于 2009-02-15T16:10:41.310 回答
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 语句可能没问题,但是如果你想在其他地方使用类似的逻辑会发生什么。

与其他建议不同,在 whereisInvestible上创建一个方法Customer,我会说 Cgtegory 应该成为一个成熟的类,并用于做出这些决定:

boolean investible ;
CustomerCategory category = customer.getCategory();
investible = category.isInvestible(customer);

class PrimeCustomerCategory extends CustomerCategory {
    public boolean isInvestible(Customer customer) {
        return customer.isCeo();
    }
}
于 2009-02-16T14:00:23.080 回答
5

在某些情况下,您需要根据多个选项做出决定,而多态性是多余的(YAGNI)。在这种情况下,开关很好。Switch 只是一个工具,可以像任何其他工具一样轻松使用或滥用。

这取决于你想要做什么。然而,关键是你在使用 switch 时应该三思而后行,因为它可能表明设计不好。

于 2009-02-15T15:30:11.593 回答
5

我确实相信打开类型是一种代码味道。但是,我分享您对代码中关注点分离的担忧。但是这些可以通过多种方式解决,允许您仍然使用多态性,例如访问者模式或类似的东西。阅读四人组的“设计模式” 。

如果您的核心对象(如Customer)大部分时间保持不变,但操作经常更改,那么您可以将操作定义为对象。

    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 逻辑结束,每次添加另一个操作时都需要更新多个位置,或者我在这里看到的产品。

于 2009-02-15T17:12:48.070 回答
3

我将 switch 语句视为 if/else 块的更易读的替代方案。

我发现,如果您可以将逻辑归结为可以整体评估的结构,那么代码很可能会提供 OOP 所需的封装级别。

在某些时候,必须编写真实(杂乱)逻辑才能发布实用程序。Java 和 C# 不是严格的 OOP 语言,因为它们继承自 C。如果您想严格执行 OOP 代码,那么您需要使用一种不提供违反这种思维方式的习语的语言。我的观点是 Java 和 C# 都旨在灵活。

奇怪的是,VB6 如此成功的原因之一是它是基于对象的,而不是面向对象的。所以,我会说务实的程序员总是会结合概念。Switch 还可以导致更易于管理的代码,只要已经编写了合适的封装。

于 2009-02-15T16:15:08.037 回答
2

在你的库周围工作也是一种代码味道。您可能别无选择,但这并不是一个好的做法。

于 2009-02-15T14:10:39.380 回答
2

我发现在 OO 代码中使用 switch 语句没有任何问题。我唯一的批评是我会在 Customer 上创建一个名为 IsInvestible 的新方法,它隐藏了这个逻辑。使用 switch 语句作为该方法的内部实现有 0 个错误。正如您所说,您不能向枚举添加方法,但可以向客户添加更多方法。

如果您无法访问源代码,我会说非实例方法很好。最纯粹的 OOP 需要一个全新的对象,但在这种情况下,这似乎有点矫枉过正。

于 2009-02-15T15:03:52.827 回答
2

Î 知道你来自哪里。有些语言会强迫你这样做。

String str = getStr();
switch(str) {
case "POST" : this.doPost(); break;
case "GET" : this.doGet(); break;
//and the other http instructions
}

现在呢?当然,有一个很好的 OOP 方法可以做到这一点:

str.request(this);

太糟糕了,String 无法扩展,现在您正在考虑为每个 HttpInstruction 编写一个包含 8 个子类的 HttpInstruction 类。老实说,尤其是在谈论解析器时,这非常困难。

当然,这不是好的 OOP,但好的代码并不总是……可能。

让我暂时离题。我正在写我的论文。我个人不喜欢递归函数的通常设置。你通常有 funcRec(arg1,arg) 和 func(arg1):=func(funcRec(arg1,0));

所以我在我的论文中用默认参数定义了它。不是每个人都知道默认参数的概念。我的论文使用伪代码,但是教授让我将算法更改为传统方式,因为您不会经常遇到默认参数,所以不要使用它们。不要不必要地让你的读者感到惊讶。我认为他是对的。

但结果是现在我被一个函数困住了,它的唯一目的是传递默认参数——这可能更漂亮。

所以,底线是:真正漂亮的程序需要优秀的库、优秀的代码浏览器和工具、FogBugz 质量的错误跟踪器,至少更好、更集成、git 质量的版本管理等等。而且,嗯,你周围的人可以使用所有这些东西并且知道如何处理所有这些东西。最重要的是:一种漂亮的语言,可以优雅地解决棘手的问题。

因此,您很可能会被 Java 困住,这使得在所有情况下都很难找到一个好的开关替代品。Self 会有一个优雅的解决方案。但是您没有使用 Self,如果您使用了,您的同事将无法阅读它,所以忘记这一点。

现在找到妥协。

很难过,我知道。

于 2009-02-15T15:20:43.097 回答
2

我认为 switch 语句是否是糟糕的 OOP 实践取决于你在哪里使用 switch 语句。

例如,在工厂方法中,它可能是编写复杂且可能存在错误的基于反射的代码的一种非常简单的替代方法。

但在大多数情况下,我认为开关只是简单的设计。经常使用相同的方法将操作复杂性隐藏在不同的对象中会导致代码更易于理解,甚至可能更快。例如,如果你有一个执行很多的开关,那么预先打包的对象实际上可以节省一些 CPU 周期。

于 2009-02-16T01:21:26.447 回答
2

由于您没有引入代码,因此来自外部来源的数据本质上不能真正面向对象。如果它包含案例,您将有案例。时期。

除此之外,OOP 并不是灵丹妙药。有时是答案,有时不是。

于 2009-02-16T01:34:04.823 回答
1

是的,我受够了人们告诉你这是不好的风格。

编辑:在问题得到解决之前,这更有意义。

于 2009-02-15T14:35:00.883 回答
1

案例语句几乎总是可以用多态性代替。

public class NormalCustomer extends Customer {
    public boolean isInvestible() {
        return getSavingsAccount().getBalance() > 1e6;
    }
}

public class PreferredCustomer extends Customer {
    public boolean isInvestible() {
        return isCeo();
    }
}

这种方法将简化客户端代码。客户端代码不必知道如何计算“可投资性”的细节,也不再需要通过挖掘客户对象的状态 来打破得墨忒耳定律。

于 2009-02-15T15:02:54.200 回答
1

现在呢?当然,有一个很好的 OOP 方法可以做到这一点:

str.request(this);

太糟糕了,String 无法扩展,现在您正在考虑为每个 HttpInstruction 编写一个包含 8 个子类的 HttpInstruction 类。老实说,尤其是在谈论解析器时,这非常困难。

曾经尝试过 C# 扩展方法吗?字符串可以扩展。

于 2009-02-15T15:26:26.267 回答
1

我对 switch 语句的问题是,在现实世界的应用程序中,很少有独立存在的 switch 语句。

在我公司的代码库中需要重构的许多代码都会使整个类充满多个 switch 语句,因此您必须知道每个 switch 语句的存在。

最终,最干净的方式是将整个系统重构为策略模式,其中工厂控制基于 switch 语句的单个剩余副本的策略的创建。

由于时间限制,我们没有采取任何进一步的措施,因为这满足了我们的需求。仍然有一个巨大的 switch 语句,但只有一个,所以添加额外的策略只需要实现接口并将创建步骤添加到主 switch 语句。

于 2009-02-16T01:33:42.953 回答
1

首先,你的目标不应该是实现“好的 OO 风格”,而是好的代码。而“好”至少意味着正确、清晰、可读和尽可能简单。

所以我将把问题重新表述为:“使用 switch 是不是代码错误的标志?” ,因为这才是我真正关心的。现在我将继续回答它。

嗯,这是一个好问题 :)通常,使用一次开关并不表示代码不好。但是,如果您在课堂上的多个点打开相同的东西,那么最好考虑一种替代设计,在该设计中,您可以用子类来表示开关替代方案 - 当您考虑到这一点时,请特别问自己,如果这样创建的类将是一个当前类的专业化,并且将具有 is-a 关系。如果是这样,这为使用继承提供了更多要点。

最后一条评论:“使用 [语言特性 X]糟糕”非常接近于“语言设计者愚蠢地将 [语言特性 X] 包含在其中”。

于 2009-02-16T10:54:03.853 回答
0

case 语句几乎总是可以用多态性代替

并且

boolean investable = customer.isInvestable();

由于对 isInvestable 的调用是多态的,因此用于进行调用的实际算法取决于客户的类型。

我认为你们都错了。如果这只是希望获得商业贷款的客户的“可投资性”逻辑怎么办。也许客户对另一种产品的投资决策真的很不同,可能不是基于“类别”,而是基于他们住在哪里,是否已婚,他们在哪个行业工作?

另外,如果一直有新产品问世,每个产品都有不同的投资决策,我不想Customer每次都更新我的核心课程怎么办?

就像我说的,我并不是说switch总是好的——但同样它也可以是完全合法的。如果使用得当,它可以是一种非常清晰的编写应用程序逻辑的方式。

于 2009-02-15T15:16:00.410 回答
0

“另外,如果一直有新产品问世,每个产品都有不同的投资决策,我不想每次都更新我的核心客户类怎么办?”

这让我想起:

接口可投资
{
    boolean isIvestible(客户 c);
}

FooInvestible 类
    实现可投资
{
    public boolean isInvestible(最终客户 c)
    {
        // 任何逻辑,无论是开关还是其他东西
    }
}

最初使用 swtich 并添加新类型的决策的“问题”是,您可能最终会遇到一些无法以理智方式维护的巨大老鼠巢代码。将决策分成几类会迫使决策制定被拆分。然后,即使您使用 switch,代码也可能保持更清晰和可维护。

于 2009-02-15T17:50:19.557 回答