书接上回,我们继续聊如何用重构指导Clean Code。
在Clean Code的3.4节中有这样一段代码(代码清单3-4)。(第3章主要讲的是函数,而3.4节讨论的是switch
语句。)
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
Bob大叔细数了这段代码的几处问题:
太长,当出现新的
Employee
类型时,还会变得更长明显做了不止一件事
违反了单一权责原则,因为有好几个修改它的理由
违反了开放闭合原则,因为每当添加新类型时,就必须修改之
最麻烦的是到处皆有类似结构的函数
其中1和4说的是一回事儿,每当新增加一种类型的Employee
,就必须在这个switch
里添加新的case
。2和3说的是一回事儿,这个方法为三种类型的Employee
计算费用,任何一种类型的计算逻辑发生变化,都要修改这个计算方法(或者对应的计算方法)。
对于第5点,Bob大叔的意思是说,calculatePay(Employee e)
这个方法所在的类中还会有类似isPayDay(Employee e, Date date)
或deliverPay(Employee e, Money pay)
这样的方法,由于Employee
具有不同的类型(type
),那么这些方法中肯定也会出现类似的switch
语句。
讲到这里还是非常清晰的,然而Bob大叔接下来又开始魔幻操作,直接给出了解决方案:
将switch语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为
Employee
的派生物创建适当的实体,而不同的函数,如calculatePay
、isPayday
和deliverPay
等,则藉由Employee
接口多态地接受派遣。
哪里来的抽象工厂?Employee
怎么就有了派生类?再一看代码
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r) ;
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
妙啊,6啊,Bob大叔简直是神啊!
然而,面对同样的问题代码,我们能做出这样的修改吗?我们能构造出Employee
的继承体系吗?能抽象出EmployeeFactory
吗?反正对我来说,仅看Clean Code是做不到的。Bob大叔的技炫得神乎其神,但无奈只有结果,没有过程,让初学者不明所以。所以,这个时候就要祭出神器《重构》了。
《重构》要求我们在重构代码之前,先识别坏味道。然后针对你想去消除的那个坏味道,按照相应的重构手法,一步一步让这个坏味道消失。我曾经在真实的遗留系统中演练过“坏味道驱动的重构“(SDR,Smell Driven Refactoring,这是我发明的一个词儿),效果还是不错的。
还是上面有问题的代码,我们看看按照《重构》应该怎么重构:
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
这里的坏味道很多,最一目了然的可能就是”Switch Statements(Switch惊悚现身)“,当然还有Bob大叔指出的违反SRP和OCP,但在坏味道中,我们管它叫做”Divergent Change(发散式变化)“。然而要消除这两个坏味道都不是那么容易,我只想从最简单的开始。什么是最简单的呢?
首先calculatePay
方法有一个Employee
类型的参数,除此之外,它没有使用该方法所在类的任何成员。同理可以推断该方法所使用的其他三个方法应该也只是使用Employee
,不会使用该方法所在类的成员。这时,”Feature Envy(依恋情结)“的坏味道就显现出来了。
有一种经典气味是:函数对某个类的兴趣高过对自己所处类的兴趣。这种孺慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。
对于该方法,岂止是半打,简直就是全部。老马不但指出了问题,还给出了方案:
疗法显而易见:把这个函数移至另一个地点。你应该使用"Move Method(搬移函数)把它移到它该去的地方”
没什么好说的,挪呗。将calculateCommisstionPay
、calculateHourlyPay
、calculateSalariedPay
方法,以及它自己全部挪到Employee
中(常用IDE都支持重构,这里不再赘述):
public Money calculatePay() throws InvalidEmployeeType {
switch (this.type) {
case COMMISSIONED:
return this.calculateCommissionedPay();
case HOURLY:
return this.calculateHourlyPay();
case SALARIED:
return this.calculateSalariedPay();
default:
throw new InvalidEmployeeType(this.type);
}
}
样一来,该方法只使用Employee
自己的数据,”依恋情结“的坏味道不见了。
是祸躲不过,Switch这个坏味道早晚得面对。
从本质上说,switch语句的问题在于重复。你常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们。
这也正是Bob大叔所说的”最麻烦的是到处皆有类似结构的函数“。好在老马一如既往给出了解决方案:
面向对象中的多态概念可为此带来优雅的解决办法。
switch语句常常根据类型码进行选择,你要的是“与该类型码相关的函数或类”,……使用Replace Type Code with Subclasses 或Replace Type Code with State/Strategy。一旦这样完成继承结构之后,你就可以运用Replace Conditional with Polymorphism了。
由于我们已经通过消除”依恋情结“将方法移动到了Employee
中,所以接下来只需要从类型码入手就好了。这里我们选择使用Replace Type Code with Subclass(以子类取代类型码)这个重构手法。打开《重构》相关章节一看,给出的例子简直跟我们这里的一模一样,两位大师是商量好的吧?
abstract int getType();
abstract Money calculatePay();
static Employee create(int type) {
switch (type) {
case COMMISSIONED:
return new CommissionedEmployee();
case HOURLY:
return new HourlyEmployee();
case SALARIED:
return new SalariedEmployee();
default:
throw new InvalidEmployeeType(type);
}
}
怎么样?是不是与Bob大叔的幻改异曲同工?(EmployeeRecord
之类的属于细枝末节,不再赘述。)
除此之外,老马还给出了消除类型码的其他方案(Replace Type Code with State/Strategy),以及彻底消除switch的办法(Replace Conditional with Polymorphism)。
这样细致入微的教导,是不是比直接告诉你答案要实用得多?
事实上对于上例这种有很多坏味道的代码,你完全可以按个人喜好来选择不同的重构顺序,得到的结果可能千差万别,也可能殊途同归,但肯定都算是不错的设计。
这就是代码的魅力。