重构,第一个案例(三)
知识点的梳理:
- 如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性;
-
重构的节奏:测试,小修改,测试,小修改,测试,小修改...重要的事情说三遍;
-
案例描述
-
此实例是一个影片出租店使用的程序,计算每一位顾客的消费金额并打印详单;操作者告诉程序:顾客租了哪些影片,租期多长,程序便根据租赁时间和影片类型算出费用。影片分为三类:普通片,儿童片和新片。除了计算费用,还要位常客计算积分,积分会根据租片种类是否位新片而有不同;
-
此实例提到的类:
-
- Movie(影片)与Rental(租赁)
-
-
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){ _title =title; _priceCode = priceCode; } public int getPriceCode(){ return _priceCode; } public void setPriceCode(int arg){ _priceCode =arg; } public String getTitle(){ return _title; } } |
public class Rental { private Movie _movie; private int _daysRented; public Rental(Movie movie , int daysRented){ _movie = movie; _daysRented = daysRented; } public int getDaysRented(){ return _daysRented; } public Movie getMovie(){ return _movie; } }
|
- Customer(顾客)
import java.util.Enumeration; import java.util.Vector;
public class Customer { private String _name; private Vector _rentals = new Vector(); public Customer(String name){ _name = name; } public void addRental(Rental arg){ _rentals.addElement(arg); } public String getName(){ return _name; } public String statement(){ double totalAmount = 0; int frequentRenterPoints = 0; Enumeration rentals = _rentals.elements(); String result = "Rental Record for " + getName() + "\n"; while(rentals.hasMoreElements()){ double thisAmount = 0; Rental each = (Rental)rentals.nextElement(); 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 += 1.5; if(each.getDaysRented() > 3){ thisAmount += (each.getDaysRented() - 3) * 1.5; } 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 null; } } |
Customer提供了一个用于生成详单的函数,下图是这个函数带来的交互过程:
|
-
该段代码的问题:
- 不符合面向对象,代码复用率低;
- Customer中的statement()函数实在是~~~你懂的~
-
重构第一步
-
为即将修改的代码建立一组可靠的测试环境;
- 重构时,要依赖测试,让它告诉我们是否引入了BUG;
-
分解并重组statement():
- 代码块愈小,愈容易管理;
-
statement的第一个问题是swtich()语句,将它单独作为一个函数较好;
-
重构时,当我们提炼一个函数,我们必须知道可能出现的错误。所以在提炼之前,要先想出安全做法,避免引入BUG;
- 首先要找出这段函数中的局部变量和参数,each和thisAmount;前者会被修改,后者不会;任何不会被修改的变量都可以当成参数传入新的函数;
- 如果只有变量会被修改,可以把它当作返回值;
- thisAmount是个临时变量,其值在每次循环起始处被设为0,并且在switch语句之前不会被改变,可以把新函数的返回值赋给它;
-
- 代码重构:
-
import java.util.Enumeration; import java.util.Vector;
public class Customer { private String _name; private Vector _rentals = new Vector(); public Customer(String name){ _name = name; } public void addRental(Rental arg){ _rentals.addElement(arg); } public String getName(){ return _name; } public String statement(){ double totalAmount = 0; int frequentRenterPoints = 0; Enumeration rentals = _rentals.elements(); String result = "Rental Record for " + getName() + "\n"; while(rentals.hasMoreElements()){ double thisAmount = 0; Rental each = (Rental)rentals.nextElement(); thisAmount = amountFor(each); 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; } private double amountFor(Rental aRental) { 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 += 1.5; if(aRental.getDaysRented() > 3){ result += (aRental.getDaysRented() - 3) * 1.5; } break; case Movie.CHILDRENS: result +=1.5; if(aRental.getDaysRented() >3){ result += (aRental.getDaysRented() -3) * 1.5; } break; } return result; } } |
仔细观察amountFor()函数,你会发现这个类使用了Rental类的信息,却没有它的所在类Customer类的信息; 大多数情况下,函数应该放在它所使用的数据的所属对象内,所以amountFor()应该移到Rental类去; public class Rental { private Movie _movie; private int _daysRented; public Rental(Movie movie , int daysRented){ _movie = movie; _daysRented = daysRented; } public int getDaysRented(){ return _daysRented; } public Movie getMovie(){ return _movie; } double getCharge() { 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 += 1.5; if(getDaysRented() > 3){ result += (getDaysRented() - 3) * 1.5; } break; case Movie.CHILDRENS: result +=1.5; if(getDaysRented() >3){ result += (getDaysRented() -3) * 1.5; } break; } return result; } } //为了适应新类,要去掉参数,同时变更函数名称 |
改变Customer.amountFor()函数内容。使它委托调用新函数即可: public class Customer { //。。。 private double amountFor(Rental aRental){ return aRental.getCharge(); } } |
实际上,由于重构后的函数,只有一条语句,那么我们完全可以废弃它,直接将此语句挪出来: private String _name; private Vector _rentals = new Vector(); public Customer(String name){ _name = name; } public void addRental(Rental arg){ _rentals.addElement(arg); } public String getName(){ return _name; } public String statement(){ double totalAmount = 0; int frequentRenterPoints = 0; Enumeration rentals = _rentals.elements(); String result = "Rental Record for " + getName() + "\n"; while(rentals.hasMoreElements()){ double thisAmount = 0; Rental each = (Rental)rentals.nextElement(); thisAmount = each.getCharge(); 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; } } |
-
重构后,各个类的状态:
-
- 现在看来局部变量thisAmount有些多余了。它用来接受each.getCharge()的执行结果,然后就不再有任何改变。
public class Customer { private String _name; private Vector _rentals = new Vector(); public Customer(String name){ _name = name; } public void addRental(Rental arg){ _rentals.addElement(arg); } public String getName(){ return _name; } public String statement(){ double totalAmount = 0; int frequentRenterPoints = 0; Enumeration rentals = _rentals.elements(); String result = "Rental Record for " + getName() + "\n"; while(rentals.hasMoreElements()){ Rental each = (Rental)rentals.nextElement(); frequentRenterPoints ++; if((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1){ frequentRenterPoints ++; } 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; } } |
临时变量有时会引发许多问题,会导致大量参数传来传去,所以在可以省略的时候,要尽量省略; |
-
提炼"常客积分计算"代码
- 可以把积分计算责任放在Rental类身上。
public class Customer { //... public String statement(){ double totalAmount = 0; int frequentRenterPoints = 0; Enumeration rentals = _rentals.elements(); String result = "Rental Record for " + getName() + "\n"; while(rentals.hasMoreElements()){ Rental each = (Rental)rentals.nextElement(); 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; } } |
public class Rental { //。。。 int getFrequentRenterPoints() { if((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1){ return 2; } else{ return 1; } } }
|
- 重构前后的UML对比,左边为原始图,右边为重构图
|
|
-
去除临时变量
- 在Customer中,有totalAmount和frequentRenterPoints两个临时变量。它们都是用来从Customer对象相关的Rental对象中获得某个总量。现在利用查询函数(queryMethod)来取代totalAmount和frequentRentalPoints这两个临时变量。利用Customer中的getTotalCharge()取代totalAmount:
public class Customer { //。。。 public String statement(){ int frequentRenterPoints = 0; Enumeration rentals = _rentals.elements(); String result = "Rental Record for " + getName() + "\n"; while(rentals.hasMoreElements()){ Rental each = (Rental)rentals.nextElement(); frequentRenterPoints += each.getFrequentRenterPoints(); result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n"; } result += " Amount owed is " + String.valueOf(getTotalCharge()) + "\n"; result += " You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points "; return result; } private double getTotalCharge(){ double result = 0; Enumeration rentals = _rentals.elements(); while(rentals.hasMoreElements()){ Rental each = (Rental)rentals.nextElement(); result += each.getCharge(); } return result; } } |
||
totalAmount在循环内部被复制,我们要把循环复制到查询函数中: //... public String statement(){ int frequentRenterPoints = 0; Enumeration rentals = _rentals.elements(); String result = "Rental Record for " + getName() + "\n"; while(rentals.hasMoreElements()){ Rental each = (Rental)rentals.nextElement(); frequentRenterPoints += each.getFrequentRenterPoints(); result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n"; } result += " Amount owed is " + String.valueOf(getTotalCharge()) + "\n"; result += " You earned " + String.valueOf(getTotaIFrequentRenterPoints()) + " frequent renter points "; return result; } private int getTotaIFrequentRenterPoints(){ int result = 0; Enumeration rentals = _rentals.elements(); while(rentals.hasMoreElements()){ Rental each = (Rental)rentals.nextElement(); result += each.getFrequentRenterPoints(); } return result; } //... } |
||
此步骤后的UML交互图:
|
||
在Customer类中加入新的功能代码: public String htmlStatement(){ Enumeration rentals = _rentals.elements(); String result = "<H1>Rentals for<EN>" + getName() + "</EN></H1><P>\n"; while(rentals.hasMoreElements()){ Rental each = (Rental)rentals.nextElement(); result += each.getMovie().getTitle() + ": " + String.valueOf(each.getCharge()) + "<BR>\n"; } result +="<P>You owe<EM>" + String.valueOf(getTotalCharge()) + "</EM><P>\n"; result +="On this rental you earend<EM>" + String.valueOf(getTotaIFrequentRenterPoints()) + "</EM> frequent renter points<P>"; return result; } |
-
运用多态取代与价格相关的条件逻辑
- switch:最好不要在另一个对象的属性基础上运用switch语句。如果非要使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用;在getCharge中,使用Movie中的getPriceCode方法。所以我们最好把getCharge函数移到Movie中,同时该方法又需要Rental的_daysRented数据,将租期长度作为参数传递进去。
- 计算费用时需要两项数据:租期长度和影片类型。在这里我们是将Rental中的租期长度传给了Movie,之所以这样做的原因使该实例可能发生变化的是加入新影片类型,这种变化会带来不稳定倾向。如果影片类型有所变化,我们应该尽量控制它造成的影响,所以选在Movie对象内计算费用;
public class Movie { //。。。 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 += 1.5; if(daysRented > 3){ result += (daysRented - 3) * 1.5; } break; case Movie.CHILDRENS: result +=1.5; if(daysRented >3){ result += (daysRented -3) * 1.5; } break; } return result; } } |
将计算方法放入Movie类后,然后修改Rental的getCharge(),让它使用这个新函数: private Movie _movie; private int _daysRented; public Rental(Movie movie , int daysRented){ _movie = movie; _daysRented = daysRented; } public int getDaysRented(){ return _daysRented; } public Movie getMovie(){ return _movie; } int getFrequentRenterPoints() { if((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1){ return 2; } else{ return 1; } } double getCharge(){ return _movie.getCharge(_daysRented); } } |
- 处理完成getCharge()后,Rental中的getFrequentRenterPoints()也存在这样的问题,使用了Movie中的priceCode参数,所以将这个函数搬移至Movie:
public class Rental { //... int getFrequentRenterPoints() { return _movie.getFrequentRenterPoints(_daysRented); } double getCharge(){ return _movie.getCharge(_daysRented); } } |
public class Movie { //... 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 += 1.5; if(daysRented > 3){ result += (daysRented - 3) * 1.5; } break; case Movie.CHILDRENS: result +=1.5; if(daysRented >3){ result += (daysRented -3) * 1.5; } break; } return result; } int getFrequentRenterPoints(int daysRented) { if((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1){ return 2; } else{ return 1; } } } |
-
实现继承
- 因为有数种影片类型,它们以不同的方式回答相同的问题。这很像子类的工作,建立Movie的三个子类,每个都有自己的计费法:
|
可用多态来取代Switch语句。但在这里,一部影片可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。但是,我们可以利用"状态模式"。如果这样做的话,类看起来就像下图: |
-
为了引入"状态模式",我们需要将与类型相关的行为搬移至"状态模式"内。之后将switch语句移动Price类。
- 首先,要针对类型代码使用"自封装字段",确认任何时候都通过取值函数和设置函数来访问类型代码。多数访问操作来自其他类,它们已经在使用取值函数。
看看Movie的构造函数,它仍然直接访问价格代码: public Movie(String title,int priceCode){ _title =title; _priceCode = priceCode; } |
修改它,使用set函数为它设值: public Movie(String title,int priceCode){ _title =title; setPriceCode(priceCode); } |
- 新建Price类,在其中添加一个抽象函数,提供类型相关的行为。所有的子类中也要加上对应的具体函数:
public abstract class Price { abstract int getPriceCode(); } |
public class ChildrensPrice 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; } } |
-
现在我们要在Movie类中的设值函数中,做出类型判断。修改Movie类内的"价格代号"访问函数(也就是setter和getter),让它们使用新类:
- 引入了"状态模式",就需要要使用"状态对象"。所以在Movie类中,引入Price对象,不再保存一个_priceCode变量:
- 同时修改getPriceCode(),使用抽象类的方法,做到多态;
public int getPriceCode(){ return _price.getPriceCode(); } private Price _price; public void setPriceCode(int arg){ switch(arg){ case REGULAR: _price = new RegularPrice(); break; case CHILDRENS: _price = new ChildrensPrice(); break; case NEW_RELEASE: _price = new NewReleasePrice(); break; default: throw new IllegalArgumentException("Incorrect Price Code"); } } |
- 现在要移动getCharge()。将Movie中的代码权责,移交给"状态对象"Price
public class Movie { //。。。 double getCharge(int daysRented) { return _price.getCharge(daysRented); } //... } |
public abstract class Price { abstract int getPriceCode(); 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 += 1.5; if(daysRented > 3){ result += (daysRented - 3) * 1.5; } break; case Movie.CHILDRENS: result +=1.5; if(daysRented >3){ result += (daysRented -3) * 1.5; } break; } return result; } } |
- 用多态来取代条件表达式switch.每次取出一个case分支,在相应的类建立一个覆盖函数。先从RegularPrice开始:
public class RegularPrice extends Price { @Override int getPriceCode() { return Movie.REGULAR; } double getCharge(int daysRented){ double result = 2; if(daysRented > 2){ result += (daysRented -2) * 1.5; } return result; } } |
public class ChildrensPrice extends Price { @Override int getPriceCode() { return Movie.CHILDRENS; } double getCharge(int daysRented){ double result = 1.5; if(daysRented > 3){ result += (daysRented -3) * 1.5; } return result; } } |
public class NewReleasePrice extends Price { @Override int getPriceCode() { return Movie.NEW_RELEASE; } double getCharge(int daysRented){ return daysRented * 3; } }
|
- 将case分解完成后,修改Price中的getCharge函数:
public abstract class Price { abstract int getPriceCode(); abstract double getCharge(int daysRented); } |
- 现在运用同样的手法来处理Movie中的getFrequentRenterPoints(),将权责移交给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 int _priceCode; public Movie(String title,int priceCode){ _title =title; setPriceCode(priceCode); } public int getPriceCode(){ return _price.getPriceCode(); } private Price _price; public void setPriceCode(int arg){ switch(arg){ case REGULAR: _price = new RegularPrice(); break; case CHILDRENS: _price = new ChildrensPrice(); break; case NEW_RELEASE: _price = new NewReleasePrice(); break; default: throw new IllegalArgumentException("Incorrect Price Code"); } } public String getTitle(){ return _title; }
double getCharge(int daysRented) { return _price.getCharge(daysRented); } int getFrequentRenterPoints(int daysRented) { return _price.getFrequentRenterPoints(daysRented); } } |
public abstract class Price { abstract int getPriceCode(); abstract double getCharge(int daysRented); int getFrequentRenterPoints(int daysRented) { if((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1){ return 2; } else{ return 1; } } }
|
- getFrequentRenterPoints()函数涉及的内容与子类NewReleasePrice相关,所以将该函数与NewReleasePrice类相关的部分抽离出来,单独重写放在NewReleasePrice类中。而与该类无关的函数部分还保留在Price中:
public class NewReleasePrice extends Price { @Override int getPriceCode() { return Movie.NEW_RELEASE; } double getCharge(int daysRented){ return daysRented * 3; } int getFrequentRenterPoints(int daysRented){ return (daysRented >1) ? 2:1; } } |
public abstract class Price { abstract int getPriceCode(); abstract double getCharge(int daysRented); int getFrequentRenterPoints(int daysRented) { return 1; } } |
-
引入"状态模式"的好处:
- 对修改任何与价格有关的行为,或是添加新的定价标准,或者加入其他取决于价格的行为,程序的修改会容易很多;
-
UML图:
-
-
类图:
-