1. 前言
本文是代码重构系列的最后一篇啦。前面三篇《重构·改善既有代码的设计.01之入门基础》、《重构·改善既有代码的设计.02之代码的“坏味道”》、《重构·改善既有代码的设计.03之重构手法(上)》介绍了基础入门,代码异味,还有部分重构手法。今天继续总结重构手法下篇,从条件表达式、函数调用、以及类继承关系上阐述了各种重构手法,希望对项目能有所帮助。另外本文更新后该系列就完结了,感谢各位看官的指点。
2. 简化条件表达式
“分支逻辑”和“操作细节”分离。
1、Decompose Conditional 分解条件表达式。
复杂的条件语句(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);}private boolean notSummer(Date date) { return date.before(SUMMER_START) || date.after(SUMMER_END);}private double summerCharge(int quantity) { return quantity * _summerRate;}private double winterCharge(int quantity) { return quantity * _winterRate * _winterServiceCharge;}
2、Consolidate Conditional Expression 合并条件表达式。
有一系列条件测试,都得到相同结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成一个独立函数。
改造前:
double disabilityAmount(){ if(_seniority < 2) { return 0; } if(_monthDisabled > 12) { return 0; } if(_isPartTime) { return 0; } // todo...}
改造后:
double disabilityAmount(){ if((_seniority < 2) || (_monthDisabled > 12) || _isPartTime) { return 0; } // todo...}或:double disabilityAmount(){ if(isDisability()) { return 0; } // todo...}boolean isDisability() { return (_seniority < 2) || (_monthDisabled > 12) || _isPartTime;}
3、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();
4、Remove Control Flag 移除控制标记。
循环体中,通常需要判断何时停止条件检查。有时会引入某个控制变量来起到循环判断的作用。建议以break或continue或return 语句取代控制标记。
改造前:
void check(String[] person) { boolean found = false; // 控制标记 for(int i = 0; i < person.length; i++) { if(!found) { if(person[i] == "tom") { found = true; sendAlert(); } if(person[i] == "jose") { found = true; sendAlert(); } } } }
改造后:
void check(String[] person) { boolean found = false; // 控制标记 for(int i = 0; i < person.length; i++) { if(!found) { if(person[i] == "tom") { sendAlert(); break; } if(person[i] == "jose") { sendAlert(); break; } } } }
5、Replace Nested Conditional with Guard Clauses 以卫语句取代嵌套条件表达式。
条件表达式中,如果两条分支都是正常行为,使用形如if…else…的条件表达式;如果某个条件极为罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回(如参数校验判断)。这样的单检查称为“卫语句”。
卫语句要么就从函数中返回,要么就抛出一个异常。
改造前:
double getPayment(){ double result; if(_isDead) { result = deadAmount(); } else { if(_isSeparated) { result = separatedAmount(); } else { if(_isRetired) { result = retiredAmount(); } else { result = normalAmount(); } } } return result;}
改造后:
double getPayment(){ if(_isDead) { return deadAmount(); } if(_isSeparated) { return separatedAmount(); } if(_isRetired) { return retiredAmount(); } return normalAmount(); }
6、Replace Conditional with Polymorphism 以多态取代条件表达式。
改造前:
class Employee { private EmployeeType _type; int payment() { switch(getType()) { case EmployeeType.ENGINEER: return _monthlySalary; case EmployeeType.SALESMAN: return _monthlySalary + _commission; case EmployeeType.MANAGER: return _monthlySalary + _bonus; default: throw new RuntimeException("error"); } } int getType() { return _type.getTypeCode(); }}abstract class EmployeeType { abstract int getTypeCode();}class Engineer extends EmployeeType { int getTypeCode(){ return EmployeeType.ENGINEER; }}class Salesman extends EmployeeType { int getTypeCode(){ return EmployeeType.SALESMAN; }}class Manager extends EmployeeType { int getTypeCode(){ return EmployeeType.MANAGER; }}
改造后:
class Employee { private EmployeeType _type; int payment() { return _type.payment(); }}abstract class EmployeeType { abstract int payment(Employee emp);}class Engineer extends EmployeeType { int payment(Employee emp){ return emp.getMonthlySalary(); }}class Salesman extends EmployeeType { int payment(Employee emp){ return emp.getMonthlySalary() + emp.getCommission(); }}class Manager extends EmployeeType { int payment(Employee emp){ return emp.getMonthlySalary() + emp.getBonus(); }}
7、Introduce Null Object 引入Null对象。
将null替换为null对象。
改造前:
class Site { private Customer _customer; Customer getCustomer() { return _customer; }}class Customer { public Stirng getName() {...} public BillingPlan getPlan(){...}}// 调用Customer customer = site.getCustomer();if(customer == null) { plan = BillingPlan.basic();} else { plan = customer.getPlan();}String customerName;if(customer == null) { customerName = "default";} else { customerName = customer.getName();}
改造后:
class Site { private Customer _customer; Customer getCustomer() { return _customer == ull ? Customer.newNull() : _customer; }}class Customer { public Stirng getName() {...} public BillingPlan getPlan(){...} public boolean isNull() { return false; } static Customer newNull() { return new NullCustomer(); }}// 定义NullCustomer空对象class NullCustomer extends Customer{ public boolean isNull() { return true; }}// 调用Customer customer = site.getCustomer();if(customer.isNull()) { plan = BillingPlan.basic();} else { plan = customer.getPlan();}String customerName;if(customer.isNull()) { customerName = "default";} else { customerName = customer.getName();}
8、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();}
断言,请不要用它来检查“你认为应该为真”的条件,请只使用它来检查“一定必须为真”的条件。请勿滥用。
如果断言所指示的约束条件不能满足,代码是否仍能正常运行? 如果可以,就把断言去掉。
3. 简化函数调用
容易被理解和被使用的接口,是开发良好面向对象软件的关键。
1、Rename Method 函数改名。
将复杂的处理过程分解成小函数。但是如果做的不好,会使你费尽周折却弄不清楚这些小函数各自的用途。要避免这种麻烦,关键在于给函数起一个好名称。
1、尽可能起一个良好名称的函数,顾名思义表达该函数的作用,而不是表达该函数如何做。
2、合理安排函数签名,如果重新安排参数顺序,能够帮助提供代码的清晰度。
2、Add Parameter 添加参数。
为函数添加一个对象参数,让该对象带进函数所需信息。
动机:
你必须修改一个函数,而修改后的函数需要一些过去没有的信息,因此你需要给该函数添加一个参数。
改造前:
double getExpenseLimit(double limit) { // todo...}// 需要添加一个参数double getExpenseLimit(double limit, Date date) { // todo...}
改造后:
double getExpenseLimit(ExpenseLimit limitObj) { // todo... double limit = limitObj.getLimit(); Date date = limitObj.getDate();}class ExpenseLimit { double limit; Date date;}
但是需要警惕引用传递。其实我并不推荐整个对象传参。当你传整个参数时,对于这个函数你不能准确的说出这个函数所使用的参数。有可能对象包含了5个参数,而你理论上只需要3个。 这时候宁可将参数依次卸载参数列表中。不过所带来的影响是代码参数过长。如果过长,也是不太友好的。
推荐:适当使用参数列表和对象参数,必要时可以进行函数重载更简洁说明函数意图。比如:
double getExpenseLimit(double limit) { this.getExpenseLimit(limit, new Date());}double getExpenseLimit(double limit, Date date) { ExpenseLimit limitObj = new ExpenseLimit(limit, date); this.getExpenseLimit(limitObj);}double getExpenseLimit(ExpenseLimit limitObj) { // todo... double limit = limitObj.getLimit(); Date date = limitObj.getDate();}class ExpenseLimit { double limit; Date date;}
3、Remove Parameter 移除参数。
移除函数体无用参数。
改造前:
double getExpenseLimit(double limit, Date date) { return limit * 0.98;}
改造后:
double getExpenseLimit(double limit) { return limit * 0.98;}
4、Separate Query from Modifier 将查询函数和修改函数分离。
某个函数既返回对象状态值,又修改对象状态。
5、Parameterize Method 令函数携带参数。
若干函数做了类似的工作,但在函数本体中却包含了不同的值。
改造前:
class Employee { void tenPercentRaise() { salary *= 1.1; } void fivePercentRaise() { salary *= 1.05; }}
改造后:
class Employee { void raise(double factor) { salary *= (1 + factor); }}
6、Replace Parameter with Explicit Methods 以明确函数取代参数。
有一个函数,其中完全取决于参数值而采取不同行动。
改造前:
void setValue(String name, int value) { if(name.equals("height")) { _height = value; return ; } if(name.equals("width")) { _width = value; return ; }}
改造后:
void setHeight(int value) { _height = value;}void setWidth(int value) { _width = value;}
7、Preserve Whole Object 保持对象完整。
从某个对象中取出若干值,将它们作为某一个函数调用时的参数。
改造前:
int low = daysTempRange().getLow();int high = daysTempRange().getHigh();withinPlan = plan.withinRange(low, high);
改造后:
withinPlan = plan.withinRange(daysTempRange());
注:
该方法总有两面。如果你传的是数值,被调用函数就只依赖于这些数值,而不依赖它们所属的对象。但如果你传递的是整个对象,被调用函数所在的对象就需要依赖参数对象。如果这样,会使你的依赖结构恶化,那么就不该使用该方法。
8、Replace Parameter with Methods 以函数取代参数。
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。
改造前:
int basePrice = _quantity * _itemPrice;discountLevel = getDiscountLevel();double finalPrice = discountedPrice(basePrice, discountLevel);
改造后:
int basePrice = _quantity * _itemPrice;double finalPrice = discountedPrice(basePrice);double discountedPrice(int basePrice) { discountLevel = getDiscountLevel(); ......}
9、Introduce Parameter Object 引入参数对象。
某些参数总是很自然地同时出现。以一个对象取代这些参数。
改造前:
void amountInvoicedIn(Date start, Date end);void amountReceicedIn(Date start, Date end);void amountOverdueIn(Date start, Date end);
改造后:
void amountInvoicedIn(DateRange dateRange);void amountReceicedIn(DateRange dateRange);void amountOverdueIn(DateRange dateRange);class DateRange { Date start; Date end; ...}
10、Remove Setting Method 移除设值函数。
类中的某个字段应该在对象创建时被设值,然后就不再改变。
改造前:
class Acount { private String _id; Acount(String id) { setId(id); } public void setId(String arg) { _id = arg; }}
改造后:
class Acount { private final String _id; Acount(String id) { _id = id; }}
一般我们在处理多线程运算的时候,此类方式使用的比较多。
11、Hide Method 隐藏函数。
有一个函数,从来没有被其他任何类使用到。将这个函数修改为private。
12、Replace Constructor with Factory Method 以工厂函数取代构造函数。
你希望在创建对象时,不仅仅是简单的构建动作。
改造前:
Employee (int type) { _type = type;}
改造后:
static Employee create(int type) { return new Employee(type);}
13、Encapsulate Downcast 封装向下转型。
某个函数返回的对象,需要由函数调用者执行向下转型。将向下转型动作移动到函数中。
改造前:
Object lastReading() { return readings.lastElement();}
改造后:
Reading lastReading() { return (Reading) readings.lastEmelent();}
14、Replace Error Code with Exception 以异常取代错误码。
某个函数返回一个特定的代码,用以表示某种错误情况。
改造前:
int withdraw(int amount) { if(amount > _balance) { return -1; } else { _balance -= amount; return 0; }}
改造后:
void withdraw(int amount) throws BalanceException { if(amount > _balance) { throw new BalanceException("xxx"); } _balance -= amount;}
15、Replace Exception with Test 以测试取代异常。
面对一个调用者可以预先检查的条件,你抛出了一个异常。
改造前:
double getValueForPeriod(int period) { try { return _values[period]; } catch(ArrayIndexOutofBoundsException e) { return 0; }}
改造后:
double getValueForPeriod(int period) { if(period >= _values.length) { return 0; } return _values[period]; }
异常只应该被用于异常的、罕见的行为,也就是那些产生意料之外的错误行为,而不应该成为条件检查的替代品。如果你可以合理期望调用者在调用函数之前先检查某个条件,那么就应该提供一个测试,而调用者应该使用它。
4. 处理概括关系
专门用来处理类的概括关系(继承关系),其中主要是将函数上下移动于继承体系之中。
1、Pull Up Field 字段上移。
两个子类拥有相同的字段。将该字段移至超类。
改造前:
class Employee{ ...}class Salesman extends Employee { String name; ...}class Manager extends Employee { String name; ...}
改造后:
class Employee{ String name;}class Salesman extends Employee { ...}class Manager extends Employee { ...}
2、Pull Up Method 函数上移。
有些函数,在各个子类中产生完全相同的结果。
改造前:
class Employee{ ...}class Salesman extends Employee { String getName(){ return _name; } ...}class Manager extends Employee { String getName(){ return _name; } ...}
改造后:
class Employee { String getName(){ return _name; }}class Salesman extends Employee { ...}class Manager extends Employee { ...}
3、Pull Up Constructor Body 构造函数本体上移。
各个子类中拥有一些构造函数,他们的本地几乎完全一致。
改造前:
class Employee { String _id; String _name;}class Manager extends Employee { int _grade; public Manager(String name, String id, int grade) { _id = id; _name = name; _grade = grade; }}
改造后:
class Employee { String _id; String _name; Employee(String name, String id) { _id = id; _name = name; }}class Manager extends Employee { int _grade; public Manager(String name, String id, int grade) { super(name, id); _grade = grade; }}
4、Push Down Method 函数下移。
超类中的某个函数只与部分(而非全部)子类有关。将这个函数移到相关的那些子类去。恰好与函数上移相反。
5、Push DOwn Field 字段下移。
超类中的某个字段只被部分(而非全部)子类用到。恰好与字段上移相反。
6、Extract Subclass 提炼子类。
类中某些特性只被某些(而非全部)实例用到。可以新建一个子类,将上面所说的那一部分特性移到子类中。
7、Extract Superclass 提炼超类。
两个类具有相似特性。为这两个类建立一个超类,将相同特性移至超类。与提炼子类相反。
改造前:
class Cat { void eat(){ ... } void miaomiao(){ ... }}class Bird { void eat(){ ... } void fly(){ ... }}
改造后:
class Animal { void eat(){ ... }}class Cat extends Animal { void miaomiao(){ ... }}class Bird extends Animal { void fly(){ ... }}
8、Extract Interface 提炼接口。
改造前:
double charge(Employee emp, int days) { int base = emp.getRate() * days; if(emp.hasSpecialSkill()) { return base * 1.05; } else { return base; }}
改造后:
interface Billable { public int getRate(); public boolean hasSpecialSkill();}class Employee implements Billable { double charge(Billable billable, int days) { int base = billable.getRate() * days; if(billable.hasSpecialSkill()) { return base * 1.05; } else { return base; }}
当如果有若干个子类都实现了Billable的接口,他就会很有用。
9、Collapse Hierachy 折叠继承体系。
超类和子类之间无太大区别,可以将他们合为一体。
10、Form Template Method 塑造模板函数。
有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。可以将这些操作分别放进独立函数中,并保持他们都有相同的签名,于是源函数也就变得相同了。然后将源函数上移至超类中。
11、Replace Inheritance with Delegation 以委托取代继承。
某个子类只使用了超类接口的一部分,或是根本不需要继承而来的数据。
改造前:
// 这里的MyStack只需要push()、push()、size()、isEmpty()这四个函数。继承了vector只是因为使用了size()和isEmpty()方法class MyStack extends Vector { public void push(Object element) { insertElementAt(element, 0); } public Object pop(){ Object result = firstElement(); removeElementAt(0); return result; }}
改造后:
// 这里将继承改为类委托class MyStack { Vector _vector; public void push(Object element) { _vector.insertElementAt(element, 0); } public Object pop(){ Object result = _vector.firstElement(); _vector.removeElementAt(0); return result; } public int size() { return _vector.size(); } public boolean isEmpty() { return _vector.isEmpty(); }}
12、Replace Delegation with Inheritance 以继承取代委托。
与Replace Inheritance with Delegation刚好相反。在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。
5. 小结
到此已经汇总了书中全部的重构手法,依旧保持个人观点,部分重构手法是以牺牲一定的代码阅读性为代价。且书中提到的多数重构手法还是要视具体编程场景而定。避免错误引用。
重构手法和设计模式一样,均为编程模式中的最佳实践。是符合大多数场景和行为的思想或方法的总结。记住是大多数。了解最佳实践有助于提高平常的编码习惯以及提升代码的维护性,可修改性。但如果被错误引用,程序将因为过度设计或引用而变得臃肿。