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

1、分解条件表达式(Decompose Condition)

(1)症状:你有一个复杂的条件(if - then - else )语句

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

(3)将任何大块头代码分解成多个独立函数,根据每个小块代码的用途为分解而得的新函数命名

(4)将每个分支条件分解成新函数可以突出条件逻辑,更清楚地表明每个分支的作用,突出每个分支的原因

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);

private boolean notSummer(Date date) {
     return date.before (SUMMER_START) || date.after(SUMMER_END);
}

private double summerCharge(int quantity) {
     return quantity * _summerRate;
}

private double winterCharge(int quantity) {
     return quantity * _winterRate + _winterServiceCharge;
}

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

(1)症状:有一系列条件测试,都得到相同的结果

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

(3)一连串条件检查,检查条件各不相同,但最终行为却一致

(4)合并后的条件代码会更清晰的表明只有一次条件检查,只不过有多个并列条件需要检查而已

(5)如果有些检查彼此独立,的确不应该被视为同一次检查,就不要使用本项重构

if (seniorty < 2) {
	return 0;
}

if (monthDisabled > 12) {
	return 0;
}

if (isPartTime) {
	return 0;
}
----------------------------------------------------------------------------------
重构(合并条件表达式)后的代码
if (isNotEligableForDisability()) {
	return 0;
}

private Boolean isNotEligableForDisability() {
	return seniorty < 2 || monthDisabled > 12 || isPartTime;
}

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

(1)症状:在条件表达式的每个分支上有着相同的一段代码

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

(3)合并重复代码有利于清楚地表明哪些东西随条件的变化而变化、哪些东西保持不变

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();

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

 

4、移除控制标记(Remove Control Flag)

(1)症状:在一系列布尔表达式中,某个变量带有“控制标记(control flag)”的作用

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

(3)在未能提供break和Continue语句的编程语言中,可以使用Extract Method方法,将整段逻辑提炼到一个独立函数中

以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;
             }
          }
      }
  }
这种情况下很容易找出控制标记:当变量found 被赋予true 时,搜索就结束,可以逐一引入break 语句:
  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;
          }
      }
  }

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

(1)症状:函数中的条件逻辑使人难以看清正常的执行路径

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

(3)卫语句:如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回

(4)本项重构的精髓:给某条分支以特别的重视,告诉读者如果真的发生了,请做一些必要的整理工作,然后退出

(5)卫语句要不就从函数中返回,要不就抛出一个异常

范例1:想像一个薪资系统,其中以特殊规则处理死亡员工、驻外员工、退休员工的薪资。这些情况不常有,但的确偶而会出现。
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();
};

范例2:将条件逆反(Reversing the Conditions)
 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() {
      if (_capital <= 0.0) return 0.0;
      if (_intRate <= 0.0 || _duration <= 0.0) return 0.0;
    return (_income / _duration) * ADJ_FACTOR;
  }

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

(1)症状:你手上有个条件表达式,它根据对象类型的不同而选择不同的行为

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

(3)多态使你不必编写明显的条件表达式,可以根据对象的不同类型而采取不同的行为

(4)多态在一定程度上消除“类型码的switch语句”以及“基于类型名称的if-then-else语句”

  • 如果要处理的条件式是一个更大函数中的一部分,首先对条件式进行分析,然后使用Extract Method 将它提炼到一个独立函数去。
  • 如果有必要,使用Move Method 将条件式放置到继承结构的顶端。
  • 任选一个subclass ,在其中建立一个函数,使之覆写superclass 中容纳条件式的那个函数。将「与subclass 相关的条件式分支」拷贝到新建函数中,并对它进行适当调整。
  • 为了顺利进行这一步骤,你可能需要将superclass 中的某些private 值域声明为protected 。
  • 编译,测试。
  • 在superclass 中删掉条件式内被拷贝出去的分支。
  • 编译,测试。
  • 针对条件式的每个分支,重复上述过程,直到所有分支都被移到subclass 内的函数为止。
  • 将superclass 之中容纳条件式的函数声明为抽象函数(abstract method)。
