您的位置:首页 > 编程语言

重构:改善既有代码的设计

2019-07-01 00:37 190 查看

第一个案例:

重构的第一步:为即将改变的代码建立一组可靠的测试环境。

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.以类取代类型码

 

(未完待续)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  IntelliJ IDEA