目录

​前言​

​题目​

​初步解法​

​1.Movie类​

​2.Rental类​

​3.Customer类​

​4.谈谈初步解法的问题​

​重构实战​

​1. 重构第一步​

​2.分解并重组statements​

​2.1. Extract Method(提取函数)​

​2.2. Move Method(搬移函数)​

​2.3.提炼“积分计算”代码​

​2.4.去除临时变量(Replace Temp with Query以查询取代临时变量)​

​3.运用多态取代与价格相关的条件逻辑​

​3.1. Replace type code with state/strategy(以state/strategy取代类型码)​

​3.2.Move Method(搬移函数)​

​3.3. Replace Conditional with Polymorphism(以多态取代表达式)​

​总结​


前言

​程序员必懂的代码重构(理论篇)​​一文介绍了代码重构是什么、常用的重构手法和代码中的“坏味道”。But talk is cheap. Show me the code,本文将从实战的角度来谈谈代码重构,共分为:题目、初步解法、重构实战和总结四部分。

本篇blog是​​《重构,改善代码既有代码的设计》​​(密码: ab5g)一文的读书笔记,读书笔记与书一起食用效果更佳哦。欢迎点赞、收藏、评论三连~,谢谢大家。


题目

该程序为影片出租店用的程序,目的是计算每位顾客的消费金额并打印详单。你需要1.根据租赁时间和影片类型(普通片、儿童片和新片三类)计算费用;2.除了计算费用之外,需要计算积分。积分会根据是否是新片而有所不同。

初步解法

跟着笔者思路一起来看,按照功能可以拆分为Movie类、Rental类和Customer类。

1.Movie类

功能:该类主要记录类型、价格和标题等,是单纯数据类。

/**
* Movie记录类型、价格和标题等,单纯数据类。
* @author kevinhe
*/
public class Movie {
//三种片类型
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;

private String title;
private int priceCode;

public Movie(String title, int priceCode) {
this.title = title;
this.priceCode = priceCode;
}

public int getPriceCode() {
return priceCode;
}

public void setPriceCode(int priceCode) {
this.priceCode = priceCode;
}

public String getTitle() {
return title;
}
}

2.Rental类

功能:表示某位顾客租了一部影片,表示行为。

/**
* Rental表示某位顾客租了一部影片,表示行为。
* @author kevinhe
*/
class Rental {
private Movie movie;
private int daysRented;

public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}

public int getDaysRented() {
return daysRented;
}

public Movie getMovie() {
return movie;
}
}

3.Customer类

功能:表示顾客,有数据和相应的访问函数。

/**
* Customer表示顾客,有数据和相应的访问函数
*
* @author kevinhe
*/
public class Customer {
private String name;
//Vector 类实现了一个动态数组。和 ArrayList 很相似,但是两者是不同的;
//1.Vector 是同步访问的。2.Vector 包含了许多传统的方法,这些方法不属于集合框架。
//Vector 主要用在事先不知道数组的大小,或者只是需要一个可以改变大小的数组的情况。
private Vector rentals = new Vector();

public Customer(String name) {
this.name = name;
}

public void addRental(Rental rental) {
rentals.add(rental);
}

public String getName() {
return name;
}

/**
* 提供一个用于生成详单的函数
*/
public String statement() {
double totalAmount = 0;
//常客计算积分时使用
int frequentRenterPoints = 0;
Enumeration enumeration = rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (enumeration.hasMoreElements()) {
//总金额
double thisAmount = 0;
Rental each = (Rental) rentals.elements();
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount += 2;
//优惠力度
if (each.getDaysRented() > 2) {
thisAmount += (each.getDaysRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
//果然还是新书最贵啊
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.getDaysRented() > 3) {
thisAmount += (each.getDaysRented() - 3) * 1.5;
}
break;
}
frequentRenterPoints++;
//如果是新书,另算积分呢
if (each.getMovie().getPriceCode() == Movie.NEW_RELEASE && each.getDaysRented() >= 1) {
frequentRenterPoints++;
}
result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
return result;
}
}

4.谈谈初步解法的问题


  • 不符合面向对象的精神。

    • statement() 做的实在过多了,如果改成HTML格式网页详单输出;或者计费标准发生变化,大量重复statement的代码非常恶心;
    • 假设用户希望改变影片分类规则,进而影响到积分的计算的方式,那么HTML网页显示和现在的显示方式会很难保持修改一致性,很容易修改出bug。

