关于if else优化

(一)

赞成楼主“不要看到一堆if就想优化”的观点。但对楼主的论证过程有异议:

首先那位朋友的问题是“一堆if怎么优化”(其实应该说怎么重构提高可读性,一堆if如果能满足需求,那已经是最优的了),他没有问怎样的if需要优化。因此我觉得应该假设他已经判断过是否应该重构,但不知道具体手段。至于他怎么判断出来的,我们不知道,但不应该假设他肯定是因为“优雅强迫症”而决定重构。

其次感觉楼主举的例子没有切中要点。用查表代替硬编码的赋值是一种很常见的做法,但硬编码赋值只是if的一种非常特殊的用法。特殊到只要超过三四个分支,一般人都会想到用查表来代替,导致在实际场景根本不会有这样一段代码等着你去重构。因此我觉得应该假定那位朋友问的是“对于复杂的if分支中存在复杂处理的情况如何重构”

我觉得要正面回答这个问题,可以先不要管我回复的责任链之类的大词,直接来一步步看这种情况该怎么重构。

首先,既然每个if分支后面跟着一堆复杂的逻辑,每个分支里做的事情肯定不同,把它们堆在一个方法中并不妥当(如果楼主想讨论为什么不妥当,可以另外讨论,最常见的不妥当是因为各个分支的抽象层次不同导致阅读者思路混乱)。因此最好把各个分支的处理代码分别抽出来,分别形成独立的方法。这样每个分支处理都有明确的边界,而且我们可以在方法上写javadoc,形成良好的文档。

好,现在你有了一个单纯含有if分支的主控方法和一堆执行处理的方法,你面临的第二个问题是每个方法上都要传入一大串参数,因为原来的复杂处理往往依赖大量的上下文状态。解决这个问题的最正统有效(未必优雅)的方法是创建一个上下文(Context)类,或者也可以叫请求(Request)类来携带传入参数。这样可以解决参数文档问题,默认值问题和参数顺序问题。但你有许多个执行方法,显然你不会傻到为每个方法都创建一个上下文,而会只创建一个上下文类,每个方法都接收这个上下文实例,只取自己真正关注的属性。毕竟既然这些执行方法都从一个if结构中抽取处理,这些属性之间逻辑上总有些关联,放在一起也不会有很大问题。

现在你有了一堆参数一致,但名称不同的方法。如果你的需求变动不大,就这样就可以了。但如果你觉得需求可能会有变化,未来可能需要覆盖其中一些方法。你会发现,如果需求1需要你覆盖A,你需要创建一个子类。需求2需要你覆盖B,又要创建一个子类。需求3需要你同时具有需求A,需求B的特性,你又要创建一个子类。既然这样,何不把它们抽到独立的类中,可以分别扩展? 抽取过程中,你发现现在每个处理类都只有一个方法,方法名和类名是重复的。而且本质上它们都是某种处理器(Handler),何不让它们实现统一的接口,方法名统一改为handle。强调一下,这一步是预期需求会有变化的情况才做,如果认为需求不太可能会变化,或者预计变化有足够时间重构,完全可以在前一步就停止。

好,现在你有一个主控方法,这个方法创建一个上下文对象,再根据分支条件分别调用不同Handler子类上的handle方法,传入这个上下文。你注意到一个问题,分支条件本身和对应的处理逻辑是内聚的。如果条件发生变化,处理往往也要发生变化。反之依然。而且你读代码时,读到一个复杂的条件,往往不能轻易看出它要判断什么,这时最好的方法就是直接看看对应Handler的命名和文档,从处理方式反推这个条件对应的业务需求。既然这样,何不干脆把条件都搬到Handler里去,让每个Handler根据传入的上下文,看看在当前状态下自己是否应该执行处理。

现在你得到了一个主控类,这个类持有一堆Handler实例,主控类创建一个上下文,然后把上下文依次传给各个Handler,Handler自行判断是否应该执行自己的处理。

