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最近的地方抓住它们。
可以类比单元测试中对断言的使用来理解如何用它来简化条件表达式。