  • 建议:“如果它没坏,就不要动它”可能是不可取的,如果你需要为程序添加一个特性,发现代码结构无法让你很方便达到目的,那么是时候重构了。

重构实战

1. 重构第一步

   重构之前,检查是否有一套可靠测试机制,这些测试必须要足够自动化。


  • 为即将修改的代码建立一组可靠的测试环境,即需要可靠的测试。由于statement是输出是字符串,那么假设有顾客,各租不同影片,产生报表字符串,看新字符串和符合预期的字符串是否一致。
  • 测试需足够自动化,若新/参考字符串一致,那么OK,如果不一致,则显示问题字符串行号,测试能够自我校验,否则大把时间的对比无疑会降低开发速度。 

2.分解并重组statements

  长长的函数需要大卸八块,代码块越小,代码的移动和处理也就越轻松。将较小代码块移至更合适的类,降低代码重复使新函数更容易撰写。

2.1. Extract Method(提取函数)

1.找出逻辑泥团并运用Extract Method方法。本例中的switch语句需提炼至独立函数。找出函数内局部变量和参数。each(未被修改,可以当成参数传入新的函数)和thisAmount(会被修改,格外小心,如果只有一个变量修改,可以将其作为返回值)。那么将新函数返回值返回给thisAmount是可行的。

2.重构技术以微小的步伐修改程序,如果你犯下错误,很容易也能发现它。好的代码应该清楚表达自己的功能,变量名称是代码清晰的关键,唯有写出人类容易理解的代码,才是好的程序员。

/**
* 提供一个用于生成详单的函数
*/
public String statement() {
....
while (enumeration.hasMoreElements()) {
//总金额
double thisAmount = 0;
Rental each = (Rental) rentals.elements();
thisAmount = amountFor(each);
....
}
....
}


/**
* 金额计算
* @param aRental
* @return
*/
private double amountFor(Rental aRental) {
//注意double、int类型之间的转换。
double result = 0;
switch (aRental.getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
//优惠力度
if (aRental.getDaysRented() > 2) {
result += (aRental.getDaysRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
//果然还是新书最贵啊
result += aRental.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (aRental.getDaysRented() > 3) {
result += (aRental.getDaysRented() - 3) * 1.5;
}
break;
}
return result;
}

2.2. Move Method(搬移函数)

1.观察amountFor函数,使用了Rental类的信息却没有使用来自Customer类的信息,函数是应该放在它所使用的数据的对象内的,所以amountFor应该要放到Rental类而非Customer类,调整代码以使用新类。

2.本例较为简单,只有一个地方使用了新函数,通常来说,你得在可能运用该函数的所有类中查一遍。此时customer类中使用each.getCharge()替代了amountFor(each)方法。此时发现thisAmount变得多余了。使用Replace Temp with Query(以查询取代临时变量)将thisAmount去掉。

补充知识点:尽量去除一部分不必要的临时变量,临时变量会导致大量参数传来传去,长函数容易跟丢,引发bug。

class Rental {
....
/**
* 金额计算
* @return
*/
public double getCharge() {
//注意double、int类型之间的转换。
double result = 0;
switch (getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
//优惠力度
if (getDaysRented() > 2) {
result += (getDaysRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
//果然还是新书最贵啊
result += getDaysRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (getDaysRented() > 3) {
result += (getDaysRented() - 3) * 1.5;
}
break;
}
return result;
}
}
public class Customer {
....
/**
* 提供一个用于生成详单的函数
*/
public String statement() {
....
while (enumeration.hasMoreElements()) {
...
result += "\t" + each.getMovie().getTitle() + "\t"
+ String.valueOf(each.getCharge()) + "\n";
totalAmount += each.getCharge();
}
...
}
}

2.3.提炼“积分计算”代码

积分计算因影片种类而有所不同,针对“积分计算”代码运用Extract Method重构手法。局部变量each,另一个临时变量是frequentRenterPoints(这个参数在使用之前已初始化,但提炼出的函数并未读取该值,因此无需传入,只需作为新函数的返回值累加上去即可)。

class Rental {
...
/**
* 计算常客积分
* @return
*/
public int getFrequentRenterPoints() {
//如果是新书,另算积分呢
if (getMovie().getPriceCode() == Movie.NEW_RELEASE && getDaysRented() >= 1) {
return 2;
} else {
return 1;
}
}
}
public class Customer {
....
/**
* 提供一个用于生成详单的函数
*/
public String statement() {
double totalAmount = 0;
//常客计算积分时使用
int frequentRenterPoints = 0;
Enumeration enumeration = rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (enumeration.hasMoreElements()) {
Rental each = (Rental) rentals.elements();
//计算常客积分
frequentRenterPoints += each.getFrequentRenterPoints();
result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
totalAmount += each.getCharge();
}
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
return result;
}
}

2.4.去除临时变量(Replace Temp with Query以查询取代临时变量)

临时变量会造成冗长复杂的函数,使用Replace Temp with Query(以查询取代临时变量)方法,以查询函数替代totalAmount和frequentRentalPoints临时变量。任何函数均可调用,促成干净设计、减少冗长函数。

public class Customer {
...
/**
* 提供一个用于生成详单的函数
*/
public String statement() {
//常客计算积分时使用
Enumeration enumeration = rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (enumeration.hasMoreElements()) {
Rental each = (Rental) rentals.elements();
result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
}
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points";
return result;
}

/**
* 获取总积分
* @return
*/
private double getTotalFrequentRenterPoints() {
int result = 0;
Enumeration enumeration = rentals.elements();
while (enumeration.hasMoreElements()) {
Rental each = (Rental) rentals.elements();
result += each.getFrequentRenterPoints();
}
return result;
}

/**
* 获取总金额
* @return
*/
private double getTotalCharge() {
double result = 0;
Enumeration enumeration = rentals.elements();
while (enumeration.hasMoreElements()) {
Rental each = (Rental) rentals.elements();
result += each.getCharge();
}
return result;
}
}

重构带来了性能问题,原本while执行一次,但新版本执行三次,降低了性能。重构时可不必担心这些,优化时你需要考虑。现在Customer类的代码可以调用这些查询函数了。如果没有查询函数,你必须得看懂Rental类,并自行循环。程序编写和维护难度大大增加。这时再编写html-statement就简单一些了。

/**
* 生成HTML详单的函数
* @return
*/
public String htmlStatement() {
//常客计算积分时使用
Enumeration enumeration = rentals.elements();
String result = "HTML:Rental Record for " + getName() + "\n";
while (enumeration.hasMoreElements()) {
Rental each = (Rental) rentals.elements();
result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
}
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "On this rental You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points";
return result;
}

3.运用多态取代与价格相关的条件逻辑

用户准备修改影片分类规则。费用计算和常客积分计算也会因此而发生改变。首当其冲的就是Rental类getCharge中的switch...case...语句,除非迫不得已,switch..case..应当作用于自己的数据上,而非别人的数据上(这样会有风险)。getCharge移到Movie类中去会更好,传入的是租期长度而非影片类型,因为系统可能会加入新影片类型,不稳定,因此在Movie对象内计算费用。同样的手法处理常客积分函数。

public class Movie {
...
/**
* 根据影片类型获取费用
* @param daysRented
* @return
*/
public double getCharge(int daysRented) {
double result = 0;
switch (getPriceCode()) {
case Movie.REGULAR:
result += 2;
//优惠力度
if (daysRented > 2) {
result += (daysRented - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
//果然还是新书最贵啊
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3) {
result += (daysRented - 3) * 1.5;
}
break;
}
return result;
}

public int getFrequentRenterPoints(int daysRented) {
//如果是新书,另算积分呢
if (getPriceCode() == Movie.NEW_RELEASE && daysRented >= 1) {
return 2;
} else {
return 1;
}
}
}
class Rental {
private Movie movie;
private int daysRented;
...
/**
* 金额计算
* @return
*/
public double getCharge() {
return movie.getCharge(daysRented);
}

/**
* 计算常客积分
* @return
*/
public int getFrequentRenterPoints() {
return movie.getFrequentRenterPoints(daysRented);
}
}

多态取代switch语句,多态设计时不要直接继承Movie,而是通过Price间接去处理,一部影片可以在生命周期内修改自己的分类,但一个对象却不能再生命周期内修改自己所属的类,使用state模式叭。为了引入State模式重构,我们首先使用Replace type code with state/strategy(以state/strategy取代类型码)将与类型相关的行为搬移至state模式中,运用Move Method(搬移函数)方法将switch语句移动至price类中,最后运用Replace Conditional with Polymorphism(以多态取代表达式)去掉switch语句。

3.1. Replace type code with state/strategy(以state/strategy取代类型码)

针对类型码使用Self Encapsulate Field(自封装字段),确保任何时候都通过取/设值函数来访问类型代码,构造函数依旧可以直接访问价格代码。

新建Price类,并提供类型相关的行为,为此,加入抽象函数,并在所有子类中加上对应的具体操作。

public class Movie {
//三种片类型
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;

private String title;
private Price price;

public Movie(String title, int priceCode) {
this.title = title;
setPriceCode(priceCode);
}

public int getPriceCode() {
return price.getPriceCode();
}

public void setPriceCode(int arg) {
switch (arg) {
case Movie.REGULAR:
price = new RegularPrice();
break;
case Movie.NEW_RELEASE:
price = new NewReleasePrice();
break;
case Movie.CHILDRENS:
price = new ChildrenPrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}
....
}
}
abstract class Price {
abstract int getPriceCode();
}
public class ChildrenPrice extends Price{
@Override
int getPriceCode() {
return Movie.CHILDRENS;
}
}
public class NewReleasePrice extends Price {
@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
}
public class RegularPrice extends Price {
@Override
int getPriceCode() {
return Movie.REGULAR;
}
}

3.2.Move Method(搬移函数)

将Movie中的getCharge方法下沉至Price方法中。 

abstract class Price {
abstract int getPriceCode();
/**
* 根据影片类型获取费用
* @param daysRented
* @return
*/
public double getCharge(int daysRented) {
double result = 0;
switch (getPriceCode()) {
case Movie.REGULAR:
result += 2;
//优惠力度
if (daysRented > 2) {
result += (daysRented - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
//果然还是新书最贵啊
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3) {
result += (daysRented - 3) * 1.5;
}
break;
}
return result;
}
}
public class Movie {
private Price price;
...

public int getPriceCode() {
return price.getPriceCode();
}
....
}

3.3. Replace Conditional with Polymorphism(以多态取代表达式)

一次取出getPriceCode的一个case分支,在对应的类建立覆盖函数。同样的方法处理getFrequentRenterPoints方法。

/**
* 新建Price类,并提供类型相关的行为,为此,加入抽象函数,并在所有子类中加上对应的具体操作。
*/
abstract class Price {
/**
* 获取影片类型code码
* @return
*/
abstract int getPriceCode();
/**
* 根据影片类型获取费用
* @param daysRented
* @return
*/
abstract double getCharge(int daysRented);

/**
* 如果是新书,采用复写的方法,在超类中留下一个已定义的函数,使之成为一种默认行为。
* @param daysRented
* @return
*/
int getFrequentRenterPoints(int daysRented) {
return 1;
}
}
public class RegularPrice extends Price {
@Override
int getPriceCode() {
return Movie.REGULAR;
}

@Override
public double getCharge(int daysRented) {
double result = 2;
//优惠力度
if (daysRented > 2) {
result += (daysRented - 2) * 1.5;
}
return result;
}
}
public class NewReleasePrice extends Price {
@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}

@Override
public double getCharge(int daysRented) {
//果然还是新书最贵啊
return daysRented * 3;
}

@Override
public int getFrequentRenterPoints(int daysRented) {
return (daysRented > 1) ? 2 : 1;
}
}
public class ChildrenPrice extends Price{
@Override
int getPriceCode() {
return Movie.CHILDRENS;
}

@Override
public double getCharge(int daysRented) {
double result = 1.5;
if (daysRented > 3) {
result += (daysRented - 3) * 1.5;
}
return result;
}
}

 引入State设计模式很值,修改影片分类结构/改变费用计价规则/改变积分规则都会容易很多了。

程序员必学的代码重构(实战篇)_ide

总结

代码重构就到聊到这里啦,送给大家两个小建议:1.读完笔记之后,在项目中实战吧。写出优雅、易维护、类责任更明确的代码;2.把握重构的节奏,小步慢跑,即:测试、小修改、测试、小修改..这种节奏使得重构快速且安全。欢迎互相交流~