我已经看到它写在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是代码味道"这样的陈述是错误的.还有其他人同意吗?
我觉得这样的陈述
使用switch语句是糟糕的OOP风格.
和
案例陈述几乎总是可以用多态来代替.
过于简单化了.事实是,开启类型的 case语句是糟糕的OOP风格.这些是您想要用多态性替换的.打开一个值很好.
采取你的后续行动:
如果这只是希望获得商业贷款的客户的"可投资性"逻辑怎么办?或许客户对另一种产品的无可疑性决定真的很不一样......而且,如果有新产品一直出现,每种产品都有不同的可投资性决策,我不希望每次都更新我的核心Customer类.这发生了吗?
还有一条评论:
我并不完全确定逻辑是否靠近它运行的数据.现实世界并不像这样.当我要求贷款时,银行决定我是否有资格.他们不要求我自己决定.
你是对的,就这一点而言.
boolean investable = customer.isInvestable();
对于您所谈论的灵活性而言,它不是最佳解决方案.但是,最初的问题没有提到存在单独的产品基类.
鉴于现有的其他信息,最佳解决方案似乎是
boolean investable = product.isInvestable(customer);
可投资性决策由产品根据您的"真实世界"参数进行(多态!),并且还避免了每次添加产品时都必须创建新的客户子类.产品可以根据客户的公共界面使用它想要的任何方法进行确定.我仍然怀疑是否有适当的增加可以对客户的界面进行消除切换的需要,但它仍然是最不可能的所有邪恶.
但是,在提供的特定示例中,我很想做类似的事情:
if (customer.getCategory() < PRIME) { investable = customer.getSavingsAccount().getBalance() > 1e6; } else { investable = customer.isCeo(); }
我发现这比清单中列出每个可能的类别更清晰,更清晰,我怀疑它更有可能反映出"真实世界"的思维过程("它们是否低于素数?"与"它们是否为次级或中期" ?"),如果在某个时刻添加了SUPER_PRIME指定,它就避免了重新访问此代码.
在纯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
自身.
开关上的值是好的:OK,但switch语句是在大多数用于测试单一的情况下int
,char
,enum
,...值,而不是IF-THEN-ELSE地方范围和更奇特的条件可以进行测试.但是,如果我们分配这个单一的值,并且它不在我们的OO模型的边缘,如上所述,那么似乎交换机通常用于分派类型,而不是值.或者:如果你不能用一个开关替换if-then-else的条件逻辑,那么你可能没问题,否则你可能没有.因此我认为OOP中的切换是代码味道和声明
开启类型是糟糕的OOP风格,开启一个值就好了.
本身过于简单了.
并回到起点:一个switch
不错,它并不总是非常好.您不必使用OO来解决您的问题.如果你确实使用OOP,那么你需要特别注意开关.
这是糟糕的OOP风格.
并非所有问题都能通过OO解决.有些你想要模式匹配,哪个开关是穷人的版本.
如果有的话,我受够了人们描述这种编程风格-在一群干将的加入到"低挂"类型(客户信息,账户,银行)和有用的代码是"控制器在系统周围喷","助手"和"实用程序"类 - 以面向对象的方式.这样的代码是在一个面向对象的系统中的气味,你应该问为什么,而不致冒犯.
肯定的开关是差的OO,你不应该在函数中间放置一个返回值,魔术值是坏的,引用永远不应该为null,条件语句必须放在{braces}中,但这些都是准则.不应该虔诚地遵循它们.可维护性,可重构性和可理解性都非常重要,但实际完成工作的第二步.有时我们没有时间成为编程理想主义者.
如果任何程序员被认为是胜任的,那么应该假设他可以遵循指导原则并酌情使用可用的工具,并且应该接受他不会总是做出最好的决定.他可能会选择一条不太理想的路线或犯错,并遇到一个难以调试的问题,因为他选择了一个开关,可能他不应该拥有或传递过多的空指针.这就是生活,他从错误中学习,因为他很有能力.
我不虔诚地遵循编程教条.我认为在我自己作为程序员的背景下的指导方针并将其应用似乎是合理的.除非它们是手头问题的根本,否则我们不应该对这些类型的编程实践有所帮助.如果您想对良好的编程实践表达您的意见,最好在博客或适当的论坛(例如此处)中这样做.
罗伯特马丁关于开放封闭原则的文章提供了另一种观点:
软件实体(课程,模块,功能等)应该扩展,但需要修改.
在您的代码示例中,您实际上是在切换客户的"类别类型"
boolean investible ; switch (customer.getCategory()) { case SUB_PRIME: case MID_PRIME: investible = customer.getSavingsAccount().getBalance() > 1e6; break; case PRIME: investible = customer.isCeo(); break; }
在当前的气候下,新的客户类别可能会出现;-).这意味着必须打开这个类,并不断修改它.如果您只有一个switch语句可能没问题,但如果您想在其他地方使用类似的逻辑会发生什么.
而不是其他建议,在哪里isInvestible
制定方法Customer
,我会说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逻辑,每次添加另一个操作时都需要更新多个位置,或者我在此处看到的产品.