当前位置: 首页 > 工具软件 > Martin > 使用案例 >

重构(Martin Fowler)——简化条件表达式

刁茂才
2023-12-01

条件逻辑有可能十分复杂,因此本章提供一些重构手法,专门用来简化它们。其中一项核心重构就是Decompose Conditional

将一个复杂的条件逻辑分成若干小块。

本章的其余重构手法可用以处理另一些重要问题:

如果你发现代码中的多处测试有相同的结果,应该实施Consolidate Conditional Expression

如果条件代码中有任何重复,可以运用Consolidate Duplicate Conditional Fragments

面对对象程序的条件表达式通常比较少,因为很多条件行为都被多态机制处理掉了,多态之所以更好,是因为调用者无需了解条件行为的细节,因此条件的扩展更容易。

所以面向对象程序中很少出现switch语句,一旦出现,就考虑运用Replace Conditional with Polymorphism将它替换为多态

 

多态还有一种十分有用但鲜为人知的用途,通过Introduce Null Object去除对于null值得检验

目录

1.1Decompose Conditional(分解条件表达式)

 动机

做法

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

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

1.4Remove Control Flag(移除控制标记)

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

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

1.7Introduce Null Object(引入NUll对象)

1.8Introduce Assertion(引入断言)


1.1Decompose 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(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);
    }

具体函数

bool notSummer(Date date){
    return date.before(SUMMER_START)||date.after(SUMMER_END);
}
double summerCharge(int quantity){
    return quantity * _summerRate;
}
double winterCharge(int quantity){
    return quantity * _winterRate + _winterServiceCharge;
}

尽管这部分if和else很短小,但是依旧可以看出notSummer比条件表达式更加的清晰,一眼就知道函数要干嘛

 

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

你有一系列条件测试,都得到相同的结果

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

double disabilityAmount(){
    if(_seniority < 2) return 0;
    if(_monthDisabled > 12) return 0;
    if(_isPaerTime) return 0;
}

将其合并

double disabilityAmount(){
    if(_seniority < 2||_monthDisabled > 12||_isPaerTime) return 0;
}

现在将其进行Extract Method操作

double disabilityAmount(){
    if(isNotEligibleForDisability()) return 0;
}
bool isNotEligibleForDisability(){
    return (_seniority < 2||_monthDisabled > 12||_isPaerTime);
}

在某些情况下,可以考虑三目运算符

    if(onVaction()&&lengthOfService() > 10) return 1;
    else return 0.5;

 重构为:

    return ( onVaction()&&lengthOfService() > 10 ) ? 1:0.5;

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

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

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

    if(isSpecialDal()){
        total = price * 0.95;
        send();
    }
    else {
        total = price * 0.98;
        send();
    }

重构为:

    if(isSpecialDal()){
        total = price * 0.95;
    }
    else {
        total = price * 0.98;
    }
    send();

原则上依旧是为了保持代码的可读性

1.4Remove Control Flag(移除控制标记)

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

以break或者return语句取代控制标记

动机

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

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

这样标志物带来的麻烦会超过它的便利性。

人们之所以会使用这样的控制标记,因为结构化编程原则:每个子程序只能有一个入口和出口

做法

以break取代简单的控制标记

void checkSecurirty(string people){
    bool found = false;
    for(int i = 0;i<people.length();++i){
        if(!found){
            if(people[i] == 'D'){
                sendAlert();
                found = true;
            }
            if(people[i] == 'J'){
                sendAlert();
                found = true;
            }
        }
    }
}

这种情况还是很简单的,非常容易发现可以使用break跳出循环。

现将改变标志位的地方编程break,然后将标志物判断语句直接删除

如下:

void checkSecurirty(string people){
    for(int i = 0;i<people.length();++i){
        if(people[i] == 'D'){
            sendAlert();
            break;
        }
        if(people[i] == 'J'){
            sendAlert();
            break;
        }
    }
}

以return取代简单的控制标记

void checkSecurirty(string people){
    string found = "";
    for(int i = 0;i<people.length();++i){
        if(found == ""){
            if(people[i] == 'D'){
                sendAlert();
                found == "D";
            }
            if(people[i] == 'J'){
                sendAlert();
                found == "J";
            }
        }
    }
    someLateCode(found);
}

现在我们可以看到,found有两个用途,一个是标志物,一个是记录结果

遇到这种情况,直接用return代替即可,同时可以使用Extract Method对起进行进一步的重构

void checkSecurirty(string people){
    string found = foundMiscreant(people);
    someLateCode(found);
}
string foundMiscreant(string people){
    string found = "";
    for(int i = 0;i<people.length();++i){
        if(found == ""){
            if(people[i] == 'D'){
                sendAlert();
                return "D";
            }
            if(people[i] == 'J'){
                sendAlert();
                return "J";
            }
        }
    }
    return "";
}

 

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

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

使用卫语句表达式所有特殊情况

下面语句均是处理员工死亡,驻外,退休的薪资办理,均不常见,但是会出现。

