重构-改善既有代码的设计:简化条件表达式(七)

简化条件表达式:
一、分解和合并条件
二、去重复代码
三、简化条件表达式:控制标记、卫语句、多态。

目录

          一、分解和合并条件

1.Decompose Conditional 分解条件表达式从if、then、else三个段落中分别提炼出独立函数

2.Consolidate Conditional Expression 合并条件表达式:一系列条件的结果相同,将这些条件合并为一个条件表达式,并将这个条件表达式提炼为一个独立函数。

二、去重复代码

3.Consolodate Duplicate Conditional Fragments 合并重复的条件片段将这段重复代码移到条件表达式之外。

三、简化条件表达式

4.Remove Control Flag 移除控制标记:“单一出口“原则会让你在代码中加入让人讨厌的控制标记,大大降低条件表达式的可读性。可以通过编程语言提供break和continue语句直接跳出复杂的条件语句。

5.Replace Nested Conditional with Guard Clauses 以卫语句取代嵌套条件表达式

6.Replace Conditional with Polymorphism 以多态取代条件表达式:多态的最根本的好处是:如果你需要根据对象的不同类型而采取不同的行为,多态使你不必编写某些的条件表达式。

7. Introduce Null Object 引入Null对象将null值替换为null对象

8. Introduce Assertion 引入断言:Assert.notNull(project,"project can not null");


1.分解条件表达式 Decompose Conditional


问题:你有一个复杂的条件语句。

解决:从if、then、else三个段落中分别提炼出独立函数

目的:是减少条件分支的代码行数,通过提炼函数来到达代码自解释。

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

当你的条件表达式比较复杂时,你就可以对其进行拆分。一般拆分的规则为:经if后的复杂条件表达式进行提取,将其封装成函数。如果if与else语句块中 的内容比较复杂,那么就将其提取,也封装成独立的函数,然后在相应的地方进行替换。

if (date.before(SUMMER_START) && date.after(WINTER_END))  {
  charge = quantity * winterRate + winterServiceCharge;
} else  {
   charge = quantity * summerRate;
}

if (notSummer(date)){
    charge = winterCharge(quantity);
} else {
    charge = summerCharge(quantity);
}

或者:

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

        程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。你必须编写代码来检查不同的条件分支、根据不同的分支做不同的事,然后,你很快就会得到一个相当长的函数。大型函数自身就会使代码的可读性下降,而条件逻辑则会使代码更难阅读。在带有复杂条件逻辑的函数中,代码(包括检查条件分支的代码和真正实现功能的代码)会告诉你发生的事,当常常让你弄不清为什么会发生这样的事,这就说明代码的可读性的确大大降低了。

       和任何大块头代码一样,你可以将它分解为多个独立函数,根据每个小块代码的用途,为分解的新函数命名,并将原函数中对应的代码改为调用新建函数,从而更清楚的表达自己的意图。对于条件逻辑,将每个分支条件分解为新函数还可以给你带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。

2.合并条件表达式 Consolidate Conditional Expression


问题:你有一系列条件测试,都得到相同结果。

解决:将这些测试合并为一个条件表达式,并将这个条件表达式提炼为一个独立函数。

目的:合并不必要的逻辑条件判断,从而提高代码的可读性。

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

//A和B部分就是合并

public String getStuLevel(int score){
    if (score == 100) {
            return "A";
    } else if (score >= 90) {
            return "A";

    } else if (score >= 80) {
            return "B";
    } else if (score >= 70) {
            return "B";

    } else if (score >= 60) {
           return "C";
    } else{
            
        return "D";
    }
}

有时你会发现这样一串条件检查:检查条件各不相同,最终行为却一致。如果发现这种情况,就应该使用“逻辑或”和“逻辑与”将它们合并为一个条件表达式。

之所以要合并条件表达式,有2个重要原因:

  •      首先,合并后的条件代码会告诉你“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这一次检查的用意更清晰。当然,合并前和合并后的代码有着相同的结果,但原先代码传达出的信息却是“这里有一些各自独立的条件测试,它们只是恰好同时发生”。
  •       其次,这项重构往往可以为你使用Extract Method(提炼方法)做好准备。将检查条件提炼成一个独立函数对于厘清代码意义非常有用,因为它把描述“做什么“的语句换成了“为什么这样做”。

       条件语句的合并理由也同时指出了不要合并的理由:如果你认为这些条件检查的确彼此独立,的确不应该被视为同一次检查,那么就不要使用本项重构。因为在这种情况下,你的代码已经清晰表达出自己的意义。

3. 合并重复的条件片段 Consolodate Duplicate Conditional Fragments


问题:在条件表达式的每个分支上有着相同的一段代码。

解决:将这段重复代码移到条件表达式之外。

目的:更清楚地表明哪些东西是随着条件变化而变化的,哪些东西是永远都不会变化的。

场景:一组条件表达式的所有分支都执行了一段相同的逻辑。如果是这样,你就应该将这段代码移动到条件表达式外面。

