1、分解条件表达式(Decompose Condition)
(1)症状:你有一个复杂的条件(if - then - else )语句
(2)解决:从if、then、else三个段落中分别提炼出独立函数
(3)将任何大块头代码分解成多个独立函数,根据每个小块代码的用途为分解而得的新函数命名
(4)将每个分支条件分解成新函数可以突出条件逻辑,更清楚地表明每个分支的作用,突出每个分支的原因
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)
(1)症状:有一系列条件测试,都得到相同的结果
(2)解决:将这些测试合并为一个条件表达式,并将这个条件表达式提炼为一个独立函数
(3)一连串条件检查,检查条件各不相同,但最终行为却一致
(4)合并后的条件代码会更清晰的表明只有一次条件检查,只不过有多个并列条件需要检查而已
(5)如果有些检查彼此独立,的确不应该被视为同一次检查,就不要使用本项重构
if (seniorty < 2) {
return 0;
}
if (monthDisabled > 12) {
return 0;
}
if (isPartTime) {
return 0;
}
----------------------------------------------------------------------------------
重构(合并条件表达式)后的代码
if (isNotEligableForDisability()) {
return 0;
}
private Boolean isNotEligableForDisability() {
return seniorty < 2 || monthDisabled > 12 || isPartTime;
}
3、合并重复的条件片段(Consolidate Duplicate Conditional Fragments)
(1)症状:在条件表达式的每个分支上有着相同的一段代码
(2)解决:将这段重复代码搬移到条件表达式之外
(3)合并重复代码有利于清楚地表明哪些东西随条件的变化而变化、哪些东西保持不变
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)也可以使用同样的手法来对待异常(exceptions),如果在try 区段内「可能引发异常」的语句之后,以及所有catch 区段之内,都重复执行了同一段代码,就可以将这段重复代码移到finally区段。
4、移除控制标记(Remove Control Flag)
(1)症状:在一系列布尔表达式中,某个变量带有“控制标记(control flag)”的作用
(2)解决:以break语句或return语句取代控制标记
(3)在未能提供break和Continue语句的编程语言中,可以使用Extract Method方法,将整段逻辑提炼到一个独立函数中
以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;
}
}
}
}
这种情况下很容易找出控制标记:当变量found 被赋予true 时,搜索就结束,可以逐一引入break 语句:
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;
}
}
}
5、以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
(1)症状:函数中的条件逻辑使人难以看清正常的执行路径
(2)解决:使用卫语句表现所有特殊情况
(3)卫语句:如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回
(4)本项重构的精髓:给某条分支以特别的重视,告诉读者如果真的发生了,请做一些必要的整理工作,然后退出
(5)卫语句要不就从函数中返回,要不就抛出一个异常
范例1:想像一个薪资系统,其中以特殊规则处理死亡员工、驻外员工、退休员工的薪资。这些情况不常有,但的确偶而会出现。
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();
};
范例2:将条件逆反(Reversing the Conditions)
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() {
if (_capital <= 0.0) return 0.0;
if (_intRate <= 0.0 || _duration <= 0.0) return 0.0;
return (_income / _duration) * ADJ_FACTOR;
}
6、以多态取代条件表达式(Replace Conditional with Polymorphism)
(1)症状:你手上有个条件表达式,它根据对象类型的不同而选择不同的行为
(2)解决:将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数
(3)多态使你不必编写明显的条件表达式,可以根据对象的不同类型而采取不同的行为
(4)多态在一定程度上消除“类型码的switch语句”以及“基于类型名称的if-then-else语句”
- 如果要处理的条件式是一个更大函数中的一部分,首先对条件式进行分析,然后使用Extract Method 将它提炼到一个独立函数去。
- 如果有必要,使用Move Method 将条件式放置到继承结构的顶端。
- 任选一个subclass ,在其中建立一个函数,使之覆写superclass 中容纳条件式的那个函数。将「与subclass 相关的条件式分支」拷贝到新建函数中,并对它进行适当调整。
- 为了顺利进行这一步骤,你可能需要将superclass 中的某些private 值域声明为protected 。
- 编译,测试。
- 在superclass 中删掉条件式内被拷贝出去的分支。
- 编译,测试。
- 针对条件式的每个分支,重复上述过程,直到所有分支都被移到subclass 内的函数为止。
- 将superclass 之中容纳条件式的函数声明为抽象函数(abstract method)。
class Engineer extends EmployeeType{
int getTypeCode(){
return Employee.ENGINEER;
}
}
class Manager extends EmployeeType{
int getTypeCode(){
return Employee.MANAGER;
}
}
class Salesman extends EmployeeType{
int getTypeCode(){
return Employee.SALEMAN;
}
}
class Employee...
private employeeType _type;
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();
}
}
int getType(){
return _type.getTypeCode();
}
void setType(int arg){
_type = EmployeeType.newType(arg);
}
class employeeType...
static EmployeeType newType(int code){
switch(code){
case ENGINEER:
return new Engineer();
case SALESMAN:
return new Salesman();
case MANAGER:
return new Manager();
default:
throw new IllegalArgumentException();
}
}
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
-----------------------------------------------------------------------------------------------
重构(以多态取代条件表达式)后的代码
class Employee...
private Engineer _type;
int payAmount(){
return _type.payAmount(this);
}
class Engineer...
int payAmount(Employee emp){
return emp.getMonthlySalary();
}
class Salesman...
int payAmount(Employee emp){
return emp.getMonthlySalary() + emp.getCommission();
}
class Manager...
int payAmount(Employee emp){
return emp.getMonthlySalary() + emp.getBonus();
}
class EmployeeType...
abstract int payAmount(Employee emp);
7、引入Null对象(Introduce Null Object)
(1)症状:你需要再三检查某对象是否为null
(2)解决:将null值替换成null对象
(3)空对象一定是常量,它们的任何成分都不会发生变化
- 为源类建立一个子类,使其行为就像是源类的null版本。在源类和null子类中都加上IsNull()函数,前者的IsNull()应该返回false,后者的IsNull()返回true。
- 编译。
- 找出所有“索求源对象却获得一个null”的地方。修改这些地方,使它们改而获得一个空对象。
- 找出“将源对象与null做比较”的地方。修改这些地方,使它们调用IsNull()函数。
- 编译、测试。
- 找出这样的程序点:如果对象不是null,做A动作,否则做B动作。
- 对于每一个上述地点,在null类中覆写A动作,使其行为和B动作相同。
- 使用上述被覆写的动作,然后删除“对象是否等于null”的条件测试。编译并测试。
if (customer == null) {
plan = BillingPlan.basic();
}
else {
plan = customer.getPlan();
}
----------------------------------------------------------------------------------
重构之后的代码
class NullCustomer extends Customer {
boolean isNull() {
return true;
}
Plan getPlan() {
return new NullPlan();
}
// Some other NULL functionality.
}
// Replace null values with Null-object.
customer = (order.customer != null) ?order.customer : new NullCustomer();
// Use Null-object as if it's normal subclass.
plan = customer.getPlan();
8、引入断言(Introduce Assertion)
(1)症状:某一段代码需要对程序状态作出某种假设
(2)解决:以断言明确表现这种假设
(3)通常这样的假设并没有在代码中明确的表现出来,有时程序员会以注释写出这样的假设
(4)断言是一种更好的解决方案,断言是一个条件表达式,如果它失败了就表示程序员犯了错误
(5)不要滥用断言。不要使用它来检查“你认为应该为真”的条件,只使用它来检查“一定必须为真”的条件,滥用断言可能会造成难以维护的重复逻辑
(6)如果断言所指示的约束条件不能满足,代码是否仍能正常运行?如果可以,就把断言拿掉
(7)注意断言中的重复代码,你可以大胆使用Extract Method(提炼函数)去掉那些重复代码
(8)在项目结束前以过滤程序滤掉使用断言的每一行代码(可以使用Perl之类的语言来编写这样的过滤程序)
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();
}
尾注
- 上述的总结与思考是基于对《重构—改善既有代码的设计》这本书的精读与演绎
- 更多及时干货,请关注微信公众号:JAVA万维猿圈