那有什么天生如此,只是我们天天坚持。
本篇文章主要讲解 《重构---改善既有代码的设计》 这本书中的 第九章简化条件表达式中 的知识点,
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(提炼函数)做好准备
做法
- 确定这些条件语句都没有副作用
- 使用适当的逻辑操作符,将一系列相关条件表达式合并为一个
- 编译,测试
- 对合并后的条件表达式实施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;
else
total = 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;
else
total = price * 0.98;
send();
我们也可以使用同样的手法来对待异常(exceptions)。如果在try 区段内「可能引发异常」的语句之后,以及所有catch 区段之内,都重复执行了同一段代码,我就 可以将这段重复代码移到final 区段。
Remove Control Flag(移除控制标记)
问题:在一系列布尔表达式中,某个变量带有”控制标记”的作用
解决:以break语句或return语句取代控制标记
动机
在一系列条件表达式中,你常常会用到[用以何时停止条件检查]的控制标记。
set done to false
while not done
if (condition)
//do something
//set done to true
next step of loop
用break语句和continue语句跳出复杂的条件语句
做法
- 找出让你跳出这段逻辑的控制标记值
- 找出对标记变量赋值的语句,代以恰当的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);
a
Introduce 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() 等函数。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。