1

那有什么天生如此,只是我们天天坚持。

本篇文章主要讲解 《重构---改善既有代码的设计》 这本书中的 第九章简化条件表达式中 的知识点,

Decompose Conditional(分解条件表达式)

问题:你有一个复杂的条件(if、then、else) 语句

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

 //重构前
 if (date.before(SUMMER_START) || date.after(SUMMER_END))
        charge = quantity * _winterRate + _winterServiceCharge;
    else charge = quantity * _summerRate;
 //重构后
if (notSummer(date))
        charge = winterCharge(quantity);
    else charge = summerCharge(quantity);

动机

将条件分支的代码分解成多个独立函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新建函数,可以更清楚地表达自己的意图。

做法

  • 将if段落提炼出来,构成一个独立函数
  • 将then段落和else段落都提炼出来,各自构成一个独立函数

Consolidate Conditional Expression(合并条件表达式)

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

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

    //重构前
   double disabilityAmount(){
        if(_seniority < 2) return 0;
        if(_monthsDisabled > 12) return 0;
        if(_isPartTime) return 0;
    }
//重构后
 double disabilityAmount(){
        if(isNotEligableForDisability()) return 0;
    }

动机

如果一串条件检查:检查条件各不相同,最终行为却一致,就应该将它们合并为一个条件表达式,之所以要合并条件代码,有两个重要原因,
首先,合并后的条件代码用意更清晰,其次,这项重构往往可以为使用Extract Method(提炼函数)做好准备

做法

  • 确定这些条件语句都没有副作用
  • 使用适当的逻辑操作符,将一系列相关条件表达式合并为一个
  • 编译,测试
  • 对合并后的条件表达式实施Extract Method

Consolidate Duplicate Conditional Fragments(合并重复的条件片段)

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

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

//重构前
 if(isSpecialDeal()){
        total = price * 0.95;
        send();
    }
    else{
        total = price * 0.98;
        send();
    }
//重构后
 if(isSpecialDeal())
        total = price * 0.95;
    else
        total = price * 0.98;
    send();

动机

有助于清楚地表明哪些东西随条件的变化而变化、哪些东西保持不变

做法

  • 鉴别出”执行方式不随条件变化而变化”的代码‘
  • 如果这些共通代码位于条件表达式起始处,就将它移到条件表达式之前
  • 如果这些共通代码位于条件表达式尾端,就将它移到条件表达式之后
  • 如果这些共通代码位于条件表达式中段,就需要观察共通代码之前或之后的代码是否改变了什么东西,如果的确有所改变,应该首先将共通代码向前或向后移动,移至条件表达式的起始处或尾端,再以前面所受的办法来处理
  • 如果共通代码不止一条语句,应该先使用Extract Method(提炼函数)将共通代码提炼到一个独立函数中,再以前面所说的办法来处理

范例

//重构前
 if (isSpecialDeal()) {
         total = price * 0.95;
         send();
     }
     else {
         total = price * 0.98;
         send();
     }

由于条件式的两个分支都执行了 send() 函数,所以我应该将send() 移到条件式的外围:

     if (isSpecialDeal())
         total = price * 0.95;
     else
         total = price * 0.98;
     send();

我们也可以使用同样的手法来对待异常(exceptions)。如果在try 区段内「可能引发异常」的语句之后,以及所有catch 区段之内,都重复执行了同一段代码,我就 可以将这段重复代码移到final 区段。

Remove Control Flag(移除控制标记)

问题:在一系列布尔表达式中,某个变量带有”控制标记”的作用

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

动机

在一系列条件表达式中,你常常会用到[用以何时停止条件检查]的控制标记。

set done to false
while not done
if (condition)
    //do something
    //set done to true
next step of loop

用break语句和continue语句跳出复杂的条件语句

做法

  • 找出让你跳出这段逻辑的控制标记值
  • 找出对标记变量赋值的语句,代以恰当的break语句或continue语句
  • 每次替换后,编译并测试

范例 :以break取代简单的控制标记

//重构前
void checkSecurity(String[] people){
        boolean found = false;
        for(int i = 0; i < people.length; i++){
            if(!found){
                if(people[i].equals("Don")){
                    sendAlert();
                    found = true;
                }
                if(people[i].equals("John")){
                    sendAlert();
                    found = true;
                }
            }
        }
    }

=>


//重构后
    void checkSecurity(String[] people){
        for(int i = 0; i < people.length; i++){
            if(people[i].equals("Don")){
                sendAlert();
                break;
            }
            if(people[i].equals("John")){
                    sendAlert();
                    break;
            }
        }
    }

