第1章 重构,第一个案例
1.1 起点
[读者注]这里主要举了一个影片出租店用的程序,作者会基于这个程序做重构。
@Getter
@Setter
public class Customer {
private String name;
private Vector<Rental> retalVector = new Vector();
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration<Rental> rentals = retalVector.elements();
String result = "Retal Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = rentals.nextElement();
//determine amounts for each line
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;
}
}
//add frequent renter points
frequentRenterPoints++;
//add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) {
frequentRenterPoints++;
}
//show figures for this rental
result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result = "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
return result;
}
}
@Setter
@Getter
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 Integer priceCode;
}
@Getter
@Setter
@AllArgsConstructor
public class Rental {
private Movie movie;
private Integer daysRented;
}
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。
1.2 重构的第一步
重构前,先检验自己是否有一套可靠的测试机制。这些测试必须有自我检验能力。
1.3 分解并重构 statement()
本章重构过程的第一阶段中,我将说明如何将长长的函数切开,并将较小块的代码移到合适的类。我希望降低代码重复量,从而使新的(打印HTML详单用的)函数更容易撰写。
第一个步骤是找出代码的逻辑泥团并运用Extract Method。本例一个明显的逻辑泥团就是switch语句,把它提炼到独立函数中似乎比较好。
首先我得在这段代码里找出函数内的局部变量和参数。这里有each和thisAmount。
第2章 重构原则
2.1 何谓重构
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
两顶帽子
使用重构技术开发软件时,你把自己的时间分配给两种截然不同的行为:添加新功能,以及重构。添加新功能时,你不应该改修改既有代码,只管添加新功能。通过测试(并让测试正常运行),你可以衡量自己的工作进度。重构时你就不能再添加功能,只管改进程序结构。此时,你不应该添加任何测试(除非发现有先前遗漏的东西),只在绝对必要(用以处理接口变化)时才修改测试。
2.2 为何重构
重构改进软件设计
如果没有重构,程序的设计会逐渐腐败变质。
重构很像是在整理代码,你所做的就是让所有东西回到应处的位置上。
重构使软件更容易理解
重构帮助找到bug
Kent Beck经常形容自己的一句话:“我不是个伟大的程序员,我只是个有着优秀习惯的好程序员。”
重构提高编程速度
良好的设计是快速开发的根本。
2.3 何时重构
重构应该随时随地进行。你不应该为重构而重构,你之所以重构,是因为你想做别的事,而重构可以帮助你把那些事做好。
三次法则
第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。
添加功能时重构
修补错误时重构
评审代码时重构
第3章 代码的坏味道
3.1 Duplicated Code(重复代码)
| 例子 | 重构方法 |
|---|---|
| 同一个类的两个函数含有相同的表达式 | 采用Extract Method提炼出重复的代码 |
| 两个互为兄弟的子类内含有相同表达式 | 只需要对两个类都使用Extract Method,然后再对被提炼出来的代码使用Pull Up Method,将它推入超类 |
| 如果两个毫不相关的类出现了Duplicated Code | 考虑对其中一个使用Extract Class,将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类 |
3.2 Long Method(过长函数)
拥有短函数的对象会活得比较好,比较长。
你应该更积极地分解函数。我们应该遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。
我们可以对一组甚至短短一行代码做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能解释其用途,我们也该毫不犹豫地那么做。关键不在于函数的长度,而在于函数做“什么”和“如何做”之间的语义距离
百分之九十九的地方,要把函数变小,只需要使用Extract Method。
就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到一个独立函数中。
3.3 Large Class(过大的类)
如果想利用单个类做太多事情,其内往往就会出现太多实例变量。一但如此,Duplicated Code也就接踵而至了。
解决方法:可以运用Extract Class将多个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放到一起。例如,depositAmount和depositCurrency可能应该隶属于同一个类。通常如果类内的某个变量有着相同的前缀或字尾,这就意味着有机会把它们提炼到某个组件内。如果这个组件适合作为一个子类,你就会发现Extract Subclass往往比较简单。
3.4 Long Parameter List(过长参数列)
使用对象来传递
3.5 Divergent Change(发散式变化)
定义:如果某个类经常因为不同的原因在不同的方向上发生变化
一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。
如果你看着一个类说:“呃,如果新加入一个数据库,我必须修改这三个函数;如果新出现一个一种金融工具,我必须修改这四个函数。”那么,此时也许将这三个对象分成两个会比较好,这么一来每个对象就可以只因一种变化而需要修改。
3.6 Shotgun Surgery(霰弹式修改)
定义:如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改
解决方法: Move Method和MoveField把需要修改的代码放到同一个类。如果眼下没有合适的类,就创造一个。通常可以运用Inline Class把一系列相关行为放进用一个类。
3.7 Feature Envy(依恋情节)
定义:函数对于某个类的兴趣高于对于自己所处类的兴趣。这种孺慕之情最通常的焦点就是数据。
无数次经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。
解决方法:将函数移至另一个地点。
3.8 Data Clumps(数据泥团)
定义:数据项就像小孩子,喜欢成群结队地待在一块。
你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。
解决方法:运用Extract Class将它们提炼到一个独立对象中。
不必在意Data Clumps只用上新对象的一部分字段,只要以新对象取代两个(或更多)字段,你就只会票价了。
一个好的评判方法是:删掉众多数据中的一项。这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是个明确信号:你应该为它们产生一个新对象。
3.9 Primitive Obsession(基本类型偏执)
对象技术的新手通常不愿意在一个小任务上运用小对象——像是结合数值和币种的money类,由一个起始值和一个结束值组成的range类、电话号码和邮政编码等的特殊字符串。
3.10 Switch Statements(switch惊悚现身)
面向对象程序的一个最明显特征就是:少用switch(或case)语句。
从本质上来说,switch语句的问题在于重复。你常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的case子句,就必须找到所有switch 语句,并修改它们。
面向对象的多态概念可为此带来优雅的解决方法
大多数时候,一看到switch语句,就应该考虑用多态来替代它。
switch语句常常根据类型码进行选择,你要的是“与该类型码相关的函数或类”。所以应该使用Extract Method将switch语句提炼到一个独立函数中,再以Move Method将它搬移到需要多态性的那个类中。
3.11 Parallel Inheritance Hierachies(平行继承体系)
Parallel Inheritance Hierachies其实是Shotgun Surgery的特殊情况。在这情况,每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。
3.12 Lazy Class(冗赘类)
如果某些子类没有做足够的工作,试试Collapse Hirarchy。对于几乎没用的组件,你应该以Inline Class对付它们。
3.13 Speculative Generality(夸夸其谈未来性)
如果你的某个抽象类其实没有太大作用,请运用Collapse Hierarchy。不必要的委托可运用Inline Class除掉。如果函数的某些参数未被用上可对它实施Remove Parameter。如果函数名称带有多余的抽象意味,应该对它实施Rename Method,让它现实一点。
3.14 Temporary Field(令人迷惑的暂时字段)
3.15 Message Chains(过度耦合的消息链)
定义:用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象 解决方法:使用Hide Delegate
3.16 Middle Man(中间人)
人们可能过度运用委托。如果看到某个类接口中有一半的函数都在委托给其他类,这样就是过度运用。
这时就应该使用Remove Middle Man,直接和真正负责的对象打交道。
3.17 Inappropriate Intimacy(狎昵关系)
如果两个类过于亲密,过分狎昵,你中有我,我中有你,两个类彼此使用对方的私有的东西,就是一种坏代码味道。我们称之为Inappropriate Intimacy(狎昵关系)。
解决方法:尽量把有关联的方法或属性抽离出来,放到公共类,以减少关联
3.18 Alternative Classes with Different Interfaces(异曲同工的类)
以通过重命名,移动函数,或抽象超类等方式优化
3.19 Incomplete Library Class(不完美的库类)
大多数对象只要够用就好,如果类库构造得不够好,我们不可能修改其中的类使它完成我们希望完成的工作。可以:包一层函数或包成新的类。
3.20 Data Class(纯稚的数据类)
什么是Data Class? 它们拥有一些字段,以及用于访问(读写)这些字段的函数。这些类很简单,仅有公共成员变量,或简单操作的函数。
如何优化呢?将相关操作封装进去,减少public成员变量。比如:
- 如果拥有public字段->
Encapsulate Field - 如果这些类内含容器类的字段,应该检查它们是不是得到了恰当地封装->
Encapsulate Collection封装起来 - 对于不该被其他类修改的字段->
Remove Setting Method->找出取值/设置函数被其他类运用的地点->Move Method把这些调用行为搬移到Data Class来。如果无法搬移整个函数,就运用Extract Method产生一个可被搬移的函数->Hide Method把这些取值/设置函数隐藏起来。
3.21 Refused Bequest(被拒绝的馈赠)
子类应该继承父类的数据和函数。子类继承得到所有函数和数据,却只使用了几个,那就是继承体系设计错误,需要优化。
- 需要为这个子类新建一个兄弟类->
Push Down Method和Push Down Field把所有用不到的函数下推给兄弟类,这样一来,超类就只持有所有子类共享的东西。所有超类都应该是抽象的。 - 如果子类复用了超类的实现,又不愿意支持超类的接口,可以不以为然。但是不能胡乱修改继承体系->
Replace Inheritance with Delegation(用委派替换继承).
3.22 Comments(过多的注释)
- 如果你需要用注释来解释一块代码做了什么,试试Extract Method;
- 如果函数已经提炼出来,但还是需要注释来解释其行为,试试Rename Method;
- 如果你需要注释来说明某些系统的需求规格,试试Introduce Assertion
当你感觉需要撰写注释时,请先尝试重构,试着让所有的注释都变得多余。
第4章 构建测试体系
我并不总是测试所有可能的组合,但我会尽量测试每一个类,这可以大大减少各种组合所造成的风险。是的,我总有可能遗漏些什么,但是我觉得“花合理时间抓出大多数bug”要好过“穷尽一生抓出所有bug”.
第5章 重构列表
重构的基本技巧:小步前进,频繁测试。
许多重构手法,都引入了设计模式。
第6章 重新组织函数
6.1 Extract Method(提炼函数)
重构前
void printOwing(double amount) {
printBanner();
//print details
System.out.println("name:" + name);
System.out.println("amount:" + amount);
}
重构后
void printOwing(double amount) {
printBanner();
printDetails(amount);
}
private void printDetails(double amount) {
System.out.println("name:" + name);
System.out.println("amount:" + amount);
}
动机
有几个原因造成我喜欢简短而命名良好的函数:
- 如果每个函数的粒度都很小,那么函数被复用的机会就越大
- 这会使高层函数读起来像一系列注释
- 如果函数都是细粒度,那么函数被覆写也会更容易些
如果提炼可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。
6.2 Inline Method(内联函数)
重构前
int getRating() {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
private boolean moreThanFiveLateDeliveries() {
return numberOfLateDeliveries>5;
}
重构后
int getRating() {
return (numberOfLateDeliveries > 5) ? 2 : 1;
}
动机
本书经常以简短的函数表现动作意图,这样会让代码更清晰易读。但有时候你会遇到某些函数,其内部代码和函数名称同样清晰易读。这种情况,就应该直接去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。
另一种需要Inline Method的情况:有一群组织不合理的函数。
6.3 Inline Temp(内联临时变量)
这个临时变量只被一个简单表达式赋值一次,而它妨碍了其他重构手法。
重构前
Double basedPrice = anOrder.basePrice();
return (basedPrice>1000);
重构后
return (anOrder.basePrice()>1000);
动机
Inline Temp 多半是作为Replace Temp with Query的一部分使用的,所以真正的动机出现在后者那儿。 唯一单独使用Inline Temp的情况是:这个临时变量被赋予某个函数调用的返回值。 一般来说,这样的临时不会有任何危害,可以放心地把它留在那儿。但如果这个临时变量妨碍了其他的重构手法,例如,Extract Method,你就应该将它内联化。
做法
如果这个变量未被声明为final,那就将它声明为final,然后编译。这可以检查该临时变量是否真的只被赋值一次。
6.4 Replace Temp with Query(以查询取代临时变量)
你的程序以一个临时变量保存某一表达式的运算结果。
将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可被其他函数调使用。
重构前
double basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
return basePrice * 0.98;
重构后
if (getBasePrice() > 1000) {
return getBasePrice() * 0.95;
}
return getBasePrice() * 0.98;
private double getBasePrice() {
return quantity * itemPrice;
}
动机
临时变量的问题在于:它们是临时的,而且只能在所属函数内使用。由于临时变量只在所属函数内可见,所以它们会驱使你写出更长的函数,因为只有这样你才能访问到所需要的临时变量。
如果把临时变量替换为一个查询,那么同一个类中的所有函数都可以获得这份信息。这将带给你极大帮助,使你能够为这个类编写更清晰的代码。
Replace Temp with Query往往是你运用Extract Method 之前必不可少的一个步骤。局部变量会使代码难以被提炼,所以你尽可能将它们替换为查询式。
做法
- 找出只被赋值一次的临时变量。如果某个临时变量被赋值超过一次,考虑使用Split Temporary Variable 将它们分隔成多个变量。
- 将该临时变量声明为final
- 编译(确保这个变量只被赋值一次)
- 将“对该临时变量赋值”语句的等号右侧部分提炼到一个独立函数中。
- 编译,测试
- 实施Inline Temp
我们常常使用临时变量保存循环中的累加信息。在这种情况下,整个循环都可以被提炼为一个独立函数,这也使原本的函数可以少掉几行扰人的循环逻辑。
有时候你会在一个循环中累加好几个值,这种情况应该对每个累加值重复一次循环,这样就可以将所有的临时变量都替换为查询。
运用此手法,你可能会担心性能问题。和其他性能问题一样,我们现在不管它,因为它十有八九根本不会造成任何影响。若是性能真的出了问题,你也可以在优化时期解决它。代码组织良好,你往往能够发现更有效的优化方案:如果没有进行重构,好的优化方案就可能与你失之交臂。如果性能太糟糕,要把临时变量放回去也是很容易的。
重构前
public Double replaceTempWithQuery() {
double basePrice = quantity * itemPrice;
double discountFactor;
if (basePrice > 1000) {
discountFactor = 0.95;
}else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}
重构后
public Double replaceTempWithQuery() {
return getBasePrice() * getDiscountFactor();
}
private double getDiscountFactor() {
if (getBasePrice() > 1000) {
return 0.95;
}
return 0.98;
}
private double getBasePrice() {
return quantity * itemPrice;
}
如果没有把临时变量basePrice替换为一个查询式,将多么难以提炼getDiscountFactor()!
6.5 Introduce Explaining Variable(引入解释性变量)
将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
重构前
if(platform.toUpperCase().indexOf("Mac")>-1)&&(browser.toUpperCase().indexOf("IE")>-1)&&wasInitialized()&&resize>0){
//do soemthing
}
重构后
final boolean isMacOs=platform.toUpperCase().indexOf("Mac")>-1;
final boolean isIEBrowser=browser.toUpperCase().indexOf("IE")>-1;
final boolean wasResized=resize>0;
if(isMacOs&&isIEBrowser&&wasInitialized()&&wasResized){
//do something
}
Introduce Explaining Variable是一种很常见的重构手法,但我并不常用它,我几乎总是尽量使用Extract Method来解释一段代码的意义。 毕竟临时变量只在它所处的那个函数才有意义,局限性较大,函数则可以在对象的整个生命中都有用,并且可被其他对象使用。但有时候,当局部变量是Extract Method难以进行时,我就使用Introduce Explaining Variable。
范例
重构前
double price() {
//price is base price -quantity discount + shipping
return quantity * itemPrice -
Math.max(0, quantity - 500) * itemPrice * 0.05 +
Math.min(quantity * itemPrice * 0.1, 100.0);
}
使用Introduce Explaining Variable重构后
double price() {
//price is base price -quantity discount + shipping
final double basePrice = quantity * itemPrice;
final double quantityDiscount = Math.max(0, quantity - 500) * itemPrice * 0.05;
final double shipping = Math.min(basePrice * 0.1, 100.0);
return basePrice - quantityDiscount + shipping;
}
使用Extract Method 重构后
double price() {
return getBasePrice() - getQuantityDiscount() + getShipping();
}
private double getBasePrice() {
return quantity * itemPrice;
}
private double getShipping() {
return Math.min(getBasePrice() * 0.1, 100.0);
}
private double getQuantityDiscount() {
return Math.max(0, quantity - 500) * itemPrice * 0.05;
}
我比较喜欢Extract Method,因为同一个对象中的任务部分,都可以根据自己的需要提炼出来这些函数。并且工作量不比Introduce Explaining Variable大。
那么啥时候才使用Introduce Explaining Variable呢?
在Extract Method需要花费更大工作量时。
如果我要处理的是一个拥有大量局部变量的算法,那么使用Extract Method绝非易事。在这种情况下就会使用Introduce Explaining Variable来清理代码,然后再考虑下一步该怎么办。搞清楚逻辑之后,我总是可以运用Replace Temp with Query把中间引入的那些解释性临时变量去掉。况且,如果我最终使用Replace Method with Method Object,那么引入的那些解释性变量也有其价值。
6.6 Split Temporary Variable(分解临时变量)
有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。
针对每次赋值,创造一个独立、对应的临时变量。
重构前
double temp=2*(height+width);
System.out.println(temp);
temp=height+width;
System.out.println(temp);
重构后
final double perimeter=2*(height+width);
System.out.println(perimeter);
final double area=height+width;
System.out.println(area);
动机
如果临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,每个变量值承担一个责任。同个临时变量承担两件不同的事情,会令代码阅读者糊涂。
6.7 Remove Assignments to Parameters(移除对参数的赋值)
代码对一个参数进行赋值。
以一个临时变量取代该参数的位置
重构前
int discount(int inputVal,int quantity,int yearToDate){
if(inputVal>50){
inputVal-=2;
}
}
重构后
int discount(int inputVal,int quantity,int yearToDate){
int result=inputVal;
if(inputVal>50){
result-=2;
}
}
6.8 Replace Method with Method Object(以函数对象取代函数)
//todo
6.9 Substitute Algorithm(替换算法)
你想要把某一个算法替换成另一个更清晰的算法。
将函数本体替换为另一个算法
String foundPerson(String[] people){
for(int i=0;i<people.length;i++){
if(people[i].equals("Don")){
return "Don";
}
if(people[i].equals("John")){
return "John";
}
if(people[i].equals("Kent")){
return "Kent";
}
}
return "";
}
替换为
String foundPerson(String[] people){
List candidates=Arrays.asList(new String[] {"Don","John","Kent"});
for(int i=0;i<people.length;i++){
if(candidates.contains(people[i])){
return people[i];
}
}
return "";
}
做法
- 准备好另一个(替换用)算法,让它通过编译
- 针对现有测试,执行上述的新算法。如果结果与原本结果相同,重构结束
- 如果测试结果不同于原先,在测试和调试过程中,以旧算法为标准
第7章 在对象之间搬移特性
在对象的设计过程中,“决定把责任放在哪儿”即使不是最重要的事情,也是最重要的事之一。我使用对象技术已经十多年了,但还是不能一开始就保证做对。
常常我只需要使用Move Method和Move Field简单地移动对象行为,就可以解决这些问题。如果这两个重构手法都需要用到,我会首先使用Move Field,再使用Move Method。
7.1 Move Method(搬移函数)
有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或者被后者调用。
在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
动机
“搬移函数”是重构理论的支柱。如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,我就会搬移函数。 这往往不是容易做出的决定。
7.2 Move Field(搬移字段)
某个字段被其所驻类之外的另一个类更多地用到。
在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段
动机
在类之间移动状态和行为,是重构过程中必不可少的措施。随着系统的发展,你会发现自己需要新的类,并需要将现有的工作责任拖到新的类中。
对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,就考虑搬移这个字段。
7.3 Extract Class(提炼类)
某个类做了应该由两个类做的事。
建立一个新类,将相关的字段和函数从旧类搬移到新类。
7.4 Inline Class(将类内联化)
某个类没有做太多事情
将这个类的所有特性搬移到另一个类中,然后移除原类。
7.5 Hide Delegate(隐藏“委托关系”)
客户通过一个委托类来调用另一个对象。
在服务类上建立客户所需的所有函数,用以隐藏委托关系。
7.6 Remove Midddle Man(移除中间人)
动机
Hide Delegate的代价是:每当客户要使用受委托类的新特性,你就必须在服务端添加一个简单委托函数,随着受托类的特性(功能)越来越多,这一过程会让你痛苦不已.服务类完全变成了一个“中间人”,此时你就应该让客户直接调用受托类.
很难说什么程度的隐藏才是合适的. 还好,有了Hide Delegate 和Remove Middle Man,你大可不必担心这个问题,因为你可以在系统运行过程中不断进行调整.随着系统的变化,“合适的隐藏程度”这个尺度也在相应改变.
7.7 Introduce Foreign Method(引入外加函数)
在客户类中建立一个函数,并以第一参数形式传入一个服务类实例.
重构前
Date newStart=new Date(previousEnd.getYear(),previousEnd.getMonth(),PreviousEnd.getDate()+1);
重构后
Date newStart=nextDay(previousEnd);
private static Date nextDay(Date arg){
return new Date(arg.getYear(),arg.getMonth(),arg.getDate()+1);
}
7.8 Introduce Local Extension(引入本地扩展)
现象
你需要为服务类建立一些额外函数,但你无法修改这个类.
建立一个新类,使它包含这些额外函数.让这个扩展品成为源类的之类或包装类.
第8章 重新组织数据
8.1 Self Encapsulate Field(自封装字段)
为这个字段建立取值/设值函数,并且只以这些函数来访问字段.
(就是简单的getter方法)
在"字段访问方式"这个问题上,存在两种截然不同的观点:其中一派认为,在该变量定义所在的类中,你可以自由访问它;另一派认为,即使这个类中你也应该只使用访问函数间接访问.
8.2 Replace Data Value with Object(以对象取代数据值)
你有一个数据项,需要与其他数据和行为一起使用才有意义.
开发初期,你往往决定以简单的数据项,表示简单的情况.但是,随着开发的进行,你可能会发现,这些简单数据项不再那么简单了.比如说,一开始你可能会用一个字符串来表示“电话号码”概念,但是随后你就会发现,电话号码需要“格式化”,“抽取区号”之类的特殊行为.
如果这样的数据项只有一两个,你还可以把相关函数放进数据项所属的对象里;但是Duplicate Code坏味道和Feature Envy坏味道很快就会从代码中散发出来.当这些坏味道开始出现,你就应该将数据值变成对象.
8.3 Change Value to Reference(将值对象改为引用对象)
在很多系统中,你都可以对对象做一个有用的分类:引用对象和值对象.前者就是“客户”,“账户”这样的东西,每个对象都代表真实世界中的一个实物,你可以直接以相等操作符(==,用来检验对象同一性)检查两个对象是否相等.后者则是像“日期”,“钱”这样的东西,它们完全由其所含的数据值来定义,你并不在意副本的存在,系统中或许存在成百上千个内容为“1/1/2000”的“日期”对象.
要在引用对象和值对象之间做选择有时并不容易.
8.4 Change Reference to Value(将引用对象改为值对象)
你有一个引用对象,很小且不可变,而且不易管理.
将它变成一个值对象.
要在引用对象和值对象之间做选择,有时并不容易.作出选择后,你通常会需要一条回头路.
如果引用对象开始变得难以使用,也许就应该将它改为值对象.引用对象必须被某种方式控制,你总是必须向其控制者请求适当的引用对象.
在分布式系统和并发系统中,不可变的值对象特别有用,因为你无须考虑它们的同步问题.
值对象有一个非常重要的特性:它们应该是不可变的.无论何时,只要你调用同一对象的同一查询函数,都应该得到同样结果.如果保证了这一点,就可以放心地以多个对象表示同一个事物.如果值对象是可变的,你就必须确保对某个对象的修改会自动更新其他“代表相同事物”的对象.这太痛苦了,与其如此还不如把它变成引用对象.
“不可变”的意思:如果你以Money类代表“钱”的概念,其中有“币种”和“金额”两条信息,那么Money对象通常是一个不可变的值对象.这并非意味你的薪资不能改变,而是意味:如果要改变你的薪资,就需要使用另一个Money对象来取代现有的Money对象,而不是在现有的Money对象上修改.你和Money对象之间的关系可与改变,但Money对象自身不能改变.
8.5 Replace Array with Object(以对象取代数组)
你有一个数组,其中的元素各自代表不同的东西.
以对象替换数组,对于数组中的每个元素,以某一个字段来表示