`

过多if-else分支的优化

阅读更多

我想谈一谈这个话题是因为我的上一篇博客在ITEye上有一些朋友回复,说if-else过多的分支可以使用switch或者责任链模式等等方式来优化。确实,这是一个小问题,不过我们还是可以整理一下这个小问题的重构方式。

为什么要优化?

你没有看错。这是要放在第一条谈论的。

有许多人会说,叠起来一堆if-else分支,代码就不优雅了。可是,怎样去定义“优雅”的概念呢?再退一步说,即便不“优雅”,又有什么问题?

对于这样一段再普通不过的代码:

1
2
3
4
5
6
7
8
int code;
if("Name".equals(str))
    code = 0;
else if("Age".equals(str))
    code = 1;
else if("Address".equals(str))
    code = 2;
...

可以有好多种重构方式,但是使用这样的代码,虽然简陋,但在大多数情况下,并不会影响什么,比如,对可维护性没有影响。当然,如果你发现其中确有不好的一面,那就要考虑重构它。换言之,通常你首先要说出某段代码的问题(比如,你觉得这段代码不符合开闭原则,因为你希望保持这段代码闭合稳定),那么才去存在重构的必要,而不要总是使用“优雅”和“简洁”搪塞疑问。几乎所有的书上都说要写出优雅的、简洁的代码,这本身无可厚非,但是事物需要使用自己的判断,可不要被习惯性地洗了脑。

在我前一家公司,是典型的通讯和传统软件的公司,代码质量普遍不错,但是很多时候,会看到许许多多不够优雅的代码——也许你觉得不够简洁、美观,但是下代码严谨、清晰,我觉得这就很好。反之,某一些精巧的设计,可能会带来可阅读性和可理解性下降的问题。

寻找代替分支判断的方式

接下去我们再来考虑怎么样去重构优化过多的if-else分支。

程序逻辑最基本的组成就是分支、判断和循环。而过多if-else正是由于在某一个变化的点上,有许多判断条件和结果分支造成的。所以最基本的解决办法就是把多个判断条件合成一个,也就是把若干个分支合成一个。

但是在大多数情况下,条件判断的分支都是无法合并的。所以,我们需要把这个变化点通过别的途径封装起来,而不是采用if-else。

1. 用一个Map可以做到,if-else的变化点使用Map的get方法来代替:

1
2
3
4
5
6
Map typeCodeMap = new HashMap();
typeCodeMap.put("Name", 0);
typeCodeMap.put("Age", 1);
typeCodeMap.put("Address", 2);
...
int code = typeCode.get(type);

2. 枚举:

1
2
3
4
5
6
7
8
9
10
11
public enum Codes {
    Name(0), Age(1), Address(2);
     
    public int code;
    Codes(int code){
        this.code = code;
    }
}
 
//使用:
int code = Codes.valueOf(str).code;

3. 多态:

1
2
ICode iCode = (ICode)Class.forName("com.xxx." + str).newInstance();
int code = iCode.getCode();

当然,如果仅考虑从String转向int这样的转换,用这样的方式来简化分支判断逻辑,这个方式、这个例子不是很恰当。当然,这样的方式经常被用来做从字符串到具体对象的转换。

还有一些朋友说的这个模式那个模式来解决多if-else的问题,这些都是正确的,当然本质上也无一例外基于多态来实现的,所以我就不提及了。这些都不错,至少比那些老说用switch来代替if-else的有价值多了 :)

最后,对于如此小的一个问题,我要补充说明的一点是,看不得大片if-else和看不得大片new关键字一样,我觉得这是许多Java程序员的既有观念或者说习惯,甚至通病——这并不好。Java最有价值的地方不是它的语义语法也不是它的虚拟机跨平台和有多高性能,而在于它的社区它的无比丰富的类库,在于使用它的人可以从设计上和宏观上去思考问题。但是Java程序员,也包括我在内,很容易把这条路走得过于极端,比如遍地的Factory,比如漫山遍野的配置,比如永远也不会被复用的可复用代码,比如永远也不会被扩展的可扩展代码,还比如从前到后由内到外的分层,一层又一层。相对于这些方面无止境的追求,我们还是专注于要解决的问题,多写一些清晰可用的代码吧。

 

文章系本人原创,转载请保持完整性并注明出自《四火的唠叨》

20
5
分享到:
评论
3 楼 runfriends 2013-02-25  
这些重构方案只能作为等值判断的if-else的重构。
绝大多数情况下,都是范围比较,不等值比较,多条件综合判断,不同类型、不同属性的值出现在不同的if-else分支里,而把每一个分支封装到不同的类里又过于琐碎,似乎不值得这样做。在这些情况下,有什么重构方案,既能保证代码美观、优雅、易维护,又能做到减少if-else判断呢?
2 楼 lazynote 2013-02-25  
kidneyball 写道
赞成楼主“不要看到一堆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。我个人认为问题不大,两者的实现难度也没有差别,实现需求就行)

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




能用代码诠释一下你的观点吗?我没有完全get到你的思想。
1 楼 kidneyball 2013-02-25  
赞成楼主“不要看到一堆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。我个人认为问题不大,两者的实现难度也没有差别,实现需求就行)

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



相关推荐

    verilog中多个else_if级联造成的综合电路的低效率及解决办法

    ### Verilog中多个else_if...通过使用多个`if_else`或`case`语句等方法替代`else_if`级联,可以在很大程度上优化这些问题,从而得到更加高效的硬件设计。对于工程师来说,理解这些细节并在实践中灵活运用是非常重要的。

    if_else&case;语句分析.pdf

    case语句通常用于替代多个if-else if条件分支,特别是在没有逻辑优先级需求的情况下。它是一种并行结构,所有的条件分支都是同时被检查的。这样可以避免逻辑级数过深导致的性能问题。文档中提到,尽管在RTL...

    使用策略模式改造if分支过多的方法

    在软件设计中,面对复杂的业务逻辑,特别是涉及多种条件判断的情况,传统的做法是使用if-else语句或者switch语句来实现。然而,随着需求的不断变化,这种做法可能导致代码结构变得臃肿且难以维护。在这种场景下,...

    如何优雅的替换掉代码中的ifelse

    然而,当条件分支过多时,`if-else`结构会导致代码变得冗长且难以维护,尤其是在大型项目中,这样的代码往往被称为“烂代码”。解决这个问题的一种优雅方式是采用策略模式、工厂模式或映射表等设计模式和技术手段。 ...

    if.rar_If..._跳转

    - **扩展形式**:`if...else` 和 `if...else if...else` 结构,用于处理多个条件分支。 - **嵌套使用**:`if` 语句可以嵌套在其他 `if` 语句中,形成多层条件判断,以实现更复杂的逻辑。 2. **跳转语句** - **...

    2 状态模式-MOOC课程内容.pdf

    当对象方法中存在大量依赖于对象状态的条件判断语句,如if-else或switch-case时,状态模式能将每个条件分支放入独立的类中,从而优化代码结构,提高可维护性和扩展性。 状态模式的主要参与者包括以下几个部分: 1....

    Python 语言程序设计3.docx

    - `if-elif-else` 结构允许实现多分支决策,可以处理更复杂的逻辑,避免过多嵌套,提高代码可读性。 4. **循环结构**: - `for` 循环常用于遍历序列(列表、元组、字符串等),例如计算平均数。循环变量会依次取...

    PIcC.rar_If..._PIC_picc _picc pdf

    描述中提到的“PIC C中高效率的循环if eles 语句.pdf”进一步细化了主题,它聚焦于在PIC C编程中如何通过优化`if...else`语句来提升循环的执行效率。在嵌入式系统中,尤其是资源有限的微控制器环境中,代码的效率至...

    JavaScript优化以及前段开发小技巧

    过多的if-else或switch语句会影响性能。惰性模式是一种解决方案,它延迟执行判断,直到实际需要时才进行。例如,当检测到第三方应用不支持localStorage时,可以使用惰性模式立即或首次使用时重定义获取和设置方法,...

    arm-instruction.zip_ARM instruction bne_BNE arm instruction

    BNE指令常用于循环、条件分支结构(如if-else)、错误处理和中断服务例程中。例如,在一个简单的for循环中,可以使用BNE指令在迭代条件不满足时跳出循环。 **优化技巧:** 1. **减少分支预测错误:** 由于现代...

    php 代码优化指南,给你的php加速

    减少代码中的条件分支(如if语句)数量可以提高代码执行速度。在可能的情况下合并条件分支。 #### 26. 优化对象创建 频繁创建和销毁对象会增加垃圾回收的压力。尽量重用对象或使用对象池技术来管理对象生命周期。 ...

    C语言程序优化.pdf

    文档提到了几种循环结构:`while()`, `if...else`, `do...while` 和 `for`。循环是编程中常用的结构,但是它们也会占用较多的CPU周期。循环优化主要包括减少循环次数、消除不必要的循环、循环展开等。例如,使用`do....

    PMD错误翻译整理

    2. 简化布尔返回值(SimplifyBooleanReturns):应当避免不必要的if-else语句结构,当返回布尔值时,如果逻辑判断简单直接,则可以省略复杂的if-else结构,直接返回判断结果。 3. 简化布尔表达式...

    程序设计基础样本.doc

    4. **限制使用 goto 语句**:过多的 goto 语句可能导致代码难以理解和调试,提倡使用结构化的控制流程(如 if-else,switch,for,while等)。 **构造化程序的基本构造**包括: - **顺序构造**:语句按照出现的顺序...

    Matlab中的goto函数

    3. **复杂条件分支**:当需要根据多个条件执行不同分支时,`goto`可以避免嵌套过多的`if-else`结构,使代码更简洁。 ```matlab check_conditions: if condition1 % 执行相应操作 elseif condition2 % 执行相应操作...

    Vue02.pdf

    - `v-else`和`v-else-if`:配合`v-if`进行条件分支。 - `v-for`:用于遍历数据并渲染列表。 - `v-on`(或简写`@`):事件监听器。 - `v-bind`(或简写`: `):属性绑定。 - `v-pre`:跳过该元素及其子元素的编译。 -...

    C语言程序编写规范,简化版

    - 尽可能避免复杂的if-else分支结构。 **3.2 避免使用GOTO:** - 不推荐使用GOTO语句,因为它可能导致程序流程混乱。 **3.3 IF语句只执行单一操作:** - IF语句内部只执行单一操作,避免IF内部包含多个语句。 *...

    阿里巴巴Java编程规范.pdf

    2. **switch-case语句**:避免过多的case分支,考虑使用if-else或策略模式替代。 3. **循环语句**:循环体内的逻辑应尽量简洁,避免复杂的嵌套循环。 ##### (八)注释规约 注释规约旨在提高代码的可读性和可维护...

    用C语言编写高效的ARM程序

    循环是程序中常见的结构,但在ARM架构下,过多的循环控制语句会导致性能下降。通过循环展开技术,可以减少每次循环的迭代次数,从而降低循环控制的开销。 #### 3.3 优化数据类型和存储访问 在ARM上,访问不同类型...

Global site tag (gtag.js) - Google Analytics