• 重构必备技能之条件表达式


    本次博客的内容,我将带领大家一起来看看我们日常编码工作中遇到最多的一个语法——条件表达式。他在我们的逻辑流程控制中扮演着不可或缺的重要角色,那么怎样才能够写出高质量的条件表达式,高质量的条件分支呢?

    备注:本次讲解的例子只具备普遍性,不具有特殊性,针对一些特殊的业务逻辑,需要根据业务的实际情况灵活处理,切不可死记硬背,把这些重构方法硬套在具备某些特征的场合中。

    1.分解条件表达式

      首先我们来看一段代码,如果你觉得这段代码和你平常的写法没什么差别,那么就说明你的编码习惯就有待优化咯思密达,你就需要继续往下看咯:

    1         if (date.before(SUMMER_START) && date.after(WINTER_END))
    2         {
    3             charge = quantity * winterRate + winterServiceCharge;
    4         } else
    5         {
    6             charge = quantity * summerRate;
    7         }
    View Code

      这段代码所包含的逻辑很简单,但是有什么可以优化的地方呢?那么我们再看看下面这段代码:

    1         if (notSummer(date))
    2         {
    3             charge = winterCharge(quantity);
    4         } else
    5         {
    6             charge = summerCharge(quantity);
    7         }
    View Code

      甚至可以这样缩写:

    1         charge = notSummer(date) ? winterCharge(quantity) : summerCharge(quantity);
    View Code

      看到下面两种写法与上面的写法以后,你心里会想到什么呢?程序之中,复杂的条件逻辑是最常见的导致复杂度上升的位置之一虽然说这种重构相对于其他大型重构来说太渺小,但是殊不知再大的难读的代码都是通过这种小问题累加而成的。遇到这种情况下,我们只需要遵循一个原则:分支逻辑与操作细节分离。分支的目的在于控制程序的流向,重点不在于逻辑控制的细节,操作的细节那是方法所应该做的事情。程序要做到最小原子性,单一原则,那么你的程序的可读性,可复用性就会提高很多。

      这种重构手法主要适用的场景:条件分支中,条件判断很复杂,一般包含两个或以上逻辑操作,并且不通的流程分支都有不同的业务流程控制,那么这种重构手法就可以派上用场了。

      遇到这种问题的一般做法就是将条件判断部分,每一个分支的逻辑处理部分都单独的提成一个方法,这样就能大大提高程序的可读性,使代码结构更加清晰,这也是代码重构工作中很重要的一个环节,也是面向接口编程,面向对象编程的一个重要体现。

    2.合并条件表达式

       看到这里很多的小伙伴们可能就有疑问了,上面是要分解条件表达式,我也看懂了博主的意思,确实是那么回事儿,那么为何下面又要合并条件表达式呢?很费解!那么这里我也就先卖个关子,照旧,先看代码:

     1     public String getStuLevel(int score)
     2     {
     3         if (score == 100)
     4         {
     5             return "A";
     6         } else if (score >= 90)
     7         {
     8             return "A";
     9         } else if (score >= 80)
    10         {
    11             return "B";
    12         } else if (score >= 70)
    13         {
    14             return "B";
    15         } else if (score >= 60)
    16         {
    17             return "C";
    18         } else
    19         {
    20             return "D";
    21         }
    22     }
    View Code

      这个代码估计应该能够看出可以优化的地方(这里主要讨论的是重构手法,部分代码书写可能没有遵循编码规范,请知),那么看看下面的优化结果是不是和你所想的一致:

     1     public String getStuLevel(int score)
     2     {
     3         if (score >= 90)
     4         {
     5             return "A";
     6         } else if (score >= 70)
     7         {
     8             return "B";
     9         } else if (score >= 60)
    10         {
    11             return "C";
    12         } else
    13         {
    14             return "D";
    15         }
    16     }
    View Code

      看到这里,很多小伙伴都会说,这不是小儿科嘛,用脚趾头想都能想到要这样优化。哈哈,确实很简单我就不多废话,其中的好处我相信大家都也能看出来。

      这种重构手法主要适用的场景:在条件分支中,虽然检查的条件不一致,但是最终所流向的行为是一致的,那么我就可以将这些表现行为一致的分支逻辑合并起来,结合上面的“分解条件表达式”的重构手法,合并不必要的逻辑条件判断,从而提高代码的可读性。

    3.合并重复的逻辑片段

      在实际开发中我们可能会遇到如下这种代码冗余的情况,遇到这种场景,你首先就应该想到能不能重构:

     1     public void takeBus(double high)
     2     {
     3         double price = 0.0d;
     4         if (high > 1.2)
     5         {
     6             price = 100;
     7             takeBus();
     8         } else
     9         {
    10             price = 100 * 0.5;
    11             takeBus();
    12         }
    13     }
    View Code

      优化结果如下:

     1     public void takeBus(double high)
     2     {
     3         double price = 0.0d;
     4         if (high > 1.2)
     5         {
     6             price = 100;
     7         } else
     8         {
     9             price = 100 * 0.5;
    10         }
    11         takeBus();
    12     }
    View Code

      这种和上面的例子也是一样,都是小儿科,这种优化在于鉴别出"执行方式不随条件变化而变化"的代码。

      这种重构手法主要适用的场景:一组条件表达式的所有分支都执行了一段相同的逻辑。如果是这样,你就应该将这段代码移动到条件表达式外面。这样,代码才能更清楚地表明哪些东西是随着条件变化而变化的,哪些东西是永远都不会变化的。

    4.移除控制标记

      当我讲到这种重构手法的时候,对于那些刚从C转到JAVA或者长期从事结构化编程的人员来说,就很重要了。这一点重构手法能够让你摆脱结构化编程的“单一原则(一个程序只能有一个入口和一个出口)”的束缚。

     1     public static void main(String[] args)
     2     {
     3         boolean flag = true;
     4         for (int i = 0; i < args.length; i++)
     5         {
     6             if (flag)
     7             {
     8                 if ("ZhangGe".equals(args[i]))
     9                 {
    10                     flag = false;
    11                 }
    12             }
    13         }
    14     }
    View Code

      这种编码习惯很常见,尤其是在当前的多线程编程中。这里我们就把多线程编程当做一种特殊情况处理吧,至于为什么不在本篇博客的讨论范围之内。对于这种代码的优化我相信大家都已经想到了办法吧。

     1     public static void main(String[] args)
     2     {
     3         for (int i = 0; i < args.length; i++)
     4         {
     5             if ("ZhangGe".equals(args[i]))
     6             {
     7                 return;
     8             }
     9         }
    10     }
    View Code

      这样的控制标记带来的麻烦现在已经远远超过于它所带来的方便了。人们之所以会用这种方式是因为结构化编程原则告诉我们:每一个子程序只能有一个入口和一个出口。虽然说我们在编码的过程中要遵循“单一原则”。但是“单一出口”原则会让我们在代码中加入这样的boolean类型的标志,大大降低了条件表达式的可读性,感觉有种条件表达式滥用的感觉。这也就是为什么相JAVA这样的语言要引入return、break和continue这些关键字的原因。

      这种重构手法主要适用的场景:在循环体以内有一个表达式或者标志用来判断循环条件是否继续的场景,通过修改条件表达式或变量来控制循环体是否继续执行。这种结构的代码都可以通过break,continue和return等方式结束循环,删除标志控制,提高程序的可读性。

    5.以卫语句取代条件表达式

      首先看看下面这段代码:

     1     public double getPayAmount()
     2     {
     3         double result;
     4         if (idDead)
     5         {
     6             result = deadAmount();
     7         } else
     8         {
     9             if (isSeparated)
    10             {
    11                 result = separatedAmount();
    12             } else
    13             {
    14                 if (isRetired)
    15                 {
    16                     result = retiredAmount();
    17                 } else
    18                 {
    19                     result = normalPayAmount();
    20                 }
    21             }
    22         }
    23         return result;
    24     }
    View Code

      有人看到这段代码后,心里面会有种怪怪的感觉,觉得这代码感觉确实是有优化的地方,但是就是找不出来怎么优化,如果你一眼就看出来怎么优化,so you did a good job! 那我们就来看看怎么优化这段代码吧:

     1     public double getPayAmount()
     2     {
     3         if (idDead)
     4         {
     5             return deadAmount();
     6         }
     7         if (isSeparated)
     8         {
     9             return separatedAmount();
    10         }
    11         if (isRetired)
    12         {
    13             return retiredAmount();
    14         }
    15         return normalPayAmount();
    16     }
    View Code

      哈哈,这里的优化是不是和你所想的一样呢?或者说你有什么更好的见地?

      下面来总结一下。一般的条件分支有两种情况,第一种就是所有的分支都属于正常行为;第二种则是条件表达式中提供的答案只有一个是正常的行为,其他的行为都不常见。如果多条分支都属于正常行为,我们就直接使用if...(else if...)else...的形式;如果某个条件极其罕见,那么我们就应该单独检查该条件,并且在该条件返回为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句(guard clauses)”。

      以卫语句取代嵌套条件表达式的精髓就是:给某一条分支予以特别的重视。如果用if...else...的结构,那么每一个条件分支的重要性都是同等的,这样的代码结构给别人的感觉就是各个分支的重要性是一样的。然而卫语句就不同了,他会告诉读者:“这种情况很罕见,如果它真的发生了,请做一些重要的处理,然后退出!”。谓语句要不从函数中直接返回,要么就抛出一个异常。

      这种重构手法主要适用的场景:在条件分支中,针对一些特殊的分支,为了强调其重要性,就可以使用卫语句来强调某一个分支的逻辑重要性。

    6.以多态取代条件表达式

      先看如下这段代码:

     1 public static void main(String[] args)
     2     {
     3         System.out.println(doCompute("3*2"));
     4     }
     5 
     6     public static double doCompute(String intStr)
     7     {
     8         if (intStr.contains("+"))
     9         {
    10             String[] split = intStr.split("\+");
    11             return Integer.parseInt(split[0]) + Integer.parseInt(split[1]);
    12         } else if (intStr.contains("-"))
    13         {
    14             String[] split = intStr.split("-");
    15             return Integer.parseInt(split[0]) - Integer.parseInt(split[1]);
    16         } else if (intStr.contains("*"))
    17         {
    18             String[] split = intStr.split("\*");
    19             return Integer.parseInt(split[0]) * Integer.parseInt(split[1]);
    20         } else if (intStr.contains("/"))
    21         {
    22             String[] split = intStr.split("/");
    23             return Integer.parseInt(split[0]) / Integer.parseInt(split[1]);
    24         } else
    25         {
    26             return 0d;
    27         }
    28     }
    View Code

      查看到上述代码时,你的是否有种丈二的和尚摸不着头脑啊?上述的代码一看就知道不是什么好代码,但是这样的代码可扩展性确实不咋地,以后我要是要添加点其他算数运算的方法呢?怎么办,继续添加else if...分支?以前我相信很多小伙伴还是想过这个问题,面对着这种东西代码我该怎么优化是吧,只是没有找到更好的解决办法,那么今天我就给大家引入一种方式来看看,那么我们接着往下走:

    1 public abstract class AbstractCompute
    2 {
    3     String computStr = null;
    4 
    5     abstract double compute();
    6 }
    View Code
     1 public class AddComput extends AbstractCompute
     2 {
     3     public AddComput(String string)
     4     {
     5         this.computStr = string;
     6     }
     7 
     8     @Override
     9     double compute()
    10     {
    11         if (computStr != null)
    12         {
    13             String[] split = computStr.split("\+");
    14             return Integer.parseInt(split[0]) + Integer.parseInt(split[1]);
    15         }
    16         return 0;
    17     }
    18 }
    View Code
    1 public class RefactorWay
    2 {
    3     public static void main(String[] args)
    4     {
    5         AbstractCompute addComput = new AddComput("3+2");
    6         System.out.println(addComput.compute());
    7     }
    8 }
    View Code

      看到上面的代码后是不是有种柳暗花明的感觉呢?是的,这种重构手法就是将我们每一个操作作为一个对象进行封装,其内部来实现分支的逻辑处理。是不是很巧妙啊!这样的方式就是利用了java面向对象编程的三大特点之一的多态,通过多态来实现条件分支的处理,从而提升代码的逼格,瞬间变为高富帅,呵呵。

      这种重构手法主要适用的场景:当然是条件分支中,每个分支都具有同等的又不同的逻辑操作,那么我们就可以利用多来实现程序的扩展。当然这种重构手法不是用来取代条件分支的,而是当我们的条件分支在现在或者未来会达到一定数目的情况,可以采用这种手法来提升程序的可扩展性。

    7.引入null对象

      面向对象的程序中有一个语法叫做多态,在多态的实现中,我们不需要询问对象“你是什么类型的?”而后再根据得到的对象来调用其行为,你只管调用其方法就行了,其他的一切多态机制都会帮你安排妥当。对于这个思想不知道大家是否都有想到用到我们的代码中去,那么该如何运用呢?

      要运用实现上述方法,我们需要引入一个null对象,这里的null对象不是直接返回一个null,而是顶一个class继承自原来的类,然后提供一个方法isEmpty();父类返回false,子类(null对象)返回true,这样只要在有空判断的地方,我们都可以直接调用isEmpty()等方法解决我们程序中的为空校验,编码这样的ugly代码。

      定义一个这种类型后,其他地方也可以引用,只要是设计到为空判断的地方,子类都可以重写一个父类的这个方法,去避免条件表达式所引入的为空判断,提高代码的可读性。

    8.引入断言

      直接看代码吧:

    1     public double getExpenseLimit()
    2     {
    3         return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject;
    4     }
    View Code

      引入断言后代码可以这样写:

    1     public double getExpenseLimit()
    2     {
    3         Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);
    4         return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject;
    5     }
    View Code

      引入断言后,能够帮助程序员定位问题所在;便于代码调试;对代码没有代价。

      好了,以上都是条件分支相关的重构技巧,其中的很多技巧我相信很多小伙伴们都知道,但是就是没有总结过,当你有一些开发经验以后你会发现其实你已经会了很多,只是没有过系统的总结。学习的过程就是不断总结的过程,多总结,你就会领悟很多。软件思想也不外乎一个“理”字,只要领会其中的规律,你对编程有会有一个更高的认识。

      

    如果你觉得本博文对你有所帮助,请记得点击右下方的"推荐"哦,么么哒... 

    转载请注明出处:http://www.cnblogs.com/liushaofeng89/p/4993294.html

  • 相关阅读:
    Ubuntu:Failed to restart network.service: Unit network.service not found.
    解决 yarn或pnpm : 无法加载文件 C:\Users\hp\AppData\Roaming\npm\cnpm.ps1,因为在此系统上禁止运行脚本
    MSSQL表名、列名转大写SQL语句
    js 控制打开网页窗口的大小位置 sk
    js实现公历(阳历)和农历(阴历)的换算 sk
    一个怂女婿的成长笔记【二十四】
    牛顿迭代法(大白话)
    Source Generator实战
    i=i++
    记录下三种排序,冒泡,选择,和快速。
  • 原文地址:https://www.cnblogs.com/liushaofeng89/p/4993294.html
Copyright © 2020-2023  润新知