double getPayAmount(){
    double result;
    if(_isDead) {
        result = separatedAmount();
    }
    else {
        if(_isRetired){
            result = retiredAmount();
        }
        else {
            result = normalPayAmount();
        }
    }
    return result;
}

重构为:

double getPayAmount(){
    if(_isDead) {
        return separatedAmount();
    }
    if(_isSeparated){
        return separatedAmount();
    }
    if(_isRetired){
        return normalPayAmount();
    }
    return normalPayAmount();
}

动机

条件表达式有两种表现形式。

第一种形式:所有分支都属于正常行为

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

如果是第一种形式,两条分支都是正常行为,就应该使用形如if......else......的条件表达式。

如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立即从函数中返回,这种语句就是卫语句(guard clauses)

if - then - else每一条分支的内容都是平等的,都是同等重要的。

guard clauses:这种情况很罕见,出现了一定要做相关的处理

 

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

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

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

double getSpeed(){
    switch(_type){
    case EUROPEAN:
        return getBaseSpeed();break;
    case AFRICAN:
        return getBaseSpeed() - getLoadFactor() * _numberOfCoconuts;break;
    case NORWEGIAN_BLUE:
        return (_isNailed) ? 0 :getBaseSpeed(_voltage);break;
    default:
        return 0.0;
    }
}

 

动机

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

 

1.7Introduce Null Object(引入NUll对象)

你需要再三检查某个对象是否为nullptr

将所有Null替换为nullptr对象

动机

多态的最根本好处在于:你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为

现在有四个类,分别如下:

//一个地产公司使用site表示房屋情况,任何时候每个地点都拥有一个顾客
class Site{
public:
    Customer getCustomer(){
        return _customer;
    }
private:
    Customer _customer;
};

客户类:

class Customer{
    string getName(){
        
    }
    BillingPlan getPlan(){
        
    }
    PaymentHistory getHistory(){
        
    }
};

其他:

class BillingPlan{};
//表示客户的付款记录
class PaymentHistory{
public:
    int getWeeksDelinquentInLastYear(){}
};

当房屋没有客人居住的时候,Customer对象就等于null了,下面我们查看具体操作

    //获取客户信息
    Site * site = new Site();
    Customer * customer = site->getCustomer();
    if(customer == nullptr) {
        plan = BillingPlan.basic();
    }
    else{
        plan = customer->getPlan();
    }
    //获取客户姓名
    string customerName = "";
    if(customer == nullptr) {
        customerName = "occupant";
    }
    else{
        customerName = customer->getName();
    }
    //获取客户住宿情况
    int weeksDelinquent;
    if(customer == nullptr) {
        weeksDelinquent = 0;
    }
    else{
        weeksDelinquent = customer->getHistory()->getWeeksDelinquentInLastYear();
    }

系统中有许多地方都使用Site和Customer对象,它们都必须坚持Customer对象是否等于Null,而这样的检查完全是重复的。下面建立空对象。

class NullCustomer : public Customer{
public:
    bool isNull(){
        return true;
    }
private:
};

对class Customer里面的东西进行修改:

Customer * Customer::newNull(){
    return new NullCustomer();
}

之后检查所有能够提供customer对象的地方,将它们都加以修改,是它们不能返回nullptr,改而返回一个NullCustomer对象

//直接在getCustomer函数里面进行处理
Customer * Site::getCustomer(){
    return (_customer == nullptr) ? Customer::newNull():_customer;
}

之后将原始操作进行替换:

    //获取客户信息
    Site * site = new Site();
    Customer * customer = site->getCustomer();//①
    if( customer->isNull() ) {//②
        plan = BillingPlan.basic();
    }
    else{
        plan = customer->getPlan();
    }
    //获取客户姓名
    string customerName = "";
    if( customer->isNull() ) {
        customerName = "occupant";
    }
    else{
        customerName = customer->getName();
    }
    //获取客户住宿情况
    int weeksDelinquent;
    if( customer->isNull() ) {
        weeksDelinquent = 0;
    }
    else{
        weeksDelinquent = customer->getHistory()->getWeeksDelinquentInLastYear();
    }

①getCustomer函数的返回值为:return (_customer == nullptr) ? Customer::newNull():_customer;

所以如果是Null,那么显然返回空对象,后续条件判断语句全为true,反正为false

②将条件判断全部改成isNull的调用

 

1.8Introduce Assertion(引入断言)

某一段代码需要对程序状态做出某种假设

以断言明确表现这种假设

double getExpenseLimit(){
    return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit : _primaryProject.getMemberExpensenLimit();
}

重构为:

(以上代码为java语言中的使用方法)

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

 

动机

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

例如平方根计算只对正值才能进行,又例如某个对象可能假设其字段至少有一个不等于null。

这些代码通常并没有在代码在明确表现出来,你必须阅读整个算法才能看出。

有时程序员会以注释写出这样的假设。

现在或许有一种更好的技术:使用断言明确标明这些假设。

 

 

 

 类似资料: