1.1 起点
实例
名称:影片出租店程序
功能:计算每位顾客的消费金额并打印详单。
需求:输入信息(哪位顾客租了哪些影片、租期多长),然后让程序计算费用。影片分为普通片、儿童片、新片。除计算费用外,还要为常客计算积分,积分根据是否是新片有所不同。
UML类图如下:
/'在线作图(UML)网址:
http://www.plantuml.com/plantuml/uml/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000
如果要修改的的话,打开网址后,直接复制上图片链接(或者粘贴下方代码)修改即可'/
@startuml
Title "影片出租店程序"
class Movie{
- private int priceCode
}
class Rental{
- private int daysRented
}
class Customer{
+ public statement()
}
Rental --> Movie
Customer --> Rental
@enduml
尽可能少篇幅地将程序写在这里
class Movie://影片
static final CHILDRENS = 2//儿童片
static final REGULAR = 0//普通片
static final NEWRELEASE = 1//新片
String title
int priceCode
Movie(title, priceCode){...}//带有全参数的构造器
(title,priceCode) => getter,setter//分别title和priceCode的getter和setter方法
class Rental://租赁
Movie movie
int dayRented
Rental(movie, dayRented){...}
(movie,dayRented) => getter,setter
class Customer://顾客
String name
rentals = Vector<Rental>()
Customer(name, rentals){...}
(name,rentals) => getter,setter
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Iterator<Rental> rentalIter = rentals.iterator();
String result = "Rental Record for " + getName() + "\n";
while (rentalIter.hasNext()) {
double thisAmount = 0;
Rental each = rentalIter.next();
// determine amounts for each line(确定每行的金额)
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR :
thisAmount += 2;
if (each.getDayRented() > 2)
thisAmount += (each.getDayRented() - 2) * 1.5;
break;
case Movie.NEWRELEASE :
thisAmount += each.getDayRented() * 3;
break;
case Movie.CHILDRENS :
thisAmount += 1.5;
if (each.getDayRented() > 3)
thisAmount += (each.getDayRented() - 3) * 1.5;
break;
default :
break;
}
// add frequent renter points(增加常客的积分)
frequentRenterPoints++;
// add bonus for a two day new release rental(为两天的新释放租金增加奖金)
// 积分根据是否是新片有所不同
if ((each.getMovie().getPriceCode() == Movie.NEWRELEASE)
&& each.getDayRented() > 1)
frequentRenterPoints++;
// show figures for the 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;
}
分析:在Customer类中,statement()方法过长,内部业务过多,不符合面向对象思想。
假设有如下几点需求。
序号 | 需求 | 措施 |
---|---|---|
1 | 以HTML格式输出详单 | 阅读statement方法发现,无法复用,需要重新写一个专门生成html详单的方法htmlStatement() 此功能而言,仅需复制旧方法更改即可 |
2 | 计费标准改变 | 需求1+需求2,需要同时更改statement()和htmlStatement()两处中的计费标准 |
3 | 影片分类规则更改,具体改变方法据情况而定 | 很显然,这个需求仅仅代表着,暂时这样,将来一定会改 |
在需求1的前提下,当业务越来越复杂时,statement()和htmlStatement()方法需要保持一致,代码会越来越臃肿,也越来越容易出现问题
什么时候需要重构?
当你发现,当新需求出现时,原先的代码结构不能很好的适应,这个时候,就需要先考虑怎么将原结构优化(重构),然后再考虑加入新需求。
1.2 重构的第一步
建立测试环境:测试环境是为了保障在重构过程中,不会使原程序出现大的BUG(无论重构方法再怎么先进,总会有出现BUG的可能性,可靠的测试能够尽可能避免这种情况发生),这是一个安全锁。
搭建测试环境思路:
影片出租店程序中的statement方法,最终输出结果是字符串,其内部不会发生其他的变化,所以,在测试环境中,需要保证重构前的代码与重构后的代码输出字符串相同。所以,在搭建的测试环境中,需要有一个比对字符串的方法(告诉你哪行不一致)。
1.3 分解并重组
对于statement()方法而言,过于臃肿,需要进行分解。
代码块越小,代码功能就越容易管理,处理和移动更轻松。所以我们需要将statement()进行分解。
下面将逐步进行分解。
1.3.1 提炼函数(6.1 Extract Method)
找出代码中的整块部分(逻辑泥团),提炼成一个独立函数。
提炼函数(6.1 Extract Method):详细内容在第六章第一节。此处简单介绍:
把大型函数提出来一部分变成小型函数,需要注意:小型函数的命名(以它‘做什么’来命名)、检查变量作用域(关键)、处理临时变量、编译测试。
在statement方法中,switch语句是一个明显的逻辑泥团,可以尝试提炼出来。
首先找到函数中的局部变量和参数:each和thisAmount,前者仅被使用,后者会被修改。
任何不会被修改的变量可以直接当作参数传入;如果只有一个变量被修改,可以把它当作返回值。
thisAmount是个临时变量,switch语句前的初始值始终为0,所以可以直接在提炼函数中把返回值赋给它。
// 要被提炼的代码判断
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR :
thisAmount += 2;
if (each.getDayRented() > 2)
thisAmount += (each.getDayRented() - 2) * 1.5;
break;
case Movie.NEWRELEASE :
thisAmount += each.getDayRented() * 3;
break;
case Movie.CHILDRENS :
thisAmount += 1.5;
if (each.getDayRented() > 3)
thisAmount += (each.getDayRented() - 3) * 1.5;
default :
break;
}
// 以下代码均位于Customer类中
// 提炼后的代码
thisAmount = this.amountFor(each);
...
// 提炼函数名为amountFor
/**
* 确定出租的金额
*
* @author newre
* @param rental
* @return
*/
private double amountFor(Rental rental) {
// 在重构时,对变量名修改更易于表达提炼函数的功能
double result = 0;
switch (rental.getMovie().getPriceCode()) {
case Movie.REGULAR :
result += 2;
if (rental.getDayRented() > 2)
result += (rental.getDayRented() - 2) * 1.5;
break;
case Movie.NEWRELEASE :
result += rental.getDayRented() * 3;
break;
case Movie.CHILDRENS :
result += 1.5;
if (rental.getDayRented() > 3)
result += (rental.getDayRented() - 3) * 1.5;
break;
default :
break;
}
return result;
}
需要注意的一点是:重构需要一步一步来,每次修改的幅度不能过大,这样才能保证,当你犯下错误时,很容易便可锁定问题的位置。 )