一组条件表达式的所有分支都执行了相同的某段代码。你应该将这段代码搬移到表达式外面。这样,代码才能更清楚地表明哪些东西随条件变化而变化、哪些东西保持不变。

4.移除控制标记变量 Remove Control Flag


问题:在一系列布尔表达式中,某个变量带有“控制标记’的作用。赞同“单一入口”原则。

但“单一出口”原则会在代码中加入讨厌的控制标记,大大降低条件表达式的可读性。

解决:以break或return语句取代控制标记。

目的:通过break,continue和return等方式结束循环,删除标志控制,提高程序的可读性。

场景:在循环体以内有一个表达式或者标志用来判断循环条件是否继续的场景,通过修改条件表达式或变量来控制循环体是否继续执行。

 

                                            

在一系列条件表达式中,常常会看到用以判断何时停止条件检查的控制标记。这样的标记带来的麻烦超过了它所带来的便利。人们之所以会使用这样的控制标记,因为结构化编程原则告诉他们:每个子程序只能有一个入口和出口。“单一出口“原则会让你在代码中加入让人讨厌的控制标记,大大降低条件表达式的可读性。这就是编程语言提供break和continue语句的原因:用它们跳出复杂的条件语句。去掉控制标记所产生的效果往往让你大吃一惊:条件语句真正的用途会清晰得多。

对于那些刚从C转到JAVA或者长期从事结构化编程的人员来说,就很重要了。这一点重构手法能够让你摆脱结构化编程的“单一原则(一个程序只能有一个入口和一个出口)”的束缚。嵌套条件代码,往往由那些深心“每个函数只能由一个出口”的程序员写出。此条规则太简单粗暴了。如果对函数剩余部分不再由兴趣,应该立即退出。引导阅读者去看一个没有用的else区段,只会妨碍他们的理解。

5.以卫语句取代嵌套条件表达式 Replace Nested Conditional with Guard Clauses


问题:函数中的条件逻辑使人难以看清正常的执行途径。

解决:使用卫语句表现所有特殊情况。

目的:强调某一个分支的逻辑重要性,保持代码清晰才是最关键的。

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

条件表达式通常有2种表现形式。

第一:所有分支都属于正常行为。

第二:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况。

       这2类条件表达式有不同的用途。如果2条分支都是正常行为,就应该使用形如if…..else…..的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。

       Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)的精髓是:给某个分支以特别的重视。它告诉阅读者:这种情况很罕见,如果它真的发生了,请做一些必要的整理工作,然后退出。

       “每个函数只能有一个入口和一个出口”的观念,根深蒂固于某些程序员的脑海里。现今的编程语言都会强制保证每个函数只有一个入口,至于“单一出口”规则,其实不是那么有用。保持代码清晰才是最关键的:如果单一出口能使这个函数更清晰易读,那么就使用单一出口;否则就不必这么做。