范例 :以return返回控制标记

//重构前
  void checkSecurity(String[] people){
        String found = "";
        for(int i = 0; i < people.length; i++){
            if(found.equals("")){
                if(people[i].equals("Don")){
                    sendAlert();
                    found = "Don";
                }
                if(people[i].equals("John")){
                    sendAlert();
                    found = "John";
                }
            }
        }
        someLaterCode(found);
    }
//重构后
    void checkSecurity(String[] people){
        String found = foundMiscreant(people);
        someLaterCode(found);
    }

    String foundMiscreant(String[] people){
        String found = "";
        for(int i = 0; i < people.length; i++){
            if(found.equals("")){
                if(people[i].equals("Don")){
                    sendAlert();
                    return "Don";
                }
                if(people[i].equals("John")){
                    sendAlert();
                    return "John";
                }
            }
        }
        return "";
    }

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

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

解决:使用卫语句表现所有特殊情况(启哥备注:可以减少嵌套)

//重构前
  double getPayAmount(){
        double result;
        if(_isDead) result = deadAmount;
        else{
            if(_isSeparated) result = separatedAmount();
            else{
                if(_isRetired) result = retiredAmount();
                else result = normalPayAmount();
            }
        }
        return result;
    }
//重构后
 double getPayAmount(){
        if(_isDead) return deadAmount();
        if(_isSeparated) return separatedAmount();
        if(_isRetired) return retiredAmount;
        return normalPayAmount();
    }

动机

条件表达式通常有两种表现形式:

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

如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回,这样的单独检查常常被称为”卫语句”

做法

  • 对于每个检查,放进一个卫语句,卫语句要么从函数中返回,要么就抛出一个异常
  • 每次将条件检查替换成卫语句后,编译并测试

范例:将条件反转

//重构前
 public double getAdjustedCapital(){
        double result = 0.0;
        if(_capital > 0.0){
            if(_intRate > 0.0 && _duration > 0.0){
                result = (_income / _duration) * ADJ_FACTOR;
            }
        }
        return result;
    }
//重构后
public double getAdjustedCapital(){
        double result = 0.0;
        if(_capital <= 0.0) return 0.0;
        if(_intRate <= 0.0 || _duration <= 0.0) return 0.0;
        return (_income / _duration) * ADJ_FACTOR;
    }

Replace Conditional with Polymorphism(以多态取代条件表达式)

问题:你手上有个条件表达式,他根据对象类型的不同而选择不同的行为

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

//重构前
    double getSpeed(){
        switch(_type){
            case EUROPEAN:
                return getBaseSpeed();
            case AFRICAN:
                return getBaseSpeed() - getLoadFactory() * _numberOfCoconuts;
            case NORWEGIAN_BLUE:
                return (_isNailed) ? 0 : getBaseSpeed(_voltage);
        }
        throw new RuntimeException("Should be unreachable");
    }

==>

//重构后
clipboard.png

动机

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

做法

  • 如果要处理的条件表达式是一个更大函数中的一部分,首先对条件表达式进行分析,然后使用Extract Method将它提炼到一个独立函数中
  • 如果有必要,使用Move Method(搬移函数)将条件表达式放置到继承结构的顶端
  • 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数,将与该子类相关的条件表达式分支复制到新建函数中,并对它进行适当调整
  • 编译,测试
  • 在超类中删除条件表达式内被复制了的分支
  • 编译,测试
  • 针对条件表达式的每个分支,重复上述过程,直到所有分支都被移动子类内函数为止
  • 将超类之中容乃条件表达式的函数声明为抽象函数

范例

请允许我继续使用「员工与薪资」这个简单而又乏味的例子。

继承构:
clipboard.png


//重构前
    class Employee...
        int payAmount(int type){
            switch(type){
                case Employee.ENGINEER:
                    return _monthlySalary;
                case Employee.SALESMAN:
                    return _monthlySalary + _commission;
                case Employee.MANAGER:
                    return _monthlySalary + _bonus;
                default:
                    throw new IllegalArgumentException("Incorrect type code value");
            }
        }
        private Employee _type;
        int getType(){
            return _type.getTypeCode()
        }

    abstract class EmployeeType...
        abstract int getTypeCode();

    class Engineer extends EmployeeType...
        int getTypeCode(){
            return Employee.ENGINEER:
        }

    ...and other subclasses

