- 重构之简化条件表达式
- Decompose Conditional(分解条件表达式)
- Consolidate Conditional Expression(合并条件表达式)
- Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
- 动机
- Remove Control Flag(移除控制标记)
- Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)
- Replace Conditional with Polymorphism(以多态取代条件表达式)
- Introduce Null Object(引入Null 对象)
- Introduce Assertion(引入断言)
重构之简化条件表达式
@(常识)
[toc]
那有什么天生如此,只是我们天天坚持。
本篇文章主要讲解 《重构—-改善既有代码的设计》 这本书中的 第九章简化条件表达式中 的知识点,
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(提炼函数)做好准备做法
确定这些条件语句都没有副作用
- 使用适当的逻辑操作符,将一系列相关条件表达式合并为一个
- 编译,测试
-
Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
问题:在条件表达式的每个分支上有着相同的一段代码
解决:将这段重复代码搬移到条件表达式之外//重构前if(isSpecialDeal()){total = price * 0.95;send();}else{total = price * 0.98;send();}
//重构后if(isSpecialDeal())total = price * 0.95;elsetotal = 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;elsetotal = price * 0.98;send();
我们也可以使用同样的手法来对待异常(exceptions)。如果在try 区段内「可能引发异常」的语句之后,以及所有catch 区段之内,都重复执行了同一段代码,我就 可以将这段重复代码移到final 区段。
Remove Control Flag(移除控制标记)
问题:在一系列布尔表达式中,某个变量带有”控制标记”的作用
解决:以break语句或return语句### 制标记动机
在一系列条件表达式中,你常常会用到[用以何时停止条件检查]的控制标记。
set done to falsewhile not doneif (condition)//do something//set done to truenext step of loop
做法
找出让你跳出这段逻辑的控制标记值
- 找出对标记变量赋值的语句,代以恰当的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");}
动机
多态最根本的好处就是:如果需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式
做法
如果要处理的条件表达式是一个更大函数中的一部分,首先对条件表达式进行分析,然后使用Extract Method将它提炼到一个独立函数中
- 如果有必要,使用Move Method(搬移函数)将条件表达式放置到继承结构的顶端
- 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数,将与该子类相关的条件表达式分支复制到新建函数中,并对它进行适当调整
- 编译,测试
- 在超类中删除条件表达式内被复制了的分支
- 编译,测试
- 针对条件表达式的每个分支,重复上述过程,直到所有分支都被移动子类内函数为止
-
范例
请允许我继续使用「员工与薪资」这个简单而又乏味的例子。
继承构://重构前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);
aIntroduce Null Object(引入Null 对象)
问题:你需要再三检查某物是否为null value
解决:将null值替换为null对象if (customer == null) plan = BillingPlan.basic();else plan = customer.getPlan();
//重构后
启哥说: 和提供一个默认的不做任何处理的空实现是一个意思动机(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() 等函数。
