最近在看一本书,关于重构的,改善既有代码的设计,感谢作者给我们翻译了这本还不错的书。
本书很好的一点就是上来没有讲历史渊源这一类的催人入睡的课题,而是先用一个小案例来展示重构的过程和意义,这也是我看着本书没有止于前言的主要原因,看完了本案例,才会觉得代码真是一项艺术,与难度无关,更多的好像与强迫症有关似的,初期开发是成型,后期重构是雕琢,这也推翻了我以前的想法,比如注释要尽量写的多啊,重构基本需要推到重做啊,其实感觉本书更像是给代码习惯和编程风格一个规范化,以前一些模棱两可的习惯在这里会有一个比较严谨的分析。
好了,不说废话了,翠花,上案例。
实例非常简单,就是一个影片出租店用的程序,计算每一位顾客的消费金额并打印详单,操作者告诉程序:顾客租了哪些影片,租期多长,程序根据影片类型和租赁时间计算出费用,和为会员计算积分,影片类型:普通片,儿童片,和新片,积分根据影片是否为新片而有所不同。
首先,得有三个类:Movie Rental Customer
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 arg){
priceCode=arg;
}
public String getTitle(){
return title;
}
}
public class Rental {
private com.demo.zxp.demo1.Movie movie;
private int daysRented;
private Rental(Movie movie,int daysRented){
this.movie=movie;
this.daysRented=daysRented;
}
public int getDaysRented(){
return daysRented;
}
public Movie getMovie(){
return movie;
}
}
public class Customer1{
private String name;
private Vector _rentals=new Vector();
private Customer1(String name){
this.name=name;
}
public void addRental(Rental rental){
_rentals.addElement(rental);
}
public String getName(){
return name;
}
public String statement() {
double totalAmount = 0;
int frequentRentalPoints = 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 += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.getDaysRented() > 3) {
thisAmount += (each.getDaysRented() - 3) * 1.5;
}
break;
}
//计算积分
frequentRentalPoints++;
if (each.getMovie().getPriceCode() == Movie.NEW_RELEASE && each.getDaysRented() > 1) {
frequentRentalPoints++;
}
result+="\t"+each.getMovie().getTitle()+"\t"+String.valueOf(thisAmount)+"\n";
totalAmount+=thisAmount;
}
result += "应收总金额是" + String.valueOf(totalAmount) + "\n";
result += "您赚取了" + String.valueOf(frequentRentalPoints) + "积分";
return result;
}
}
这个程序设计的如何呢,很明显不符合面向对象的精神,其实这样一个小程序写成这样,可能也没啥大不了的但是如果他是一个复杂系统的一部分,那这个复杂系统的前途就危险了,在这个例子里
关于这个statement函数,太过于庞大,如果用户希望对系统进行修改一下,希望以HTML的格式输出详单,直接在网页上显示,现在想一下,这个变化会带来什么影响,看看代码就发现这个函数根本不能复用,唯一能做的就编写一个html()函数大量复用statement中的行为,当然现在做这个还不费力,如果计费标准发生变化呢,就必须同时修改statement()和html()函数了,并且还要确保两处修改一致,当后续还需要修改的时候,复制粘贴带来的问题就浮现了,如果用户还希望改变影片的分类规则呢,但是还没决定好怎么改,他们想到了几种方案,都会影响到积分和消费额的计算,作为一个开发者,我们可以肯定的就是:
不论用户提出什么方案,我们唯一能够获得保证就是他们一定会在六个月之内在此修改它。
重构第一步:就是为即将修改的代码建立一个可靠的测试环境,
2 分解并重组statement()
代码块越小,代码的功能越容易管理,代码的处理和移动就越轻松,首先,找出代码的逻辑泥团,并运用 Extract Method ,要在这段代码里找出函数内的局部变量和参数,任何不会被修改的变量可以当做参数传入新函数,至于被修改的就需要小心一点,如果只有一个变量会被修改,可以死把它当做函数返回值,
下面是重构后的代码
public class Customer {
private String name;
private Vector _rentals=new Vector();
private Customer(String name){
this.name=name;
}
public void addRental(Rental rental){
_rentals.addElement(rental);
}
public String getName(){
return name;
}
public String statement() {
double totalAmount = 0;
int frequentRentalPoints = 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);
frequentRentalPoints++;
if (each.getMovie().getPriceCode() == Movie.NEW_RELEASE && each.getDaysRented() > 1) {
frequentRentalPoints++;
}
result+="\t"+each.getMovie().getTitle()+"\t"+String.valueOf(thisAmount)+"\n";
totalAmount+=thisAmount;
}
result += "应收总金额是" + String.valueOf(totalAmount) + "\n";
result += "您赚取了" + String.valueOf(frequentRentalPoints) + "积分";
return result;
}
private double amountFor(Rental each){
double thisAmount=0;
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;
}
return thisAmount;
}
}
现在测试完没问题以后,就可以修改自己不喜欢的变量名字了,each-->aRental thisAmount--> result
观察这个函数,我们使用了Rental类的信息,却没有使用Customer类的信息,这就立刻值得怀疑,它是否被放错了位置,函数应该放在它所使用的数据的所属对象内,所以这个函数应该搬到Rental类中,运用Move Method
public class Rental {
private Movie movie;
private int daysRented;
private Rental(Movie movie,int daysRented){
this.movie=movie;
this.daysRented=daysRented;
}
public int getDaysRented(){
return daysRented;
}
public Movie getMovie(){
return movie;
}
private double amountFor(){
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;
}
}
此时,Customer类中的函数委托调用新函数即可(如果 我们不想修改对外的调用)
private double amountFor(Rental aRental){
return aRental.amountFor();
}
找到旧函数所有的调用点,修改
thisAmount=each.getCharge();
下一个应该引起我们注意的是。thisAmount变得多余了,他接受了函数的赋值以后就没有再变化过,所以这里运用 Replace Temp with Query 把thisAmount 除去
public String statement() {
double totalAmount = 0;
int frequentRentalPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for" + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
frequentRentalPoints++;
if (each.getMovie().getPriceCode() == Movie.NEW_RELEASE && each.getDaysRented() > 1) {
frequentRentalPoints++;
}
result+="\t"+each.getMovie().getTitle()+"\t"+String.valueOf(each.amountFor())+"\n";
totalAmount+=each.amountFor();
}
result += "应收总金额是" + String.valueOf(totalAmount) + "\n";
result += "您赚取了" + String.valueOf(frequentRentalPoints) + "积分";
return result;
}
临时变量往往容易引发问题,他们会导致大量参数被传来传去,而其实完全没有这种必要,因为你很容易跟丢他们,尤其是在长长的函数之中更是如此,当然,我们也需要付出性能上的代价,例如,本例子中费用的计算就被 计算了两次,但是这也很容易在Rental中被优化,而且如果代码有合理的组织和管理,优化就会有很好的效果,关于这个问题后边会有作者的详细解释
下一步对会员积分的计算做相似处理
Rental类中
public int getFrequentRenterPoints(){
if (getMovie().getPriceCode() == Movie.NEW_RELEASE && getDaysRented() > 1) {
return 2;
}else {
return 1;
}
}
去除临时变量
目前临时变量有两个 totalAmount 和frequentRentalPoints ,运用Replace Temp with Query
public class Customer {
private String name;
private Vector _rentals=new Vector();
private Customer(String name){
this.name=name;
}
public void addRental(Rental rental){
_rentals.addElement(rental);
}
public String getName(){
return name;
}
public String statement() {
Enumeration rentals = _rentals.elements();
String result = "Rental Record for" + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result+="\t"+each.getMovie().getTitle()+"\t"+String.valueOf(each.getCharge())+"\n";
}
result += "应收总金额是" + String.valueOf(getTotalCharge()) + "\n";
result += "您赚取了" + String.valueOf(getTotalFrequentRenterPoints()) + "积分";
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;
}
private double getTotalFrequentRenterPoints(){
double result=0;
Enumeration rentals = _rentals.elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result+=each.getFrequentRenterPoints();
}
return result;
}
}
做完这次重构,作者说需要停下来思考一下,这次重构存在一个问题,原本代码执行性while循环一次,新版本却要执行三次,如果while循环耗时很多,就可能大大降低程序的性能,单单因为这个原因,很多程序员就不愿进行这个重构动作,但是请注意用词:如果和可能,除非进行评测,否则就无法确定循环的执行时间,也无法知道这个循环是否被经常使用以至于影响系统的整体性能,重构时你不必担心这些,优化才需要担心他们。但那个时候你已经处于一个比较有利的位置,有更多的选择可以完成更有效的优化,(后续会讨论这个问题)
现在Customer类内任何代码都可以调用这些查询函数了,如果系统其它部分需要这些信息,也可以轻松的将查询函数加入Customer类的接口。如果没有这些查询函数,其它函数就必须了解Rental类,并自行简历循环,在一个复杂的系统中,这将使程序的编写难度和维护难度大大增加,所以如果现在客户要求详单一html的格式输出,我们在编写htmlStatement()函数的时候,就不必剪贴复制了,所以如果计算规则发生了改变,只需要在程序中一处做修改,完成其他类型的详单也都是很快很容易的事情了,而这时我们无论如何都得做的。
运用多态取代与价格相关的条件逻辑
这个问题的一部分就是switch语句,最好不要在另一个对象的属性基础上运用switch语句,如果不得不使用,也要在自己的数据上使用,这就暗示getCharge()方法需要移动到Movie类中,为了让它得以运作,必须把租期长度作为参数传递过去,当然,租期长度来自Rental类,计算费用的时候需要两项数据:租期长度和影片类型,为什么选择将租期长度传给Movie对象,而不是将影片类型喜欢递给Rental对象呢,因为本系统可能发生变化的是加入新影片类型,这种变化带有不稳定倾向,如果影片类型有所变化,我们希望尽量控制它造成的影响,所以选择在Movie中计算费用。同样的,积分计算也如此处理
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 arg){
priceCode=arg;
}
public String getTitle(){
return title;
}
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;
}
}
}
public class Rental {
private Movie movie;
private int daysRented;
private Rental(Movie movie,int daysRented){
this.movie=movie;
this.daysRented=daysRented;
}
public int getDaysRented(){
return daysRented;
}
public Movie getMovie(){
return movie;
}
public double getCharge(){
return movie.getCharge(daysRented);
}
public int getFrequentRenterPoints(){
return movie.getFrequentRenterPoints(daysRented);
}
}
终于来到继承
我们有数种影片类型,他们一不同的方式回答相同的问题,这听起来很像子类的工作,我们可以建立Movie的三个子类,每个都有自己的计费方法,这么一来就可以用多态来取代switch语句了,很遗憾这里有个小小的问题,一部影片可以在生命周期内修改自己的分类,一个对象却不能再生命周期内修改自己所属的类,不过还有一个解决办法:State模式,加入一层间接性,Price,我们可以再Price对象内进行子类动作化
那这个算是State还是Strategy,取决于Price类究竟代表计费方式(PriceStrategy)还是代表影片的某个状态,在这个阶段,对于模式名称的选择反映出你对结构的想法,此时我把他视为影片的某种状态,如果未来我觉得Strategy模式能更好的说明我的意图,我会再重构他,修改名字,以形成Strategy
为了引入State模式,使用了三个重构方法。首先运用Replace Type Code with State将与类型相关的行为搬移到State模式内,然后运用Move Method将switch语句移到Price类,最后运用Replace Conditional with Polymorphism 去掉switch语句
第一步针对类型代码使用Self Encapsulate Field ,确保任何时候都通过取值函数和设置函数来访问类型代码,多数访问操作来自其他类,他们已经在使用取值函数,但是构造函数仍然直接访问价格代码,这里可以用一个设置函数来替代,
public class Movie ......
public Movie(String title,int priceCode){
this.title=title;
// this.priceCode=priceCode;
setPriceCode(priceCode);
}
现在新建一个Price类,并在其中提供类型相关的行行为,为了实现这一点,在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类中保存一个Price对象,而不再是一个price_Code变量,此外,还要修改访问函数
public class Movie ......
// private int priceCode;
private Price price;
public Movie(String title,int priceCode){
this.title=title;
// this.priceCode=priceCode;
setPriceCode(priceCode);
}
public int getPriceCode(){
return price.getPriceCode();
}
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方法实施Move Method,
public class Movie ......
public double getCharge(int daysRentend){
return price.getCharge(daysRentend);
}
将getCharge方法具体实现搬到Price类中,这里就不贴代码了,继续,运用Replace Conditional with Polymorphism ,做法是一次取出一个分支,在相应的类建立一个覆盖函数,
这里就把是三个分支都弄了吧,毕竟这样贴代码好累 啊
public class RegularPrice extends Price{
@Override
int getPriceCode() {
return Movie.REGULAR;
}
public 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;
}
public 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;
}
public double getCharge(int daysRented){
return daysRented * 3;
}
}
处理完所有的分支以后,就把Price.getChar()声明为abstract,
public abstract class Price ......
abstract int getPriceCode();
public abstract double getCharge(int daysRented);
然后同样的手法处理getFrequentRenterPoints(),首先把这个函数移动到Price类,但是这一次不会声明成abstract,只为新片类型增加一个覆写函数,并在父类内留下一个已定义的函数,使他成为一种默认的行为
ublic abstract class Price {
abstract int getPriceCode();
public abstract double getCharge(int daysRented);
public int getFrequentRenterPoints(int daysRentend){
return 1;
}
}
public class NewReleasePrice extends Price{
@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
public double getCharge(int daysRented){
return daysRented * 3;
}
public int getFrequentRenterPoints(int daysRentend){
return (daysRentend>1) ? 2:1;
}
}
好的,圆满结束,引入State模式花了作者不少力气,(我这一通敲,也是累够呛)值得吗?这么做的收获就是:如果以后要修改任何与价格有关的行为,或者是添加新的定价标准,或者是加入其它取决于价格的行为,程序的修改会容易的多,这个程序的其它部分并不知道我运用了State模式,对于我目前拥有的这么几个小量行为来说,任何功能或者特性上的修改也许都不合算,但如果在一个更复杂的系统中,有十多个与价格相关的函数,程序修改的难易度就会有很大区别,以上修改都是小步骤进行,进度四惠过慢,但是基本一次都没有打开调试器,所以整个过程很快就过去了。
结语:
作者希望用这个简单的例子让大家对代码重构有一点感觉,里边提到了很多重构手法,Extract Method(提炼函数) ,Move Method(搬移函数) ,Replace Conditional with Polymorphism (以多态取代条件表达式),Self Encapsulate Field (自封装字段),ReplaceType Code with State/Strategy(State/Strategy取代类型代码),这些重构行为都让责任分配更合理,代码维护更轻松。
因为我是这个本书的搬运者,所以在措辞和表达上想尽力表达出作者的最原始意图,所以描述的有点繁琐了,当我们熟悉了整个重构的套路,对程序的修改就会变得快多了,因为至少你不用想写博客的事情,也不用想别人知不知道你再用什么方法来重构。
通过读了这本书,对重构的理解感觉就像是教师的工作,纯粹是一种良心活,因为也许你做完一个版本就走了,这个程序的后期维护就与你没关系了,而你还要去为了后续人的方便来一遍一遍的review自己的代码,重构它,这良心,也是没谁了,所以提倡大家对代码有一种洁癖感,写一行有质量的代码,功德无量。
撒花,结束
对错别字真是忍不了啊