到了这一步,其实已经差不多了。不过对于某些人,他在进行前一步的重构时,就会醒悟:主控类现在已经变成了一个单纯的任务转发人(分配者)。它根本没有必要持有一个Handler的列表再分别逐个调用,还要管理该继续还是该中断等等逻辑(这些逻辑是依赖每个Handler返回的标志来决定的)。何不让Handler自己负责把控制向后分发,主控类只需要知道领头的那个Handler最终会把事情处理好就行了。这种结构还有一个好处,就是每个Handler可以自行决定是否该往下传递控制,还可以根据需要替换上下文实例的实现来影响后续的处理。(这一步与上一步是二选一,有些人喜欢在主控类中持有Handler队列,有些人喜欢链式Handler。我个人认为问题不大,两者的实现难度也没有差别,实现需求就行)

最后,我们为了交流方便,把这种组合方式称为“责任链”。

(二)

我的看法是,如果一个复杂if-else结构所涉及的条件是业务需求带来的,我倾向于把它做成责任链,因为这样一个复杂的分支结构说明这里有一个比较重要的业务决策点。一般来说,一个系统如果设计得好的话,纯粹由业务需求形成复杂if嵌套的地方不会太多,别忘了我们有很多其他方法(例如把常用判断抽成工具方法、使用策略模式等等)消除重复出现的if结构。而且责任链实现起来也很方便,整个系统可以使用同一的责任链基类,具体子类只要实现handle方法就行,如果没有明确的复用需要,做成私有静态内部类就行了。这样以后如果需要复用可以随时抽出去。等将来Java8流行起来,可以用Lambda表达式来写,就更方便了。

另一方面,只要能足够肯定分支逻辑不会有太大变动,各个分支的处理也很简单,保留if-else也很直观。如果嵌套得太深,或者条件太复杂影响阅读,可以考虑以下一些手段来改写:

1. 超过三层的嵌套,可以考虑把内部嵌套抽成独立方法。一般来说,超过三层的嵌套,阅读代码时已经很难直观地通过阅读条件来理解对应的需求了。这时阅读者往往需要根据处理逻辑反推条件的含义,所以即使内部处理还是一个if-else分支,你也应该考虑加点注释说明这里的处理逻辑在需求层面的实际含义,方便读者理解前面的条件。但与其写内部注释(我个人很不喜欢在方法中写内部注释),不如干脆就把这部分代码抽出来形成独立的方法,起个好点的名字。

2. 合理使用return。虽然有些观点说一个方法只有一个出口比较好。但我在实践中没有发现这种做法有任何实质性的好处,反而无谓地增加了代码嵌套层数。可能这种观点的背景是长函数盛行的结构化编程年代。只要方法足够简短,适当使用return可以有效减少if的嵌套层数,使方法更加易读。特别是在方法开头就进行一些判断直接return,是在各种开源项目中都非常常见的做法。

3. 注意不要做无谓的判断。最常见是无用的非空判断,经常会看到在同一个方法里,之前已经直接调用过某个对象变量上的方法,然后这个变量一直没有修改,后面还在做这个变量的非空判断。如果这个变量为空早就抛空指针了。空指针判断要做的话,尽量在方法早期做。尽量判断为空后进行分支处理,要么抛异常,要么return,要么设默认值。主线逻辑可以写在if外部。

4. 如果一个if里的条件比较复制,可以考虑把条件单独抽成局部变量或独立方法。多个连续嵌套的if可能的话尽量用&&或||代替。

5. 如果用Intellij的话,IDE提供了自动合并多个嵌套if, 简化if(自动去掉恒真或恒假的条件,或者把分支内返回布尔值的if转换为三元表达式等等), 复杂条件自动加括号,条件取反等功能。可以尝试重构一下看看哪种表达方式更易读。


转自:http://raychase.iteye.com/blog/1814187中kidneyball的两篇回复

责任链设计模式(也即过滤器、拦截器)。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值