有没有人不同意这句话:“使用开关是不好的OOP风格”?

时间:2009-02-15 14:06:27

标签: java oop switch-statement

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

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

最后,交换机简洁明了:

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

22 个答案:

答案 0 :(得分:56)

我认为像

这样的陈述
  

使用switch语句是糟糕的OOP风格。

  

案例陈述几乎总是可以用多态来代替。

过于简单化了。事实是,打开类型的案例陈述是糟糕的OOP风格。这些是您想要用多态性替换的。打开就可以了。

答案 1 :(得分:17)

接受你的跟进:

  

如果这只是希望获得商业贷款的客户的“可投资性”逻辑怎么办?或许客户对另一种产品的无可疑性决定真的很不一样......而且,如果有新产品一直出现,每种产品都有不同的可投资性决策,我不希望每次都更新我的核心Customer类。这会发生什么时候?

和你的一条评论:

  

我不完全确定逻辑是否靠近它运行的数据。现实世界并不像这样。当我要求贷款时,银行决定我是否有资格。他们不要求我自己决定。

你是对的,就此而言。

boolean investable = customer.isInvestable();

不是您所谈论的灵活性的最佳解决方案。但是,最初的问题没有提到存在单独的产品基类。

鉴于现有的其他信息,最佳解决方案似乎是

boolean investable = product.isInvestable(customer);

可投资性决策由产品根据您的“真实世界”参数进行(多态!),并且还避免了每次添加产品时都必须创建新的客户子类。产品可以根据客户的公共界面使用它想要的任何方法进行确定。我仍然怀疑是否有适当的增加可以对客户的界面进行消除切换的需要,但它可能仍然是最不可能的所有邪恶。

在提供的特定示例中,我很想做类似的事情:

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

我发现这比清单中列出每个可能的类别更清晰,更清晰,我怀疑它更有可能反映“真实世界”的思维过程(“它们是否低于素数?”而不是“它们是次要的还是mid-prime?“),如果在某个时刻添加SUPER_PRIME指定,它可以避免重新访问此代码。

答案 2 :(得分:16)

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

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

切换可以表示您没有定义接近其运行数据的行为,并且没有利用子类型多态性。

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

最后,在编写OO程序时,当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本身。

打开一个值很好:好的,但是切换语句在大多数用于测试单个intcharenum,...值的情况下,而if-then-else则可以测试范围和更奇特的条件。但是,如果我们分配这个单一的值,并且它不在我们的OO模型的边缘,如上所述,那么似乎交换机通常用于分派类型,而不是值。或者:如果你可以用交换机替换if-then-else的条件逻辑,那么你可能没问题,否则你可能没有。因此,我认为OOP中的开关是代码味道,而声明

  

开启类型是糟糕的OOP风格,   打开一个值很好。

本身过于简单。

回到起点:switch并不坏,它并不总是非常好。您不必使用OO来解决您的问题。如果你确实使用OOP,那么你需要额外注意开关。

答案 3 :(得分:14)

这是糟糕的OOP风格。

并非所有问题都能通过OO解决。有些你想要模式匹配,哪个开关是穷人的版本。

答案 4 :(得分:12)

如果有的话,我厌倦了描述这种编程风格的人 - 其中一堆getter被添加到“低挂”类型(Customer,Account,Bank)中,有用的代码喷洒在系统周围在“控制器”,“帮助器”和“实用程序”类中 - 作为面向对象。像这样的代码在OO系统中是的气味,你应该问为什么而不是冒犯。

答案 5 :(得分:7)

当然开关很差OO,你不应该在函数中间放置一个返回值,魔术值是坏的,引用永远不应该为null,条件语句必须放在{braces}中,但这些都是指南。不应该虔诚地遵循它们。可维护性,可重构性和可理解性都非常重要,但实际完成工作的第二步。有时我们没有时间成为编程理想主义者。

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

我不虔诚地遵循编程教条。我认为在我自己作为程序员的背景下的指导方针并将其应用似乎是合理的。除非它们是手头问题的根本,否则我们不应该对这些类型的编程实践有所帮助。如果你想对良好的编程实践表达你的观点,最好在博客或相应的论坛(例如这里)中这样做。

答案 6 :(得分:6)

罗伯特·马丁关于Open Closed Principle的文章提供了另一种观点:

  

软件实体(课程,模块,功能等)   应该开放扩展,但是关闭   改性。

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

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

在当前的气候下,新的客户类别可能正在涌现;-)。这意味着必须打开这个类,并不断修改它。如果您只有一个switch语句可能没问题,但如果您想在其他地方使用类似的逻辑会发生什么。

