最近在读重构,第一章的案例感觉比较经典,提取出来便于经常查阅
案例: 影片出租店,计算顾客消费金额并打印详单,操作员录入顾客租的影片、租期,程序根据影片类型 计算出费用。影片分三类:普通片、儿童片和新片,除了费用 还有积分计算
案例中有三个对象:影片(Movie),租赁(Rental) 和 顾客(Customer),初始uml和代码如下:
class Movie {
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
public static final int CHILDREN = 2;
private String title;
private int priceCode;
public Movie(String title, int priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int priceCode) {
this.priceCode = priceCode;
}
}
class Rental {
private Movie movie;
private int dayRented;
public Rental(Movie movie, int dayRented) {
this.movie = movie;
this.dayRented = dayRented;
}
public Movie getMovie() {
return movie;
}
public void setMovie(Movie movie) {
this.movie = movie;
}
public int getDayRented() {
return dayRented;
}
public void setDayRented(int dayRented) {
this.dayRented = dayRented;
}
}
class Customer {
private String name;
private Vector rentals = new Vector();
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Vector getRentals() {
return rentals;
}
public void addRentals(Rental rental) {
this.rentals.add(rental);
}
public String statement() {
double totalAmount = 0;
int frequentRenterPointers = 0;
Enumeration rentalEnumeration = rentals.elements();
String result = "Rental Records for " + getName() + "\n";
while (rentalEnumeration.hasMoreElements()) {
double thisAmount = 0;
Rental each = rentalEnumeration.nextElement();
// determine amounts for each line
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount += 2;
if (each.getDayRented() > 2) {
thisAmount += (each.getDayRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
thisAmount += each.getDayRented() * 3;
break;
case Movie.CHILDREN:
thisAmount += 1.5;
if (each.getDayRented() > 3) {
thisAmount += (each.getDayRented() - 3) * 1.5;
}
break;
}
// add frequent renter points
frequentRenterPointers++;
// add bonus for two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDayRented() > 1) {
frequentRenterPointers++;
}
//show figures for this rental
result += "\t" + each.getMovie().getTitle() + "\t" + thisAmount + "\n";
totalAmount += thisAmount;
}
// add footer lines
result += "Amount owed is " + totalAmount + "\n";
result += "You earned " + frequentRenterPointers + " frequent renter points";
return result;
}
}
代码评价:设计不符合面向对象,实现起来快速随性,不方便后期扩展
如果发现自己需要为程序添加一个特性,而代码结构是你无法很方便的达成目的,那就先重构,是的特性的添加比较容易进行然后在添加特性
重构前先检查是否有一套可靠的测试机制,这些测试必须有子午检测能力
一、分解重组segment()
找出segment中逻辑泥团,运用Extract Method,提炼到一个独立函数中,提炼规则:
1. 任何一个不会被修改的变量都可以被当成传入的新参数。
2、如果只有一个变量会被修改,可以把他当做返回值。
文中提炼的是segment 中 switch部分:each为参数,thisAmount为返回值
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount += 2;
if (each.getDayRented() > 2) {
thisAmount += (each.getDayRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
thisAmount += each.getDayRented() * 3;
break;
case Movie.CHILDREN:
thisAmount += 1.5;
if (each.getDayRented() > 3) {
thisAmount += (each.getDayRented() - 3) * 1.5;
}
break;
}
提取后变量更名
方法名: amountFor(), 变量名: each->aRental, thisAmount->result,
任务一个傻瓜都能写出计算机可以理解的代码,唯有写出人容易理解的代码,才是优秀的程序员
二、搬移 "金额计算" 代码
多数情况下方法应该放在他所使用数据的归属对象内,amountFor() 在Customer中,用的是Rental中的信息,这一步运用 Move Method 将 amountFor()移至到Rental中,更名为getCharge(),测试没问题后,修改所有引用点
三、提炼"常客积分计算"代码
积分计算和影片的种类和租赁的天数有关,此处Extract Method重构手法将积分计算移至Rental中
方法名:getFrequentRenterPoints()
提炼后编译、测试,重构是小步前进,降低犯错概率
四、去除临时变量
临时变量只在所属方法中有效,它使得方法变得冗长而复杂,此处用查询方法替换 totalAmount 和 frequentRentalPoints(Replace Temp with Query)
用getTotalCharge() 取代 totalAmount;
用getFrequentRentalPoints() 取代 frequentRentalPoints
将变量提到查询方法中增加了while循环,重构时可先不考虑性能问题,优化时统一处理
五、运用多态取代与价格相关的条件逻辑
Rental 中 getCharge中的 switch用到 Movie中获取影片类型的方法,按照 (二)的规则,把getCharge()再做一次搬移,租赁天数getDaysRented()作为参数传入。
这个方法需要两项数据:租赁天数和影片类型,之所以选择迁移到Movie,是因为影片类型的变化带有不稳定倾向,为了尽量控制它造成的影响。
同样把根据影片类型变化的积分计算getFrequentRenterPoints() 也迁移到Movie中
到现在为止重构的类图:
六、终于·····我们来到了继承
Movie有三种影片类型,他们以不同的方式回答相同的问题,我们可以建立Movie的三个子类,每个都有自己的计费法。这样用多态取代Switch。
一部影片类型可能在生命周期内发生改变,一个对象却不能在生命周期内修改自己所属的类型。所以不能直接建立Movie的子类,这里可以用State模式:
首先用 Replace Type Code with State/Strategy,将与类型相关的行为搬至State模式内,然后运用Move Method 将 switch语句移到Price类。最后用Replace Conditional with Polymorphism去掉switch。
第一步针对类型代码使用 Self Encapsulate Field, 确保任何时候都通过取值函数和设置函数来访问类型代码,包括自身的构造函数。
然后新建Price类,加入一个抽象方法 abstrace int getPriceCode();创建三个子类:ChildrensPrice,NewReleasePrice,RegularPrice,并在所有子类加上具体的的方法。
最后修改priceCode的get和set方法;通过Price.getPriceCode()方法获取,设置时根据参数判断类型创建具体Price子类对象。
类创建完毕,把跟priceCode相关的方法迁移至 Price类
getCharge() 对应每一种priceCode的取值都不一样,所有在父类Price中创建 abstract double getCharge(int daysRented);在子类中实现getCharge();
getFrequentRenterPoints(),只对应新片有特殊处理,其他两种是一样的,所以在父类中留下一个已经定义的方法,即默认行为,新片Price类中增加一个覆写方法
引入state模式,去掉了原getCharge()和getFrequentRenterPoints()方法中priceCode的判断,使得修改任何和价格相关行为都会比较方便。