针对庞大的类通常使用两个主要的重构方法:新生类和新生方法,将不同职责的类或方法单独剥离出来。
单一职责原则:每个类应该仅承担一个职责,它在系统中的意图应当是单一的,且修改它的原因应该只有一个。
问题代码
RuleParser是一个不大的类,但是确承担多个职责:解析;表达式求值;项的字元化;变量管理。重新分解后的职责关系图如下:
识别职责的方法
方法分组
寻找相似的方法名,将一个类上的所有方法都列出来(以及它们的访问权限),找出那些看起来是一伙的方法。
方法分组还是一个极佳的团队练习:在一块黑板上,列出你们的米格主要类里面的所有方法名。团队成员可以在上面做标记,指出不同方法分组方式。整个团队共同筛选出比较好的分组方式并决定代码的架构。
观察隐藏方法
注意哪些私有或受保护的方法:大量的私有或受保护方法往往意味着一个类内部有另一个类急迫地想要独立出来。
如果你急迫需要测试一个私有方法,那么这个方法就不应该是私有的;如果将它改为公有方法会带来麻烦,那么可能因为它本就属于另一个独立的职责。在RuleParser类就是一个很好的例子,它有两个公有方法:evaluate和addVariable。除此之外都是私有的。假设nextTerm和hasMoreTerm作为RuleParser类的公有函数,看起来会比较奇怪。
提取可以更改的决定
寻找代码中的决定,寻找代码中已经作出的决定(比例如代码采用硬编码与数据库交互、与另一组对象交互等等),将他们提取成为一个可以在高层可以反映意图的方法。
有些函数可能承担了比较多的职责,光从方面名上也难以看出他承担了多少职责。对于这些方法首先需要进行方法提取。提取方法的要点就是寻找代码中的决定。代码调用了某个特定API中的方法吗?代码是否假定它将总是访问同一个数据库?将这些过程提取出来,能在高层面反映你的意图的方法。完成这些提取之后解开实现细节的依赖,此后更容易分组函数或分解类了。
寻找内部关系
寻找成员变量和成员函数之间的关系,“这个变量只被这些方法使用吗?”
一般情况下一个类中并不是所有成员函数都访问所有成员变量,而总是某几个成员函数访问某几个成员变量。寻找方法抱团的现象就是针对类的内部关系画一张草图:特征草图。通过特征草图展示一个类里面每个方法使用那些成员变量以及其他方法。
class Reservation
{
private int duratioin;
private int dailyRate;
private Date date;
private Customer customer;
private List fees = new ArraryList();
public Reservation(Customer customer, int duration, int dailyRate, Date date){
this.customer = customer;
this.duration = duration;
this.dailyRate = dailyRate;
this.date = date;
}
public void extend(int additionalDays){
duration += additionalDays;
}
public void extendForWeek(){
int weekRemainder = RentalCalendar.weekRemainderFor(date);
final int DAYS_PER_WEEK = 7;
extend(weekRemainder);
dailyRate = RateCalculator.computeWeekly(customer.getRateCode())
/DAYS_PER_WEEK;
}
public void addFee(FeeRider rider){
fees.add(rider);
}
int getAdditionalFees(){
init total = 0;
for (Iterator it = fees.iterator(); it.hasNext();){
total += ((FeeRider)(it.next())).getAmount();
}
return total;
}
int getPrincipalFee(){
return dailyRate * RateCalculator.rateBase(customer) * duration;
}
pulbic int getTotalFee(){
return getPrincipalFee() + getAdditionalFees();
}
}
- 为每个成员变量换一个圈;
- 然后观察每个成员方法,也给每个成员方法画一个圈。
- 在任一个成员方法与该方法用到的任何成员变量或成员方法之间画一个带箭头的线。
绘制影响结构图时不考虑构造函数
在个影响结构图中可以看到分为两个部分:左边和右边。剥离左边到一个新的类之前需要明确:
- 这个新类是否具有良好的清晰的职责,能够替他想出一个名字来?左边的方法似乎使用Reservation不错,但是这是原来类的名字。
换一个思路将右下方的部分剥离出一个新类似乎更好,我们可以把新类叫做FeeCalculator。
在新类和旧类之间有一个调用过程,可以先调用getPrincipalFee,然后将值传递给FeeCalculator对象。最终的重构后的代码为:
public class Reservation
{
private int duratioin;
private int dailyRate;
private Date date;
private Customer customer;
private FeeCalculator calculator = new FeeCalculator();
public Reservation(Customer customer, int duration, int dailyRate, Date date){
this.customer = customer;
this.duration = duration;
this.dailyRate = dailyRate;
this.date = date;
}
public void extend(int additionalDays){
duration += additionalDays;
}
public void extendForWeek(){
int weekRemainder = RentalCalendar.weekRemainderFor(date);
final int DAYS_PER_WEEK = 7;
extend(weekRemainder);
dailyRate = RateCalculator.computeWeekly(customer.getRateCode())
/DAYS_PER_WEEK;
}
public void addFee(FeeRider fee){
calculator.addFee(fee);
}
int getPrincipalFee(){
return dailyRate * RateCalculator.rateBase(customer) * duration;
}
pulbic int getTotalFee(){
int baseFee = getPrincipalFee();
return calculator.getTotalFee(baseFee);
}
}
类图如下
寻找主要职责
尝试用一句话概括某个类的功能和职责
违反单一职责原则有两种形式:接口层面违反原则、实现层面违反原则。
- 实现层面的违反原则
如果描述一个类的职责时发现有多个事情,在这些事情中那个比其余事情都重要?如果有,那么这个职责就是该类的关键职责。其他职责或许应该被分解到其他类当中。需要将实现层面的多个职责分配到几个类中完成。 - 接口层面的违反原则
接口层面的违反原则相对要好一些,这个大类只不过是一大堆小类的前端,一个容易掌控的facade类。如果需要解决可考虑接口隔离,针对客户感兴趣的职责抽象出一个小的接口类。小的接口类承担主要职责,通过注入原大类对象实现功能。例如ScheduleJob类
为客户代码提供一个小的接口类并新建一个StandardJobController类。新的控制类使用原有的大类,承担用户职责。通过新的接口类和控制类可以一点点的对大类接口进行分解。
草稿式重构
如果实在很难看清一个类内部的职责,那么可以对它进行草稿式重构。
关注手头的工作
注意手头正在做的事情。如果发现你自己正在为某个事情提供另外一个解决方案,那么意味着存在一个应该被提取并允许替代的职责。
注意事项
分解大类的重构必定影响进度和版本质量,无论进行了多么丰富的测试保护。最佳步骤是首先先识别职责,确保团队每个人都理解职责;然后在需要时再对该类进行分解。这样做法可以把奉献降低,并允许在迭代开发过程中陆续进行