第一个案例:
重构的第一步:为即将改变的代码建立一组可靠的测试环境。
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 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; } }
public class Rental { private Movie _movie; private int _daysRented; public Rental(Movie _movie, int _daysRented) { this._movie = _movie; this._daysRented = _daysRented; } public Movie getMovie() { return _movie; } public int getDaysRented() { return _daysRented; } }
public class Cunstomer { private String _name; private Vector _rentals = new Vector(); public Cunstomer(String _name) { this._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 += 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; } }
现在需要重构的就是这个statement:
重构这个statement分为以下几步:
第一步:switch语句
我用idea重构功能,他并自动生成的方法,thisAmount也是作为入参传进来的,看起来好像没多大差,至少看到这我是没看出来差别或者特殊的用意的。
第二步:搬移“金额计算”的代码
观察amountFor()时,发现这个函数使用了Rental类的信息,没有使用来自Customer类的信息。绝大多数情况下,函数应该放在它所使用的数据的所属对象内,所以amountFor()应该移动到Rental类去。然后找出旧函数的引用点并修改为引用新函数(全局搜索一下看都有哪些引用)。去掉旧函数。然后进行测试。
有时候会保留旧函数,然后让它调用新函数。比如说旧函数是public的函数,而又不想修改其他类的接口,这就是一种很有效的方法。
移动之后,函数会有微调,到Rental类的方法,不需要参数了。
第三步:改完之后,thisAmount就变得多余了,因为他接受的是each.getCharge()的结果,并且不会发生变化,所以可以去掉thisAmount这个局部变量。
第四步:提炼“常客积分计算”代码
首先,积分计算应该放在Rental类。然后,再看一下局部变量。each,可以被当作参数传入新函数;另一个临时变量是frequentRenterPoints,它在被使用前有初始化值,但提炼出来的函数并没有读取该值,所以我们不需要把它当作参数传进去,只需要把新函数的返回值累加上去就行了。
第五步:去除临时变量。
临时变量可能是个问题,他们使函数变得冗长而复杂。这个例子有2个临时变量totalAmount和frequentRentalPoints,他们都是用来从Rental对象中获得某个总量,可以利用查询函数来取代totalAmount和frequentRentalPoints这2个临时变量。这样类中任何函数都可以调用这个查询函数了。
第六步:
假如,需要修改影片分类规则,费用计算方式和常客积分计算方式有待确定。这个时候盲目重构,肯定是不合适的。
这个时候可以运用多态取代与价格相关的条件逻辑。
首先,是switch,最好不要再另一个对象的属性基础上运用switch语句,如果不得不使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用。
也就是getCharge()应该从Rental类移动到Movie类中。这个时候想让方法可以使用,必须把租期长度作为参数传递进去。租期长度来自Rental对象。计算费用时需要2项数据:租期长度和影片类型。选择把租期长度传给Movie对象而不是将影片类型传给Rental对象,是因为影片类型可能会发生变化,所以选择在Movie对象内计算费用。
第7步:继承
加入这一层间接性,可以在Price对象内进行子类化动作,可以在任何必要时刻修改价格。
为了引入State模式,首先运用Replace Type Code with State/Strategy,将与类型相关的行为搬移到State模式内。然后运用Move Method讲switch语句移动到Price类,最后运用Replace Conditional with Palymorphism去掉switch语句。
运用Replace Type Code with State/Strategy:
第一步骤,针对类型代码使用SelfEncapsulate Field,确保任何时候都通过取值函数和设置函数来访问类型代码。多数访问操作来自其他类,他们已经在使用取值函数,但构造函数仍然直接访问价格代码,可以在Movie构造函数中,用一个设置函数来代替直接访问价格,然后编译测试,确保没有破坏任何东西。新建一个Price类,并在其中提供类型相关的行为,即在Price类中加入一个抽象函数,并在所有子类中加上对应的具体函数。
第二步:修改Movie类中的“价格代号”访问函数(取值函数/设置函数),让他们使用新类。在Movie类内保存一个Price对象,而不再保存一个_priceCode变量,修改访问函数。然后重新编译测试。
Move Method:对getCharge()实施Move Method。
搬移之后,运用Replace Conditional with Palymorphism:一次取出一个case分支,在相应的类建立一个覆盖函数。这个函数覆盖了父类中的case分支,父类中的代码先不动,在取出下一个case分支,一次处理,并编译测试(注意确保执行的是子类的)。处理完所有的case分支之后,把P.getCharge()声明为abstract。再运用同样的手法处理getFrequentRenterPoints().但是这个方法不需要把超类函数声明为abstract,只需要为新片类型增加一个覆盖函数,并在超类留下一个已定义的函数,使它成为一种默认行为。
引入State模式,可以做到,如果要修改任何与价格有关的行为,或是添加新的定价标准,或是加入其它取决于价格的行为,程序的修改会容易很多。
经过以上重构之后,代码变成了如下的样子:
public abstract class Price { abstract int getPriceCode(); abstract double getCharge(int daysRented); public int getFrequentRenterPoints(int daysRented) { return 1; } }
public class RegularPrice extends Price { @Override int getPriceCode() { return Movie.REGULAR; } public double getCharge(int daysRented) { double thisAmount = 2; if (daysRented > 2) thisAmount += (daysRented - 2) * 1.5; return thisAmount; } }
public class ChildrensPrice extends Price { @Override int getPriceCode() { return Movie.CHILDRENS; } public double getCharge(int daysRented) { double thisAmount = 1.5; if (daysRented > 3) thisAmount += (daysRented - 3) * 1.5; return thisAmount; } }
public class NewReleasePrice extends Price { @Override int getPriceCode() { return Movie.NEW_RELEASE; } public double getCharge(int daysRented) { return daysRented * 3; } public int getFrequentRenterPoints(int daysRented) { return daysRented > 1 ? 2 : 1; } }
public class Rental { private Movie _movie; private int _daysRented; public Rental(Movie _movie, int _daysRented) { this._movie = _movie; this._daysRented = _daysRented; } public Movie getMovie() { return _movie; } public int getDaysRented() { return _daysRented; } public double getCharge() { return _movie.getCharge(_daysRented); } public int getFrequentRenterPoints() { return _movie.getFrequentRenterPoints(_daysRented); } }
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) { _title = title; setPriceCode(priceCode); } public String getTitle() { return _title; } public void setTitle(String _title) { this._title = _title; } public int getPriceCode() { return _price.getPriceCode(); } public void setPriceCode(int args) { switch (args) { case REGULAR: _price = new RegularPrice(); break; case CHILDRENS: _price = new ChildrensPrice(); break; case NEW_RELEASE: _price = new NewReleasePrice(); break; default: throw new IllegalArgumentException("Incorrent Price Code"); } } public double getCharge(int daysRented) { return _price.getCharge(daysRented); } public int getFrequentRenterPoints(int daysRented) { return _price.getFrequentRenterPoints(daysRented); } }
public class Cunstomer { private String _name; private Vector _rentals = new Vector(); public Cunstomer(String _name) { this._name = _name; } public void addRental(Rental arg) { _rentals.addElement(arg); } 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(amountFor(each)) + "\n"; } result += "Amount owed is" + String.valueOf(getTotalCharge()) + "\n"; result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points"; return result; } private int getTotalFrequentRenterPoints() { int frequentRenterPoints = 0; Enumeration rentals = _rentals.elements(); while (rentals.hasMoreElements()) { Rental each = (Rental) rentals.nextElement(); frequentRenterPoints++; frequentRenterPoints = each.getFrequentRenterPoints(); } return frequentRenterPoints; } private double getTotalCharge() { double totalAmount = 0; Enumeration rentals = _rentals.elements(); while (rentals.hasMoreElements()) { Rental each = (Rental) rentals.nextElement(); totalAmount += amountFor(each); } return totalAmount; } private double amountFor(Rental rental) { return rental.getCharge(); } }
可以试试把最开始的代码考到idea中,然后看看如果是你,你会怎么优化,然后按提示优化下,再跟答案比一下,最后对比一下自己的答案,还是挺有意思的,这个就是第一章的内容。
第二章:重构原则
重构的目的是使软件更容易被理解和修改。重构不会改变软件可观察的行为,重构之后软件功能一如以往。
与之形成对比的是性能优化。和重构一样,性能优化通常不会改变组建的行为(除了执行速度),之后改变内部结构。但是两者出发点不同:性能优化往往使代码较为难理解,但为了得到所需要的性能不得不那么做。
为何重构:
1.重构改进软件设计,比如,消除重复代码,确定所有事物和行为在代码中只表述一次,方面未来代码的修改。
2.重构使软件更容易理解:在重构上花一点点时间,就可以让代码更好地表达自己的用途,即“准确说出我所要的”。而且,还可以利用重构来帮助自己协助理解不熟悉的代码。真正动手修改代码,让它更好地反映出我的理解,然后重新执行,看它是否正常运行,检验自己的理解是否正确。随着代码的简洁,就可以看到以前看不到的设计层面的东西。
3.重构帮助找bug:
4.重构提高编程速度
何时重构:
重构不是一件应该特别拨出事件做的事情,重构应该随时进行,不应该为了重构而重构。之所以重构,是因为想做别的事情,而重构可以帮助把那些事做好。
三次法则:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次在做类似的事,就应该重构。事不过三,三则重构。
1.添加功能时重构
2.修补错误时重构:调试过程中运用重构,让代码更具有可读性,因为代码还不够清晰,没有让自己一眼看出bug。
3.复审代码时重构:开始重构前可以先阅读代码,得到一定程度的理解,并提出一些建议。一旦想到一些点子,考虑是否可以通过重构立即轻松地实现它们。多做几次重构,代码会变得更清楚,提出更多恰当的建议,可以获得更高层次的认识。重构还可以帮助代码复审工作得到更具体的认识,不仅获得建议,而且其中许多建议能够理解实现,从实现中得到成就感。
间接层和重构:
间接层的价值:允许逻辑共享;分开解释意图和实现;隔离变化;封装条件逻辑
找出一个缺乏“间接层利益”之处,在不修改现有行为的前提下,为它加入一个间接层,获得一个更有价值的程序,提高程序质量,让自己再明天受益。
找出不值得的间接层,将它拿掉。这种间接层常以中介函数形式出现,它可能是个组件,你本来期望在不同地方共享它或者让它表现出多态性,最终却只在一处用到。
重构的难题:
1.数据库:重构经常出问题的一个领域就是数据库。第一,绝大多数商用程序都和背后的数据库结构紧密耦合在一起;第二,数据迁移。就算将系统分层,将数据库结构和对象模型间依赖降至最低,但数据库结构改变还是让你不得不迁移所有数据,这是漫长而烦琐的工作。
在非对象数据库中,解决这个问题的办法之一就是:在对象模型和数据库模型之间插入一个分隔层,这就可以隔离两个模型各自的变化。升级某一模型是,只需升级上述的分离层即可。这样的分割层会增加系统复杂度,但可以带来很大的灵活度。如果同时拥有多个数据库,或如果数据库模型较为复杂使你难以控制,那么就是不进行重构,这分隔层也是很重要的。
对开发者而言,对象数据库既有帮助也有妨碍。自行完成迁移时,必须留神类中的数据结构变化,可以放心把类的行为转移过去,但是转移字段时必须格外小心,数据尚未被转移前就得先运用访问函数造成“数据已经转移”的假象。一旦确定知道数据应该放在何处,就可以一次性将数据迁移过去。这是唯一需要修改的只有访问函数,可以降低错误风险。
2.修改接口:如何面对那些必须修改“已发布接口”的重构手法?尽量让旧接口调用新接口。但这样会使接口变得复杂,还有另一个选择,能不发布接口的时候,尽量不要发布接口。
何时不该重构:现行代码不能正常运行,满是错误,重构不如重写来的简单。重构之前,代码必须在大部分情况下正常运行。项目接近最后期限,避免重构。
重构与性能:除了对性能有严格要求的实时系统,其他任何情况下“编写快速软件”的秘密就是,首先写出可调的软件,然后调整它以求获得足够速度。
第三章:代码的坏味道
1.重复代码
2.过长函数
3.过大的类
4.过长参数列
5.发散式变化
6.霰(xian,第四声)弹式修改
7.依恋情节
函数对某个类的兴趣高过对自己所处类的兴趣,这种孺慕之情通常就是数据。把这个函数移到另一个地方,使用Move Method,有时候函数中只有一部分受这种依恋之苦,就应该使用Extract Method提炼到独立的函数,在使用move method带它去他该去的地方。如果一个函数用到几个类的功能,判断哪个类拥有最多被此函数使用的数据,把该函数移到那个类,如果先以Extract Method将这个函数分解为数个较小函数并分别置放于不同地点,上述步骤就更容易完成了。将总是一起变化的东西放在一块儿。如果例外出现,就搬移那些行为,保持变化只在一地发生。Stragegy和Visitor使得可以轻松修改函数行为,但多一层间接性的代价。
8.数据泥团
9.基本类型偏执
10.switch语句
11.平行继承体系
12.冗余类
13.夸夸其谈未来性
14.令人迷惑的暂时字段
15.过度耦合的消息链。
16.中间人
17.狎昵关系
18.异曲同工的类
19.不完美的库类
20.纯粹的数据类
21.被拒绝的遗赠
22.过多的注释
第4章:构建测试体系
自动化的单元测试:test suit
每当收到bug报告,请先写一个单元测试来暴露bug
继续添加更多测试:观察类该做的所有事情,然后针对任何一项功能的任何一种可能失败情况,进行测试。
测试的一项重要技巧就是“寻找边界条件”。“寻找边界条件”也包括寻找特殊的,可能导致测试失败的情况。
当事情被认为应该会出错,要记得检查是否抛出了预期的异常。
把测试集中在可能出错的地方。“花合理时间抓出大多数bug”要好过“穷尽一生抓出所有bug”。
构建一个良好的bug检测器并经常运行它,对任何开发工作都将大有裨益,并且是重构的前提。
第5章:重构列表
第6章:重新组织函数
1.提炼函数
有局部变量时:
局部变量最简单的情况是:被提炼代码段只是读取这些变量的值,并不修改它们。这种情况下我可以简单地将它们当作参数传给目标函数。
如果局部变量是个对象,而被提取代码调用了对该对象造成修改的函数,也可以如法炮制,只需要将这个对象作为参数传递给目标函数即可。只有在被提炼代码真的对一个局部变量赋值的情况下,才必须采取其他措施。
2.内联函数
3.内联临时变量
有一个临时变量,只被简单表达式赋值一次,而它妨碍了其他重构手法。就需要内联化。
4.以查询取代临时变量
5.引入解释性变量
将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
6.分解临时变量
程序中某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果,针对每次赋值,创造一个独立、对应的临时变量。
7.移除对参数的赋值
(这个我不是很能理解和吸收)
8.以函数对象取代函数
有一个大型函数,其中对局部变量的使用使你无法采用Extract Method。将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后可以在同一个对象中将这个大型函数分解为多个小型函数。
它带来的好处是:可以轻松地对compute()函数采取Extract Method,不必担心参数传递问题。
9.替换算法
第七章:在对象之间搬移特性
1、搬移函数:
2.搬椅字段
3.提炼类
4.将类内联化
5.隐藏委托关系
6.移除中间人
7.引入外加函数
8.引入本地扩展
这个根本没看懂。
第八章:
1.自封装字段
2.以对象取代数据值
3.将值对象改为引用对象
4.将引用对象改为值引用
5.以对象代替数组
6.复制被监视的数据
7.将单向关联改为双向关联
8.将双向关联改为单向关联
7和8都没怎么懂
9.以字面常量取代魔法数
10.封装字段
11.封装集合
12.以数据类取代记录
13.以类取代类型码
(未完待续)