分解条件式
if (data.before(SUMMER_START) || data.after(SUMMER_END))
charge = quantity * _winterRate + _winterServiceCharge;
else
charge = quantity * _summerRate;
========================>
if (notSummer(data))
charge = winterCharge(quantity);
else
charge = summerCharge(quantity);
private boolean notSummer(Date date){
return data.before(SUMMER_START) || data.after(SUMMER_END);
}
private double summerCharge(int quantity){
return quantity * _summerRate;
}
...
动机
程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。必须写代码检查不同的分支,根据不同的分支做不同的事情,然后会得到也给很长的函数,条件逻辑会使代码很难理解。
做法
- 将if段落提取出来,构成一个独立函数
- 将then段落和else段落都提炼出来,各自构成也给独立函数。
合并条件式
double disabilityAmount(){
if (_seniority < 2) return 0;
if (_monthsDisabled > 12) return 0;
if (_isPartTime) return 0;
...
}
========================>
double disabilityAmount(){
if (isNotEligableForDisability()) return 0;
...
}
boolean isNotEligibleForDisability(){
return ((_seniority < 2) || (_monthsDisabled > 12) || (_isPartTime));
}
动机
检查条件各不相同,最终行为却一致。如果发现这种情况,就应该使用logical-AND 和 logical-OR 将它们合并为一个条件式。
之所以要合并条件代码:
- 实际上只有一次条件检查,只不过有数个并列条件需要检查而已
- 将检查条件提炼成一个独立函数对于厘清代码意义非常有用,因为它把描述**【做什么】的语句换成了【为什么这样做】**。
- 条件语句的【合并理由】也同时指出了【不要合并】的理由。
做法
- 确定这些条件语句都没有副作用(连带影响)
- 如果条件式有副作用,你就不能使用本项重构。
- 使用适当的逻辑操作符,将一系列相关条件式合并为一个。
- 编译,测试。
- 对合并后的条件式实施Extract Method。
范例
代码展示"logical-AND"的用法:
if (onVacation())
if (lengthOfService()>10)
return 1;
return 0.5;
========================>
if (onVacation() && lengthOfService() > 10) return 1;
else return 0.5;
return (onVacation() && lengthOfService() > 10) ? 1: 0.5;
合并重复的条件片段
在条件式的每个分支上有着相同的一段代码。
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();
动机
有时候你会发现,一组条件式的所有分支都执行了相同的某段代码。如果这样应该将这段代码搬移到条件式外面。这样,代码才能更清楚地表明那些东西随时间变化而变化、那些东西保持不变。
做法
- 鉴别出【执行方式不随条件变化而变化】的代码
- 如果这些共通代码位于条件式起始处,就将它移到条件式之前。
- 共通代码位于条件式尾端, 就将它移到条件式之后
- 这些共通代码位于条件式中段, 就需要观察共通代码之前或之后的代码是否改变了什么东西,如果的确有所改变, 应该首先将共通代码向前或向后 移动, 移至条件式的起始处或尾端。
- 如果共通代码不止一条语句, 你应该首先使用以Extract Method 将共通 代码提炼到一个独立函数中, 再以前面所说的办法来处理。
移除控制标记
在一系列布尔表达式( boolean expressions) 中, 某个变量带有「 控制标记」 ( control flag) 的作用。以break 语句或return 的语句取代控制标记。
动机
set done to false
while not done
if (condition)
do something
set done to true
next step of loop
结构化编程原则告诉他们:
每个子程序( routines) 只能有一个入口( entry) 和一个出口( exit) 。
但是「 单一出口」 原则会让你在代码中加入讨厌的控制标记, 大大降低条件表达式的可读性。
这就是编程语言提供break 语句和continue 语句的原因: 你可以用它们跳出复杂的条件语句。
做法
对控制标记处理,使用break或者continue语句。
- 找出让你得以跳出这段逻辑的控制标记值。
- 找出将可跳出条件式之值赋予标记变量 的那个语句, 代以恰当的break 语句或continue 语句。
- 每次替换后, 编译并测试。
在未能提供break 和continue 语句的编程语言中
- 将整段逻辑提炼到一个独立函数中
- 找出让你得以跳出这段逻辑 的那些控制标记值。
- 找出将可跳出条件式之值赋予标记变量的那个语句, 代以恰当的return 语句。
- 每次替换后, 编译并测试。
范例
下列函数用来检查一系列人名之中是否包含两个可疑人物的名字:以break/continue
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";
}
}
}
}
================step: 1================>
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();
found = "Don";
}
if (people[i].equals("John")){
sendAlert();
found = "John";
}
}
}
return found;
}
================step: 2================>
String foundMiscreant(String[] people){
String found = "";
for (int i = 0; i < people.length; i ++){
// if (found.equals("")){ 删掉控制变量语句
if (people[i].equals("Don")){
sendAlert();
// found = "Don"; 赋予标记变量的那个语句,以return代替
return "Don";
}
if (people[i].equals("John")){
sendAlert();
// found = "John"; 赋予标记变量的那个语句,以return代替
return "John";
}
// }
}
return found;
}
以卫语句取代嵌套条件式
函数中的条件逻辑( conditional logic) 使人难以看清正常的执行路径。
动机
条件式呈现形式:
- 所有分支都属于正常行为
- 只有一种是正常行为, 其他都是不常见的情况
卫语句告诉阅读者: 『 这种情况很罕见, 如果它真的发生了, 请做 一些必要的整理工作, 然后退
出。 』
作法
- 对于每个检查, 放进一个卫语句
- 从函数中返回/抛出一个异常
- 将「 条件检查」 替换成「 卫语句」 后, 编译并测试
- 所有卫语句都导致相同结果,使用绝对条件表达
范例
薪资系统
double getPayAmount(){
double result;
// 非正常情况的检查掩盖了正常情况的检查
// 应该使用『卫语句」 来取代这些检查, 以提高程序清晰度
if (_isDead) result = deadAmount();
else
if (_isSeparated) result = separatedAmount();
else {
if (_isRetired) result = retiredAmount();
// 正常情况
else result = normalPayAmount();
}
}
return reuslt;
}
=============================================>
// 并不遵循每个函数只能有一个出口!!!!
// 嵌套条件代码往往由那些深信「每个函数只能有一个出口」 的程序员写出。
double getPayAmount(){
if (_isDead) return deadAmount();
if (_isSearated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
}
将条件逆反
double getAdjustedCapital(){
double result = 0.0;
if (_capital > 0.0){
if (_intRate > 0.0 && _duration > 0.0){
result = (_income / _duration) * ADJ_FACTOR;
}
}
return result;
}
====================================================>
// 这次在插入卫语句(guard clauses) 时, 我需要将相应的条件逆反过来:
double getAdjustedCapital(){
double result = 0.0;
// if (_capital > 0.0){ 逆反条件
if (_capital <= 0.0) return result;
// 分两步逆反:1. 首先加入一个"logical-NOT"操作
// if (_intRate > 0.0 && _duration > 0.0){
// if (!(_intRate > 0.0 && _duration > 0.0)) return result;
if (_intRate <= 0.0 || _duration <= 0.0) return result;
// 剩下的情况就是原来满足所有条件的表达式
result = (_income / _duration) * ADJ_FACTOR;
}
// }
return result;
}
// 2. 将临时变量移除
double getAdjustedCapital(){
if (_capital <= 0.0) return 0.0;
if (_intRate <= 0.0 || _duration <= 0.0) return 0.0
return (_income / _duration) * ADJ_FACTOR;
}
以多态取代条件式
动机
多态( polymorphism) 最根本的好处就是:
如果你需要根据对象的不同型别而采取不同的行为, 多态使你不必编写明显的条件式( explicit conditional ) 。「 针对type code( 型别码) 而写的switch 语句」 以及「 针对type string ( 型别名称字符串) 而写的if-then-else 语句」 在面向对象程序中很少出现。
做法
- 如果要处理的条件式是一个更大函数中的一部分, 首先对条件式进行分析, 然后使用Extract Method 将它提炼到一个独立函数去。
- 如果有必要, 使用Move Method 将条件式放置到继承结构的顶端。
- 任选一个subclass , 在其中建立一个函数, 使之覆写superclass 中容纳条件式的那个函数。 将「与subclass 相关的条件式分支」 拷贝到新建函数中, 并对它进行适当调整。
- 为了顺利进行这一步骤, 你可能需要将superclass 中的某些private 值域声明为protected 。
- 编译, 测试。
- 在superclass 中删掉条件式内被拷贝出去的分支。
- 编译, 测试。
- 针对条件式的每个分支, 重复上述过程, 直到所有分支都被移到subclass 内的函数为止。
- 将superclass 之中容纳条件式的函数声明为抽象函数(abstract method) 。
范例
员工与薪资
class Employee...
/* 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("Incorrect Employee");
}
}
*/
int getType() {
return _type.getTypeCode();
}
private EmployeeType _type;
// EmployeeType 才是被subclassing 的class 。
abstract class EmployeeType...
abstract int payAmount(Employee emp);
stract int getTypeCode();
class Engineer extends EmployeeType...
int payAmount(Employee emp) {
return emp.getMonthlySalary();
}
int getTypeCode() {
return Employee.ENGINEER;
}
... and other subclasses
引入Null对象
动机
系统在对对象发送一个消息之前, 总要检査对象是否存在,这样的检査出现很多次。造成的重复代码让我们很烦心。编写了一个MissingPerson class, 让它返回 ‘0’ 薪资等级( 我们把null objects 称为missing object( 虚构对象)。请记住: null objects 一定是常量, 它们的任何成分都不会发生变化,可以使用Singleton 模式[Gang of Four]来实现它们。
作法
- 为source class 建立一个subclass , 使其行为像source class 的null 版本。在source class 和null class 中都加上isNull() 函数, 前者的isNull() 应该返回false, 后者的isNull() 应该返回true。
- 建立一个nullable 接口, 将isNull() 函数放在其中, 让source class 实现这个接口。
- 创建一个testing 接口, 专门用来检查对象是否为null
- 编译
- 找出所有「 索求source object 却获得一个null 」 的地方。 修改这些地方, 使它们改而获得一个null object
- 找出所有「 将source object 与null 做比较」 的地方。 修改这些地方, 使它们调用isNull() 函数。
- 每次只处理一个source object 及其客户程序, 编译并测试后, 再处理另一个source object 。
- 在「 不该再出现null value」 的地方放上一些assertions( 断言) , 确保null 的确不再出现。
- 编译, 测试
- 如果对象不是null , 做A动作, 否则做B 动作。
- 对于每一个上述地点, 在null class 中覆写A动作, 使其行为和B 动作相同
- 使用上述的被覆写动作( A) , 然后删除「 对象是否等于null」 的条件测试。 编译并测试。
引入断言
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();
}
动机
有时程序员会以注释写出这样的假设。 而我要介绍的是一种更好的技术: 使用assertion( 断言) 明确标明这些假设。assertion 是一个条件式, 应该总是为真。 如果它失败, 表示程序员犯了错误。
作法
如果程序员不犯错, assertions 就应该不会对系统运行造成任何影响, 所以加入assertions 永远不会影响程序的行为。
- 如果你发现代码「 假设某个条件始终( 必须) 为真], 就加入一个assertion 明确说明这种情况
- 你可以新建一个Assert class, 用于处理各种情况下的assertions 。
你应该常常问自己: 如果assertions 所指示的约束条件不能满足, 代码是否仍能正常运行? 如果可以, 就把assertions 拿掉。