switch 语句已经被很好地提炼出来,因此我不必费劲再做一遍。不过我需要将它移至EmployeeType class,因为EmployeeType 才是被subclassing 的class 。

class EmployeeType...
   int payAmount(Employee emp) {
       switch (getTypeCode()) {
           case ENGINEER:
              return emp.getMonthlySalary();
           case SALESMAN:
              return emp.getMonthlySalary() + emp.getCommission();
           case MANAGER:
              return emp.getMonthlySalary() + emp.getBonus();
           default:
              throw new RuntimeException("Incorrect Employee");
       }
   }

由于我需要EmployeeType class 的数据,所以我需要将Employee 对象作为参数传递给payAmount()。这些数据中的一部分也许可以移到EmployeeType class 来,但那是另一项重构需要关心的问题了。

调整代码,使之通过编译,然后我修改Employee 中的payAmount() 函数,令它委托(delegate,转调用)EmployeeType :

class Employee...
   int payAmount() {
       return _type.payAmount(this);
   }

现在,我可以处理switch 语句了。这个过程有点像淘气小男孩折磨一只昆虫——每次掰掉它一条腿(意思就是「去掉一个分支」)。首先我把switch 语句中的"Engineer"这一分支拷贝到Engineer class:

class Engineer...
   int payAmount(Employee emp) {
       return emp.getMonthlySalary();
   }

这个新函数覆写了superclass 中的switch 语句之内那个专门处理"Engineer"的分支。我是个徧执狂,有时我会故意在case 子句中放一个陷阱,检查Engineer class 是否正常工作(是否被调用):

class EmployeeType...
   int payAmount(Employee emp) {
       switch (getTypeCode()) {
           case ENGINEER:
              throw new RuntimeException ("Should be being overridden");
           case SALESMAN:
              return emp.getMonthlySalary() + emp.getCommission();
           case MANAGER:
              return emp.getMonthlySalary() + emp.getBonus();
           default:
              throw new RuntimeException("Incorrect Employee");
       }
   }

接下来,我重复上述过程,直到所有分支都被去除为止:

class Salesman...
   int payAmount(Employee emp) {
       return emp.getMonthlySalary() + emp.getCommission();
   }
class Manager...
   int payAmount(Employee emp) {
       return emp.getMonthlySalary() + emp.getBonus();
   }

然后,将superclass 的payAmount() 函数声明为抽象函数:

class EmployeeType...
   abstract int payAmount(Employee emp);
   
   
   

a

Introduce Null Object(引入Null 对象)

问题:你需要再三检查某物是否为null value

解决:将null值替换为null对象

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

//重构后
clipboard.png

启哥说: 和提供一个默认的不做任何处理的空实现是一个意思

动机(Motivation)

当实例变量的某个字段内容允许为null时,在进行操作时往往要进行非空判断,这个工作是非常繁杂的,
所以不让实例变量被设为null,而是插入各式各样的空对象——它们都知道如何正确地显示自己,这样就可以摆脱大量过程化的代码

空对象一定是常量,它们的任何成分都不会发生变化,因此可以使用Singleton模式来实现它们

做法

  • 为源类建立一个子类,使其行为就像是源类的null版本,在源类和null子类中都加上isNull()函数,前者的应该返回false,后者的应该返回true,或者建立一个nullable接口,将isNull()函数放入其中,让源类实现这个接口
  • 编译
  • 找出所有”索求源对象却获得一个null”的地方,修改这些地方,使它们改而获得一个空对象
  • 找出所有”将源对象与null做比较”的地方,修改这些地方,使它们调用isNull()函数
  • 编译,测试
  • 找出这样的程序点:如果对象不是null,做A动作,否则做B动作
  • 对于每一个上述地点,在null类中覆写A动作,使其行为和B动作相同
  • 使用上述被覆写的动作,然后删除”对象是否等于null”的条件测试,编译并测试

范例

—家公用事业公司的系统以Site 表示地点(场所)。庭院宅等和集合公寓(apartment)都使用该公司的服务。任何时候每个地点都拥有(或说都对应于)一个顾客,顾客信息以Customer 表示:

//重构前
class Site...
        Customer getCustomer(){
            return _customer;
        }
        Customer _customer;

    class Customer...
        public String getName(){...}
        public BillingPlan getPlan(){...}
        public PaymentHistory getHistory(){...}

    public class PaymentHistory...
        int getWeesDelingquentInLastYear()

    Customer customer = site.getCustomer();
    BillingPlan plan;
    if(customer == null) plan = BillingPlan.basic();
    else plan = customer.getPlan();
    ...
