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

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

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

最后,交换机简洁明了:

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

采取你的跟进:

如果这只是希望获得商业贷款的客户的“可投资性”逻辑,该怎么办? 或许客户对另一种产品的无可疑性决定真的是完全不同……另外,如果有新产品一直出现,每种产品都有不同的可投资性决策,我不希望每次都更新我的核心客户类这发生了吗?

还有一条评论:

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

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

 boolean investable = customer.isInvestable(); 

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

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

 boolean investable = product.isInvestable(customer); 

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

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

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

我觉得这比清单中列出每个可能的类别更清晰,更清晰,我怀疑它更有可能反映“真实世界”的思维过程(“它们是否低于素数?”与“它们是次级或中期” ?“),如果在某个时刻添加了SUPER_PRIME指定,它就避免了重新访问此代码。

我觉得这样的陈述

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

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

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

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

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

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

使用OO语言时,您不必以OO方式编程。 因此,如果您选择使用更多function或基于对象的编程风格(例如,使用仅包含数据但没有行为的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 :很棒,多态。 但是现在您将这种逻辑与客户联系起来,并且您需要为每种类型的客户创建一个子类来实现不同的行为。 上次我检查时,这不是应该如何使用inheritance。 您可能希望客户类型成为客户的财产,或者具有可以决定客户类型的function。

双重调度:多态性两次。 但是你的访客类基本上仍然是一个很大的转变,它有一些与上面解释的相同的问题。

此外,遵循OP的示例,多态性应该是客户的类别,而不是Customer本身。

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

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

本身过于简单了。

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

这是糟糕的OOP风格。

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

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

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

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

我不虔诚地遵循编程教条。 我认为在我自己作为程序员的背景下的指导方针并将其应用似乎是合理的。 除非它们是手头问题的基础,否则我们不应该对这些类型的编程实践加以强调。 如果您想对良好的编程实践表达您的意见,最好在博客或相应的论坛(例如此处)中这样做。

罗伯特马丁关于开放封闭原则的文章提供了另一种观点:

软件实体(课程,模块,function等)应该是扩展,但是为了修改而关闭。

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

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

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

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

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

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

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

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

如果您的核心对象(如客户)在大多数情况下保持固定,但操作经常更改,那么您可以将操作定义为对象。

  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逻辑,每次添加其他操作时都需要更新多个位置,或者我在此处看到的产品

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

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

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

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

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

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

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

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

 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,如果你是,你的同事将无法阅读它,所以忘了。

现在找到妥协。

我知道,这很难过。

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

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

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

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

除此之外,OOP不是一颗银弹。 有时它是答案,有时候不是。

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

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

这种方法将简化客户端代码。 客户端代码不必知道如何计算“可投资性”的细节,并且不再需要通过深入了解客户对象的状态来破坏Demeter法则 。

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

str.request(本);

太糟糕了,String无法扩展,现在你正在考虑为每个HttpInstruction编写一个带有8个子类的HttpInstruction类。 老实说,特别是在谈论解析器时,它实在是非常困难。

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

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

编辑:在修复问题之前,这更有意义。

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

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

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

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

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

因此,我将重新阐述问题: “使用交换机是不良代码的标志吗?” 因为那才是我真正关心的。 现在我将继续回答它。

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

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

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

并且

 boolean investable = customer.isInvestable(); 

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

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

此外,如果有新产品一直出现,每个产品都有不同的可投资性决策,我不希望每次发生这种情况时都更新我的核心Customer类?

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

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

这让人想到:

界面可投资
 {
     boolean isIvestible(Customer c);
 }

类FooInvestible 
    实施Investible
 {
     public boolean isInvestible(最终客户c)
     {
         //无论是什么逻辑,无论是转换还是其他事情
     }
 }

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