我认为Cagtegory应该成为一个完全成熟的类,并用于做出这些决定,而不是其他建议,其中isInvestibleCustomer上成为一种方法:

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

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

答案 7 :(得分:5)

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

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

答案 8 :(得分:5)

我确实认为开启类型是代码味道。但是,我分享了您对代码中关注点分离的担忧。但是那些可以通过许多方式解决,这些方法允许你仍然使用多态性,例如访客模式或类似的东西。阅读Gang of Four的“设计模式”

如果像 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);
      }
    };

这看起来有些过分,但是当您需要将操作作为集合处理时,它可以轻松地为您节省大量代码。例如。在列表中显示所有这些内容并让用户选择一个。如果将操作定义为功能,您很容易就会遇到大量硬编码的交换机案例逻辑,每次添加其他操作时都需要更新多个位置,或者我所看到的产品这里。

答案 9 :(得分:3)

我将switch语句视为if / else块的可读替代方法。

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

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

使VB6如此成功的一件事,奇怪的是,它是基于对象的,而不是面向对象的。所以,我会说实用程序员总是将概念结合起来。只要已经编程好的封装,开关也可以带来更易于管理的代码。

答案 10 :(得分: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质量的bugtrackers,至少,更好的集成,git质量的版本管理,等等。而且,嗯,你周围的人可以使用所有这些东西并知道如何处理所有这些事情。最重要的是:一种美妙的语言,可以为优雅的解决方案提供棘手的问题。

所以,很有可能,你坚持使用Java,这使得很难在所有情况下都能很好地替换掉交换机。自我会有一个优雅的解决方案。但是你没有使用Self,如果你是,你的同事将无法阅读它,所以忘了。

现在找到妥协。

很遗憾,我知道。

答案 11 :(得分:2)

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

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

答案 12 :(得分:2)

我认为switch语句是否是一个糟糕的OOP实践取决于你使用switch语句的位置。

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

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

答案 13 :(得分:2)

来自外部资源的数据本身不能真正面向对象,因为您没有引入代码。如果它包含案例你就会有案件。周期。

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

答案 14 :(得分:2)

解决你的库也是一种代码味道。你可能没有选择,但这并不是一个好习惯。

答案 15 :(得分:1)

  

现在呢?当然,有一个很好的   OOP方式:

     

str.request(本);

     

太糟糕了,String无法扩展   现在你正在考虑写一个   HttpInstruction类有8个   每个HttpInstruction的子类。   老实说,尤其是在交谈时   关于解析器,它只是   非常困难。

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

答案 16 :(得分:1)

案例陈述几乎总是可以用多态来代替。

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

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

此方法将简化客户端代码。客户端代码不必知道如何计算“可调查性”的细节,并且不再需要通过深入了解Customer对象的状态来中断Law of Demeter

答案 17 :(得分:1)

是的,我已经厌倦了人们告诉你这种风格很糟糕。

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

答案 18 :(得分:1)

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

许多需要在我公司的代码库中进行重构的代码会使整个类充满多个switch语句,这样你就必须知道每个switch语句的存在。

最终,最简洁的将整个系统重构为策略模式,工厂根据switch语句的单个剩余副本控制策略的创建。

由于时间限制,我们没有采取任何进一步措施,因为这符合我们的需求。还有一个巨大的巨型switch语句,但只有一个,所以添加其他策略只需要ipmlementing接口并将创建步骤添加到master switch语句。

答案 19 :(得分:1)

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

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

嗯,这是一个很好的问题:) 通常,使用一次开关不是坏代码的标志。但是如果你在班级的几个点上开启相同的东西,最好考虑一个替代设计,在这个设计中你用子类代表交换选择 - 当你考虑到这一点时,尤其要问问自己,如果这样创建的类是当前类的特化并且将具有is-a关系。如果是这样,这为使用继承提供了更多的要点。

最后一条评论:“在所有中使用[语言功能X] 是不好的”危险地接近“语言设计者愚蠢地将[语言特征X]包含在其中”。

答案 20 :(得分:0)

  

案例陈述几乎总是可以用多态性替换

boolean investable = customer.isInvestable();
     

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

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

此外,如果有新产品一直在推出,每个产品都有不同的可投放性决策,我不想每次都更新我的核心Customer课程呢?

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

答案 21 :(得分:0)

“此外,如果有新产品一直在推出,每个产品都有不同的可投资性决策,我不希望每次发生这种情况时都更新我的核心客户类会怎么样?”

这让人想起:

interface Investable
{
    boolean isIvestible(Customer c);
}

class FooInvestible 
    implements Investible
{
    public boolean isInvestible(final Customer c)
    {
        // whatever logic, be it switch or other things
    }
}

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