简化条件表达式

0. 本章内容导图

本章提供的重构手法专门用来简化复杂的条件逻辑。


简化条件表达式

1. 重构手法

1.1 分解条件表达式

概要:
你有一个复杂的条件(if-then-else)语句。
从if、then、else三个段落中分别提炼出独立函数。
动机:
a. 降低代码复杂度,提高可读性
b. 突出条件逻辑,更清楚地表明每个分支的作用,突出每个分支的原因
示例:
重构前:

if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
    charge = quantity * mWinterRate + mWinterServiceCharge;
} else {
    charge = quantity * mSummerRate;
}

重构后:

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 winterCharge(int quantity) {
    return quantity * mWinterRate + mWinterServiceCharge;
}

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

总结:
    条件表达式一般并不长,但它们却增加了代码的复杂度,代码复杂度高,就会降低代码的可读性、可理解性以及可测试性,尽管提炼出的函数也都不长,但通过函数名的解释作用,重构后的代码读起来像一段注释一样清晰而明白。

圈复杂度:一种代码复杂度的衡量标准,数量上表现为独立线性路径条数。

1.2 合并条件表达式

概要:
你有一系列条件测试,都得到相同结果。
将这些测试合并为一个条件表达式,并将这个条件表达式提炼成一个独立函数。
动机:
a. 使条件检查的逻辑更清晰
b. 为提炼函数做前期准备
示例:
重构前:

double disabilityAmount() {
    if(mSeniority < 2) {
        return 0;
    }
    if(mMonthsDisabled > 12) {
        return 0;
    }
    if(mIsPartTime) {
        return 0;
    }
    // compute the disability amount
    ...
}

重构后:

double disabilityAmount() {
    if(isNotEligibleForDisability()) {
        return 0;
    }
    // compute the disability amount
    ...
}

private boolean isNotEligibleForDisability() {
    return (mSeniority < 2) || (mMonthsDisabled > 12) || mIsPartTime;
}

总结:
    合并的条件测试是并列的,它们各自独立地说明着自己的测试条件。

1.3 合并重复的条件片段

概要:
在条件表达式的每个分支上有着相同的一段代码。
将这段重复代码搬移到条件表达式之外。
动机:
a. 使代码更清楚地表明哪些东西随条件的变化而变化、哪些东西保持不变
示例:
重构前:

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

总结:
    共同的代码可能位于条件表达式的起始处、中段、尾端,需要视不同情况做应对处理。

1.4 移除控制标记

概要:
在一系列布尔表达式中,某个变量带有“控制标记”的作用。
以break语句或return语句取代控制标记。
动机:
a. 跳出复杂的条件语句,厘清条件语句的真正用途
示例:
重构前:

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

总结:
    首先要确认表达式中的控制变量,再确认移除标记变量是否会影响这段逻辑的最后结果。

1.5 以卫语句取代嵌套条件表达式

概要:
函数中的条件逻辑使人难以看清正常的执行路径。
使用卫语句表现所有特殊情况。
动机:
a. 给予某一条分支特别的重视,并简化代码流程
示例:
重构前:

double getPayAmount() {
    double result;
    if(mIsDead) {
        result = deadAmount();
    } else {
        if(mIsSeparated) {
          result = separatedAmount();
        } else {
           if(mIsRetired) {
              result = retiredAmount();
           } else {
              result = normalPayAmount();
           }
        }
    }
  return result;
}

重构后:

double getPayAmount() {
    if(mIsDead) {
        return deadAmount();
    }
    if(mIsSeparated) {
        return separatedAmount();
    }
    if(mIsRetired) {
        return retiredAmount();
    }
    return normalPayAmount();
}

总结:
    条件表达式常有两种表现形式:a)所有分支都属于正常行为;b)表达分支中只有一种是正常行为,其他都是不常见的情况。如果两条分支都是正常行为,就应该使用形如if...else...的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常被称为“卫语句”。
    if...then...else...结构传递给阅读者的消息是各个分支有同样的重要性。卫语句传递的消息是这种情况很罕见,如果真的发生了,做一些必要的工作后,及时退出。
    有时候可将条件反转来实现这种重构。