//重构后
class NullCustomer extens Customer{
        public boolean isNull(){
            return true;
        }
    }

    class Customer...
        public boolean isNull(){
            return false;
        }
        static Customer new Null(){
            return new NullCustomer();
        }

    class Site...
        Customer getCustomer(){
            return (_customer == null) ? Customer.newNull() : _customer;
        }

    Customer customer = site.getCustomer();
    BillingPlan plan;
    if(customer.isNull()) plan = BillingPlan.basic();
    else plan = customer.getPlan();

Introduce Assertion(引入断言)

问题:如果某一段代码需要对程序状态做出某种假设

解决:以断言明确表现这种假设

//重构前
  double getExpenseLimit(){
        return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit:_primaryProject.getMemberExpenseLimit();
    }
//重构后
double getExpenseLimit(){
        Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null);
        return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit:_primaryProject.getMemberExpenseLimit();
    }

动机

常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行,这时应该使用断言,把不符合条件的假设标明出来

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

在一段逻辑中加入断言是有好处的,因为它迫使你重新考虑这段代码的约束条件,如果不满足这些约束条件,程序也可以正常运行,断言就不会带给你任何帮助,只会把代码变得混乱,并且有可能妨碍以后的修改

做法

  • 如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况

范例

下面是一个简单例子:开支(经费)限制。后勤部门的员工每个月有固定的开支限额;业务部门的员工则按照项目的开支限额来控制自己的开支。一个员工可能没有开支额度可用,也可能没有参与项目,但两者总得要有一个(否则就没有经费可用 了)。在开支限额相关程序中,上述假设总是成立的,因此:

//重构前
class Employee...
   private static final double NULL_EXPENSE = -1.0;
   private double _expenseLimit = NULL_EXPENSE;
   private Project _primaryProject;
   double getExpenseLimit() {
      return (_expenseLimit != NULL_EXPENSE) ?
          _expenseLimit:
          _primaryProject.getMemberExpenseLimit();
  }
   boolean withinLimit (double expenseAmount) {
      return (expenseAmount <= getExpenseLimit());
  }

这段代码包含了一个明显假设:任何员工要不就参与某个项目,要不就有个人开支限额。我们可以使用assertion 在代码中更明确地指出这一点:

  double getExpenseLimit() {
      Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null);
      return (_expenseLimit != NULL_EXPENSE) ?
          _expenseLimit:
          _primaryProject.getMemberExpenseLimit();
  }

这条assertion 不会改变程序的任何行为。另一方面,如果assertion中的条件不为真,我就会收到一个运行期异常:也许是在withinLimit() 函数中抛出一个空指针(null pointer)异常,也许是在Assert.isTrue() 函数中抛出一个运行期异常。有时assertion 可以帮助程序员找到臭虫,因为它离出错地点很近。但是,更多时候,assertion 的价值在于:帮助程序员理解代码正确运行的必要条件。
我常对assertion 中的条件式使用Extract Method ,也许是为了将若干地方的重复码提炼到同一个函数中,也许只是为了更清楚说明条件式的用途。

//重构后
 double getExpenseLimit() {
      Assert.isTrue (Assert.ON &&
          (_expenseLimit != NULL_EXPENSE || _primaryProject != null));
      return (_expenseLimit != NULL_EXPENSE) ?
          _expenseLimit:
          _primaryProject.getMemberExpenseLimit();
  }

或者是这种手法

//重构后
 double getExpenseLimit() {
      Assert.isTrue (Assert.ON &&
          (_expenseLimit != NULL_EXPENSE || _primaryProject != null));
      return (_expenseLimit != NULL_EXPENSE) ?
          _expenseLimit:
          _primaryProject.getMemberExpenseLimit();
  }

如果Assert.ON 是个常量,编译器(译注:而非运行期间)就会对它进行检查; 如果它等于false ,就不再执行条件式后半段代码。但是,加上这条语句实在有点丑陋,所以很多程序员宁可仅仅使用Assert.isTrue() 函数,然后在项目结束前以过滤程序滤掉使用assertions 的每一行代码(可以使用Perl 之类的语言来编写这样 的过滤程序)。
Assert class应该有多个函数,函数名称应该帮助程序员理解其功用。除了isTrue() 之外,你还可以为它加上equals() 和shouldNeverReachHere() 等函数。


石志远
572 声望62 粉丝