所谓重构(Refactoring)就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
定义很明确,清楚,但是怎么证明重构真的改善了代码的质量,性能?怎么证明重构提高了软件的扩展性合并可维护性呢? 换句话说,怎么证明我们花在重构上的时间真的产生了价值?
在软件代码的开发中,坏味道是公认的不好的代码,而设计模式,Clean Code是公认的好的代码,因此,如果能够把有坏味道的代码重构为使用模式的代码,或者符合Clean Code标准的代码, 那么就可以认为重构改善了代码的质量,提高了代码的可扩展性和可维护性,这个从不好的代码到好的代码就是重构产生的价值。
从上面的论述中,可以看出,任何重构一定是基于代码坏味道的,因此,重构的基础是对代码坏味道的识别能力,任何不是针对代码坏味道的重构都是耍流氓。
在做重构时,通常会涉及到3种类型的重构:
一是某个类内部的修改,包括重命名,抽取方法等等,这类重构很简单,有编程经验的程序员们基本上都会;
二是把多个类的相同代码,往上提,从而整合出一个类层次结构,以方便代码复用,提高扩展性等等。
三是把多个类中功能相似的代码提取出来,归总到一个类中。
接下来就通过一个例子来看一下这整个的重构过程:有一个停车场ParkingLot,,这是第一阶段的需求,对于ParkingLot的实现如下:
public class ParkingLot {
private Map<ParkingTicket, Car> carports = new HashMap<ParkingTicket, Car>();
private int capacity;
private int spaces;
public ParkingLot(int capacity) {
this.capacity = capacity;
this.spaces = capacity;
}
public ParkingTicket park(Car car) {
if(spaces == 0){
return null;
}
ParkingTicket ticket = new ParkingTicket();
carports.put(ticket, car);
spaces--;
return ticket;
}
public boolean isFull() {
return spaces == 0;
}
public Car unpark(ParkingTicket ticket) {
return carports.get(ticket);
}
}
首先,对于park()方法的逻辑有点多,进行第一步重构,在park()方法的整个逻辑应该是获取一个停车位,然后把车停进去。因此可以把park方法按如下修改:
public ParkingTicket park(Car car) {
return isFull() ? null : parkCarIntoCarport(car);
}
public boolean isFull() {
return spaces == 0;
}
private ParkingTicket parkCarIntoCarport(Car car) {
ParkingTicket ticket = new ParkingTicket();
carports.put(ticket, car);
spaces--;
return ticket;
}
,接下来,第二阶段的需求,有一个泊车小弟Parker,管理一批停车场ParkingLot,每次有车来,他都按照顺序一个停车场停满再停下一个停车场。实现这个Parker的代码如下:
public class Parker {
private List<ParkingLot> parkingLotList = new ArrayList<ParkingLot>();
public void addParkingLot(ParkingLot parkingLot) {
parkingLotList.add(parkingLot);
}
public ParkingTicket park(Car car) {
for (ParkingLot parkingLot : parkingLotList) {
if (!parkingLot.isFull()) {
return parkingLot.park(car);
}
}
return null;
}
public Car unpark(ParkingTicket ticket) {
for (ParkingLot parkingLot : parkingLotList) {
if (parkingLot.unpark(ticket) != null) {
return parkingLot.unpark(ticket);
}
}
return null;
}
}
第三阶段需求,有另外一个泊车小弟VacancyParker, 它也管理一批停车场,但是他放车的顺序是按照停车场空置率,哪个停车场空置率高就放哪个停车场。于是VacancyParker的代码如下:
public class VacancyParker {
private List<ParkingLot> parkingLotList = new ArrayList<ParkingLot>();
public void addParkingLot(ParkingLot parkingLot) {
parkingLotList.add(parkingLot);
}
public ParkingTicket park(Car car) {
ParkingLot choosedLot = null;
double vacancyRate = 0;
for (ParkingLot parkingLot : parkingLotList) {
if (parkingLot.vacancyRate() > vacancyRate) {
vacancyRate = parkingLot.vacancyRate();
choosedLot = parkingLot;
}
}
return choosedLot.park(car);
}
public Car unpark(ParkingTicket ticket) {
for (ParkingLot parkingLot : parkingLotList) {
if (parkingLot.unpark(ticket) != null) {
return parkingLot.unpark(ticket);
}
}
return null;
}
}
,在这个阶段,我们把VacancyParker的park()方法整理一下,如下:
public ParkingTicket park(Car car) {
ParkingLot choosedLot = choosePark();
return choosedLot == null ? null : choosedLot.park(car);
}
private ParkingLot choosePark() {
ParkingLot choosedLot = null;
double vacancyRate = 0;
for (ParkingLot parkingLot : parkingLotList) {
if (parkingLot.vacancyRate() > vacancyRate) {
vacancyRate = parkingLot.vacancyRate();
choosedLot = parkingLot;
}
}
return choosedLot;
}
整理之后,就能发现一个很明显的代码异味,就是Parker和VacancyParker的代码重复特别多,除了在选择ParkingLot的逻辑上之外,其他的代码都是一样的,因此我们可以把代码重构为如下的类层次结构:
模式就出来了。
第四阶段需求,有一个ParkerManager,它负责管理几个Parker,为了了解他管理的整个停车场的状态,需要获取其管理的所有停车场的状态报告,按如下格式:
parkerManager:
Parker1:
ParkingLot1:10/21 (注:共有21个车位,˙其中10个有车)
ParkingLot2:8/21
Parker2:
ParkingLot1:10/21
ParkingLot2:8/21
..........
实现这个需求时,第一阶段的代码如下:
public class ParkerManager {
private List<Parker> parkers = new ArrayList<Parker>();
public void addParker(Parker parker) {
parkers.add(parker);
}
public String report() {
StringBuilder stringBuilder = new StringBuilder().append("manager:\n");
for (Parker parker : parkers) {
stringBuilder.append(parker.report());
}
return stringBuilder.toString();
}
}
Parker中的关于report()方法的代码如下:
public String report() {
StringBuilder stringBuilder = new StringBuilder().append(TWO_SPACE_INDENT).append("parker:\n");
for (ParkingLot parkingLot : parkingLotList) {
stringBuilder.append(parkingLot.info());
}
return stringBuilder.toString();
}
ParkingLot中关于report的方法如下:
public String info() {
StringBuilder stringBuilder = new StringBuilder()
.append(FOUR_SPACE_INDENT)
.append("parkinglot:")
.append(capacity - spaces + "/" + capacity)
.append("\n");
return stringBuilder.toString();
}
在完成这个代码之后,有一个不易被发现的Bad Smell,当我们需要修改report的方式的时候,我们需要遍历整个代码结构树上的类,并做出修改。这种情况下,有一个通用的设计模式可以解决这类问题,这就是Visitor模式。即为类层次结构中全部,或部分成员添加一个report方法,该report方法接受一个Report类型的参数(根据需求,可使用不同的实现类),然后,把该成员的report功能委托给Report去做。经过提取参数,pull members up等重构手法对代码拾掇以后,类的层次结构变为:
如果,还有新的需求,我们可以继续通过如上的方法,明确需求-》查找合适的设计模式 -》重构出这个设计模式,保持一个干净的代码环境。
总结:
1、简单设计,以最快的速度实现当前需求
2、TDD保证重构信心
3、模式是重构出来
4、保持对代码异味的高度敏感性
本文中的示例代码地址:https://github.com/xianlinbox/ChessDemo/tree/master/src/main/java/parkinglot