(卫语句就是把复杂的条件表达式拆分成多个条件表达式,比如一个很复杂的表达式,嵌套了好几层的if - then-else语句,转换为多个if语句,实现它的逻辑,这多条的if语句就是卫语句.:

//未使用卫语句
public void getHello(int type) {
    if (type == 1) {
        return;
    } else {
        if (type == 2) {
            return;
        } else {
            if (type == 3) {
                return;
            } else {
                setHello();
            }
        }
    }
} 

//使用卫语句
public void getHello(int type) {
    if (type == 1) {
        return;
    }
    if (type == 2) {
        return;
    }
    if (type == 3) {
        return;
    }
    setHello();
}

6.以多态取代条件表达式 Replace Conditional with Polymorphism


问题:你手上一个条件表达式,它根据对象类型的不同而选择不同的行为。当需要扩展新的类型时,我们不得不追加if else(或switch)语句块,以及相应的逻辑,这无疑降低了程序的可扩展性,也违反了面向对象的开闭原则。

解决:将这个条件表达式的每个分支放进一个子类的覆写函数中,然后将原始函数声明为抽象函数。

目的:这种重构手法不是用来取代条件分支的,而是当我们的条件分支在现在或者未来会达到一定数目的情况,可以采用这种手法来提高代码的可读性和可扩展性

场景:当然是条件分支中,每个分支都具有同等的又不同的逻辑操作,那么我们就可以利用多来实现程序的扩展。

public int calculate(int a, int b, String operator) {
    int result = Integer.MIN_VALUE;
 
    if ("add".equals(operator)) {
        result = a + b;
    } else if ("multiply".equals(operator)) {
        result = a * b;
    } else if ("divide".equals(operator)) {
        result = a / b;
    } else if ("subtract".equals(operator)) {
        result = a - b;
    }
    return result;
}

当出现大量类型检查和判断时,if else(或switch)语句的体积会比较臃肿,这无疑降低了代码的可读性。另外,if else(或switch)本身就是一个“变化点”,当需要扩展新的类型时,我们不得不追加if else(或switch)语句块,以及相应的逻辑,这无疑降低了程序的可扩展性,也违反了面向对象的开闭原则。

基于这种场景,我们可以考虑使用“多态”来代替冗长的条件判断,将if else(或switch)中的“变化点”封装到子类中。这样,就不需要使用if else(或switch)语句了,取而代之的是子类多态的实例,从而使得提高代码的可读性和可扩展性。很多设计模式使用都是这种套路,比如策略模式、状态模式。

public interface Operation { 
  int apply(int a, int b); 
}

public class Addition implements Operation { 
  @Override 
  public int apply(int a, int b) { 
    return a + b; 
  } 
}

public class OperatorFactory {
    private final static Map<String, Operation> operationMap = new HashMap<>();
    static {
        operationMap.put("add", new Addition());
        operationMap.put("divide", new Division());
        // more operators
    }
 
    public static Operation getOperation(String operator) {
        return operationMap.get(operator);
    }
}

public int calculate(int a, int b, String operator) {
    if (OperatorFactory .getOperation == null) {
       throw new IllegalArgumentException("Invalid Operator");
    }
    return OperatorFactory .getOperation(operator).apply(a, b);
}

      多态的最根本的好处是:如果你需要根据对象的不同类型而采取不同的行为,多态使你不必编写某些的条件表达式。

       正因为有了多态,所以你会发现:“类型吗的switch语句”以及 ”基于类型名称的if-then-else语句“在面向对象程序中很少出现。

       多态能够给你带来很多好处。如果同一组条件表达式在程序的许多地点出现,那么使用多态的收益是最大的。使用条件表达式时,如果你想添加一种新类型,就必须查找并更新所有条件表达式。但如果使用多态,只需建立一个新的子类,并在其中提供适当的函数就行了。类的用户不需要了解这个子类,这就大大降低了系统各部分之间的依赖,使系统升级更加容易。

7. Introduce Null Object 引入Null对象


你需要再三检查某对象是否为null,当使用一个方法返回的对象时,而这个对象可能为空,这个时候需要对这个对象进行操作前,需要进行判空,否则就会报空指针。当这种判断频繁的出现在各处代码之中,就会影响代码的美观程度和可读性,甚至增加Bug的几率。

问题:空引用的问题在Java中无法避免,需要使用条件表达式进行判空。

解决:通过代码编程技巧(引入空对象)来改善这一问题,将null值替换为null对象。

目的:通过子类都可以重写一个父类的这个方法,去避免条件表达式所引入的为空判断,提高代码的可读性。

if (customer == null) {
  plan = BillingPlan.basic();
} else {
  plan = customer.getPlan();
}


重构之后的代码:

class NullCustomer extends Customer {
  boolean isNull() {
           return true;
  }
  Plan getPlan() {
    return new NullPlan();
  }
  // Some other NULL functionality.
}


 
// Replace null values with Null-object.
customer = (order.customer != null) ?order.customer : new NullCustomer();
 
// Use Null-object as if it's normal subclass.
plan = customer.getPlan();
 

       多态的最根本好处在于:你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为-你只管调用该行为就是了,你只管调用其方法就行了,其他的一切多态机制会为你安排妥当。当某个字段内容是null时,多态可扮演另一个较不直观的用途。

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

//空对象的例子
public class OperatorFactory { 
  static Map<String, Operation> operationMap = new HashMap<>(); 
  static { 
    operationMap.put("add", new Addition()); 
    operationMap.put("divide", new Division()); 
    // more operators 
  } 
  public static Optional<Operation> getOperation(String operator) { 
    return Optional.ofNullable(operationMap.get(operator)); 
  } 
} 
public int calculate(int a, int b, String operator) { 
  Operation targetOperation = OperatorFactory.getOperation(operator) 
     .orElseThrow(() -> new IllegalArgumentException("Invalid Operator")); 
  return targetOperation.apply(a, b); 
}

//特殊对象的例子
public class InvalidOp implements Operation { 
  @Override 
  public int apply(int a, int b)  { 
    throw new IllegalArgumentException("Invalid Operator");
  } 
}

8. 引入断言 Introduce Assertion


某一段代码需要对程序状态做出某种假设。以断言明确表现这种假设。

常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行。

并且Spring框架本身也提供了断言的工具类,比如下面这段代码:

public void getProjectLimit(String project){
    if(project == null){
        throw new RuntimeException("project can not null");
    }
    doSomething();
}

加入Spring的断言后的代码

public void getProjectLimit(String project){
    Assert.notNull(project,"project can not null");
    doSomething();
}

       这样的假设通常并没有在代码中明确表现出来,你必须阅读整个算法才能看出。有时程序员会以注释写出这样的假设。可以使用断言明确标明这些假设。

       断言是一个条件表达式,应该总是为真。如果它失败,不是程序员犯了错误。因此断言的失败应该导致一个非受控异常。断言绝对不能被系统的其他部分使用。实际上,程序最后的成品往往将断言删除。因此,标记“某个东西是个断言”是很重要的。

       断言可以作为交流与调试的辅助。在交流的角度上,断言可以帮助程序阅读者了解代码所做的假设;在调试的角度上,断言可以在距离bug最近的地方抓住它们。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hguisu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值