1.6 以多态取代条件表达式

概要:
你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。
将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。
动机:
a. 利用多态去除条件表达式,这种条件表达式是因为对象的类型不同而造成彼此行为各异。
示例:
重构前:

double getSpeed() {
    switch(mType) {
        case EUROPEAN:
            return getBaseSpeed();
        case AFRICAN:
            return getBaseSpeed() - getLoadFactor() * mNumberOfCoconuts;
        case NORWEGIAN_BLUE:
            return mIsNailed ? 0 : getBaseSpeed(mVoltage);
    }
    throw new RuntimeException("Should be unreachable.");
}

重构后:

abstract class Bird {
    abstract double getSpeed();
}

class European extends Bird {
    double getSpeed() {
        return getBaseSpeed();
    }
}

class African extends Bird() {
    double getSpeed() {
            return getBaseSpeed() - getLoadFactor() * mNumberOfCoconuts;
    }
}

class NorwegianBlue extends Bird {
    double getSpeed() {
            return mIsNailed ? 0 : getBaseSpeed(mVoltage);
    }
}

总结:
    多态是面向对象思想的基本特性之一,它与继承是紧耦合的关系,指的是有共同继承关系的类之间对同一个消息有其自己的应答。多态消除的条件表达式有两类:a)基于类型码的switch语句;b)基于类型名称的if...else语句。多态的引入使得你在需要新添加一种类型时,只需新建一个子类,并在其内提供适当的函数行为就行了,符合开闭原则。

1.7 引入Null对象

概要:
你需要再三检查某对象是否为null
将null值替换为null对象。
动机:
a. 通过Null Object模式,去除代码中对某一对象的多次判空处理
示例:
重构前:

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

重构后:

class Custom {
    public Plan getPlan() {
        return normalPlan;
    }

    public boolean isNull() {
        return false;
    }
}

class NullCustom extends Custom {
    public Plan getPlan() {
        return BillingPlan.basic();
    }

    public boolean isNull() {
        return true;
    }
}

//其中mCustom可以为Custom,也可以为NullCustom
plan = mCustom.getPlan();

总结:
    Null Object也叫虚拟对象,它也是一种设计模式,它可以去除重复的判空处理,但弊端是会造成问题侦查和查找上的困难。
    空对象一定是常量,它的任何成分都不会发生变化,因此可以用单例模式来实现它。
    只有当大多数客户代码都要求空对象做出相同响应时,行为搬移才有意义。
    可以新建一个接口来昭告使用了空对象。

interface Nullable {
    boolean isNull();
}

1.8 引入断言

概要:
某一段代码需要对程序状态做出某种假设。
以断言明确表现这种假设。
动机:
a. 以断言明确标注代码能正确运行的背后所隐藏的条件假设
总结:
    断言本质上是一个条件表达式,这是可以用它简化条件表达式的原因,它总是为真,只是用它来检查“一定必须为真”的条件。断言可作为交流与调试的辅助。在交流的角度上,断言可以帮助程序阅读者理解代码正确运行背后所作的假设;在调试角度上,断言可以让你在距离bug最近的地方抓住它们。
   可以类比单元测试中对断言的使用来理解如何用它来简化条件表达式。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 1 Decompose Conditional(分解条件表达式) 从复杂表达式if-then-else三个段落中分...
    hklbird阅读 1,427评论 0 0
  • 简化条件表达式 9.1 Decompose Conditional (分解条件表达式) 你有一个复杂的条件表达式语...
    rxdxxxx阅读 479评论 0 0
  • 我们在编写代码的过程中,有时会因为复杂的业务,导致我们编写的代码圈复杂度过大,if...then...else 过...
    司鑫阅读 533评论 0 3
  • 昨天近中午时,我正在搜肠刮肚的码字,忽然接到一个电话,说让我马上去接一下某亲戚。 春寒料峭,她拎着两个包在风里焦急...
    林姑娘与波斯猫阅读 1,241评论 2 2
  • 早晨起来的时候都是雪,外面的空场是,菜地上白茫茫的一大片。我好像有五年以上没有看过大雪了。以前都是在广东待得多那里...
    leayrainy阅读 111评论 0 1