class Engineer extends EmployeeType{
    int getTypeCode(){
        return Employee.ENGINEER;
    }
}
class Manager extends EmployeeType{
    int getTypeCode(){
        return Employee.MANAGER;
    }
}
class Salesman extends EmployeeType{
    int getTypeCode(){
        return Employee.SALEMAN;
    }
}

class Employee...
private employeeType _type;
int payAmount(){
    switch(getType()){
    case EmployeeType.ENGINEER:
        return _monthlySalary;
    case EmployeeType.SALESMAN:
        return _monthlySalary + _commission;
    case EmployeeType.MANAGER:
        return _monthlySalary + bonus;
    default;
        throw new RuntimeException();
    }
}
int getType(){
    return _type.getTypeCode();
}
void setType(int arg){
    _type = EmployeeType.newType(arg);
}

class employeeType...
static EmployeeType newType(int code){
    switch(code){
        case ENGINEER:
            return new Engineer();
        case SALESMAN:
            return new Salesman();
        case MANAGER:
            return new Manager();
        default:
            throw new IllegalArgumentException();
    }
}
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
-----------------------------------------------------------------------------------------------
重构(以多态取代条件表达式)后的代码
class Employee...
private Engineer _type;
int payAmount(){
    return _type.payAmount(this);
}
 
class Engineer...
int payAmount(Employee emp){
    return emp.getMonthlySalary();
}

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

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

7、引入Null对象(Introduce Null Object)

(1)症状:你需要再三检查某对象是否为null

(2)解决:将null值替换成null对象

(3)空对象一定是常量,它们的任何成分都不会发生变化

  • 为源类建立一个子类,使其行为就像是源类的null版本。在源类和null子类中都加上IsNull()函数,前者的IsNull()应该返回false,后者的IsNull()返回true。
  • 编译。
  • 找出所有“索求源对象却获得一个null”的地方。修改这些地方,使它们改而获得一个空对象。
  • 找出“将源对象与null做比较”的地方。修改这些地方,使它们调用IsNull()函数。
  • 编译、测试。
  • 找出这样的程序点:如果对象不是null,做A动作,否则做B动作。
  • 对于每一个上述地点,在null类中覆写A动作,使其行为和B动作相同。
  • 使用上述被覆写的动作,然后删除“对象是否等于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();

8、引入断言(Introduce Assertion)

(1)症状:某一段代码需要对程序状态作出某种假设

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

(3)通常这样的假设并没有在代码中明确的表现出来,有时程序员会以注释写出这样的假设

(4)断言是一种更好的解决方案,断言是一个条件表达式,如果它失败了就表示程序员犯了错误

(5)不要滥用断言。不要使用它来检查“你认为应该为真”的条件,只使用它来检查“一定必须为真”的条件,滥用断言可能会造成难以维护的重复逻辑

(6)如果断言所指示的约束条件不能满足,代码是否仍能正常运行?如果可以,就把断言拿掉

(7)注意断言中的重复代码,你可以大胆使用Extract Method(提炼函数)去掉那些重复代码

(8)在项目结束前以过滤程序滤掉使用断言的每一行代码(可以使用Perl之类的语言来编写这样的过滤程序)

double getExpenseLimit() {  
    // should have either expense limit or a primary project  
    return (_expenseLimit != NULL_EXPENSE) ?  
        _expenseLimit:  
        _primaryProject.getMemberExpenseLimit();  
}   
----------------------------------------------------------------------------------
 重构(引入断言)之后的代码
double getExpenseLimit() {  
    Assert.isTrue (_expenseLimit != NULL_EXPENSE ||   
    _primaryProject != null);  
    return (_expenseLimit != NULL_EXPENSE) ?  
        _expenseLimit:  
        _primaryProject.getMemberExpenseLimit();  
} 

 

尾注

  • 上述的总结与思考是基于对《重构—改善既有代码的设计》这本书的精读与演绎
  • 更多及时干货,请关注微信公众号:JAVA万维猿圈

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值