在写重构的学习笔记之前,首先我们需要向伟大的软件设计师Martin Fowler致敬,是他带给了我们重构的思想,敏捷的思想。
重构--改善既有代码的设计。意味着对现有运行中的代码进行新的修改、设计。这对很多项目经理来说是不可思议的,因为他们一直奉行的是软件业的一句经典“如果代码可以运行,就不要去修改它”在这条“真理”的引导下,当出现新的功能,新的BUG的时候,后续的程序员总是在原有的基础上修修补补,导致代码越来越庞大,业务逻辑越来越不明了,到最后维护的人员终于看不懂代码逻辑了,程序员开始抓狂了,白头发开始白了,职业病来了,项目死了。曾经在CSDN上流传着这样几个关于代码注释的笑话。1. //这段代码的实现逻辑,作为开发者的我已经不知道为什么这样设计了,请不要试图去理解这段代码并去修改它 2.//如果你试图修改这段代码,但却导致了系统其他地方的BUG,请在下面的计数器上加一,以提醒下一位程序员不要动试图去修改它的念头。
什么时候我们的代码需要重构了?
我在看一本UI设计书《写给大家看的设计书》中提到,要学会设计其实很简单,主要是掌握3把斧! a. 你需要知道哪里需要修改 b. 你需要知道该怎么样去修改 c. 实践、动手去修改它。我们学习并利用重构也是一样,首先你的知道代码中的坏味道,其实你的知道怎样去掉这些坏味道,最后动手去修改它。
首先我们通过一个简单的例子来给大家分享重构的过程和乐趣。题目是这样的:这是一个影片出租店德程序,计算每一位顾客的消费金额并打印详单。操作者告诉程序,顾客租了那些影片,租期多长,程序根据租期多长以及影片的类型算出费用。影片分为三类:普通片、儿童片、新片。除了计算费用外,还要为顾客计算积分,不同类型的积分不同。
先看一个不优秀的代码设计: 依据题意,我们定义3个类Customer(顾客) Rental(租赁) Movice(电影)
Customer 类
1: public class Customer {
2:
3: /** 顾客的姓名 **/
4: private String name;
5:
6: public String getName() {
7: return name;
8: }
9:
10: /** 租赁的所有影片和租期 **/
11: private List rentals = new ArrayList();
12:
13: public Customer(String _name){
14: name = _name;
15: }
16:
17: /** 添加租赁影片的租赁关系 **/
18: public void addMovice(Rental _rental){
19: rentals.add(_rental);
20: }
21:
22: /** 生成账单 **/
23: public void createBill(){
24:
25: double totalAmount = 0;
26: int renterPoint = 0;
27: String billInfo = "";
28:
29: for (Rental _rental : rentals) {
30: double thisAmount = 0;
31: int thisPoint = 0;
32: int type = _rental.getMovice().getType();
33:
34: switch (type) {
35: case Movice.CHILDREN:
36: thisAmount += 2;
37: if(_rental.getDaysRental() > 2){
38: thisAmount += (_rental.getDaysRental() -2) * 1.5;
39: }
40: break;
41: case Movice.NEW:
42: thisAmount += _rental.getDaysRental() * 3;
43: break;
44: case Movice.NORMAL:
45: thisAmount += 1.5;
46: if(_rental.getDaysRental() > 3){
47: thisAmount += (_rental.getDaysRental() - 3) * 1.5;
48: }
49: break;
50: default:
51: break;
52: }
53:
54: thisPoint ++;
55: if(type == Movice.NEW && _rental.getDaysRental() > 1){
56: thisPoint ++;
57: }
58:
59: totalAmount += thisAmount;
60: renterPoint += thisPoint;
61: billInfo += "书名:" + _rental.getMovice().getName() + "/t" +
62: "价格:" + thisAmount + "/t" +
63: "积分:" + thisPoint + "/t" +
64: "天数:" + _rental.getDaysRental() + "/n";
65: }
66:
67: billInfo += "本次总价:" + totalAmount + "/n" +
68: "本次积分:" + renterPoint;
69:
70: System.out.println(billInfo);
71: }
72:
73: }
Rental 类
public class Rental {
/** 租赁的影片 **/
private Movice movice;
/** 影片的租期 **/
private int daysRental;
public Rental(Movice _movice,int _daysRental){
movice = _movice;
daysRental = _daysRental;
}
public Movice getMovice() {
return movice;
}
public int getDaysRental() {
return daysRental;
}
}
Movic类
public class Movice {
public static final int NORMAL = 0;
public static final int CHILDREN = 1;
public static final int NEW = 2;
/** 影片的名称 **/
private String name;
/** 影片的类型**/
private int type;
public String getName() {
return name;
}
public int getType() {
return type;
}
public Movice(int _type,String _name) {
name = _name;
type = _type;
}
}
朋友们,从上面的代码,你们找到了那些代码的坏味道了?
1. Duplicated Code (重复代码): 单我需要创建另外一种账单的打印方式:比如按照XML的格式打印时候,我需要另外写一个函数,然后重复前面获取租赁电影的价钱和积分。
2. Long Method (过长的方法) : Customer类的createBill 功能不单一,方法过长
3. Customer类过多的魔鬼数字和字符,导致后续的字符和参数的替换不方便
4. Switch Statements (Switch 惊悚现身): Customer通过Switch来判断影片的类型,随着影片的类型增多,Switch的判断必然增多
5. 发散式变化 :单我的影片价格调整,积分调整的时候,我需要在Customer生成不同账单的函数中去修改。
6. 依赖情节 : 这是一种“讲数据和对数据的操作行为包装在一起的技术”,有一种经典的气味是:函数对某个类的兴趣高过对自己所处的类的兴趣。
7. 语法错误 : 代码语法的漏洞
如果你能发现以上的代码坏味道,甚至更多,那恭喜你,你已经开始进入了重构的大门。接下来我们通过重构来一步步优化代码。请记住:重构代码讲究一小步一小步的修改,测试。不要一开始就对整个结构进行调整,修改。
A. 通过分析代码的坏味道,我们发现第3点:魔鬼数字是最好修改的。替换Customer类中的魔鬼数字得到新的Customer类为:红色部分是我们添加的常量定义,替换到魔鬼数字和字符
1: public class Customer01 {
2: private String name;
3: private static final String BOOKNAME_STRING = "书名:";
4: private static final String PRICE_STRING = "价格:";
5: private static final String POINT_STRING = "积分:";
6: private static final String DAY_STRING = "天数";
7: private static final String TOTLEAMOUNT_STRING = "总价格:";
8: private static final String TOTLEPOINT_STRING = "总积分";
9: private static final String CHAT_T_STRING = "/t";
10: private static final String CHAT_N_STRING = "/n";
11:
12: private static final int MOVICE_CHILDREN_PRICE = 2;
/** 儿童片租赁后可以使用的天数 **/
13: private static final int MOVICE_CHILDREN_DEADLINE = 2;
/** 超过租赁天数后,应付的价钱 **/
14: private static final double MOVICE_CHILDREN_DELAY_PRICE = 1.5;
15:
16: private static final int MOVICE_NEW_PRICE = 3;
17:
18: private static final double MOVICE_NORMAL_PRICE = 1.5;
19: private static final int MOVICE_NORMAL_DEADLINE = 3;
20: private static final double MOVICE_NORMAL_DEALY_PRICE = 1.5;
21:
22: private static final int POINT_ADD_MIN_DAY = 1;
23:
24: public String getName() {
25: return name;
26: }
27:
28: private List rentals = new ArrayList();
29:
30: public Customer01(String _name){
31: name = _name;
32: }
33:
34: public void addMovice(Rental _rental){
35: rentals.add(_rental);
36: }
37:
38:
39: public void createBill(){
40:
41: double totalAmount = 0;
42: int renterPoint = 0;
43: StringBuffer billInfo = new StringBuffer();
44:
45: for (Rental _rental : rentals) {
46: double thisAmount = 0;
47: int thisPoint = 0;
48: int type = _rental.getMovice().getType();
49:
50: switch (type) {
51: case Movice.CHILDREN:
52: thisAmount += MOVICE_CHILDREN_PRICE;
53: if(_rental.getDaysRental() > MOVICE_CHILDREN_DEADLINE){
54: thisAmount += (_rental.getDaysRental() - MOVICE_CHILDREN_DEADLINE) * MOVICE_CHILDREN_DELAY_PRICE;
55: }
56: break;
57: case Movice.NEW:
58: thisAmount += _rental.getDaysRental() * MOVICE_NEW_PRICE;
59: break;
60: case Movice.NORMAL:
61: thisAmount += MOVICE_NORMAL_PRICE;
62: if(_rental.getDaysRental() > MOVICE_NORMAL_DEADLINE){
63: thisAmount += (_rental.getDaysRental() - MOVICE_NORMAL_DEADLINE) * MOVICE_NORMAL_DEALY_PRICE;
64: }
65: break;
66: default:
67: break;
68: }
69:
70: thisPoint ++;
71: if(type == Movice.NEW && _rental.getDaysRental() > POINT_ADD_MIN_DAY){
72: thisPoint ++;
73: }
74:
75: totalAmount += thisAmount;
76: renterPoint += thisPoint;
77:
78: billInfo.append(BOOKNAME_STRING + _rental.getMovice().getName() + CHAT_T_STRING);
79: billInfo.append(PRICE_STRING + thisAmount + CHAT_T_STRING);
80: billInfo.append(POINT_STRING + thisPoint + CHAT_T_STRING);
81: billInfo.append(DAY_STRING + _rental.getDaysRental() + CHAT_N_STRING);
82:
83: }
84:
85: billInfo.append(TOTLEAMOUNT_STRING + totalAmount + CHAT_N_STRING);
86: billInfo.append(TOTLEPOINT_STRING + renterPoint + CHAT_N_STRING);
87: System.out.println(billInfo);
88: }
89: }
B. 过长的方法:我们发现Customer类的createBill()方法过长,通过分析该方法后,我们发现该方法主要做了以下几件事情:1. 依次获得单个租赁碟片的价格 2. 依次获得单个碟片的积分 3. 按规程生成账单 因此我们通过抽取业务逻辑形成方法的方式修改Customer类的createBill()方法,同时我们发现String使用的错误,当添加多个字符串的时候,需要使用StringBuffer。结果如下:红色部分为修改的代码
1: public class Customer02 {
2: private String name;
3: private static final String BOOKNAME_STRING = "书名:";
4: private static final String PRICE_STRING = "价格:";
5: private static final String POINT_STRING = "积分:";
6: private static final String DAY_STRING = "天数";
7: private static final String TOTLEAMOUNT_STRING = "总价格:";
8: private static final String TOTLEPOINT_STRING = "总积分";
9: private static final String CHAT_T_STRING = "/t";
10: private static final String CHAT_N_STRING = "/n";
11:
12: private static final int MOVICE_CHILDREN_PRICE = 2;
13: private static final int MOVICE_CHILDREN_DEADLINE = 2;
14: private static final double MOVICE_CHILDREN_DELAY_PRICE = 1.5;
15:
16: private static final int MOVICE_NEW_PRICE = 3;
17:
18: private static final double MOVICE_NORMAL_PRICE = 1.5;
19: private static final int MOVICE_NORMAL_DEADLINE = 3;
20: private static final double MOVICE_NORMAL_DEALY_PRICE = 1.5;
21:
22: private static final int POINT_ADD_MIN_DAY = 1;
23:
24: public String getName() {
25: return name;
26: }
27:
28: private List rentals = new ArrayList();
29:
30: public Customer02(String _name){
31: name = _name;
32: }
33:
34: public void addMovice(Rental _rental){
35: rentals.add(_rental);
36: }
37:
38: /**
39: * <获得用户租赁的碟片的价格和积分,生成账单>
40: * <1. 获得单个租赁碟片的价格 >
41: * <2. 获得单个碟片的积分 >
42: * <3. 按规程生成账单>
43: */
44:
45: private StringBuffer billInfo = new StringBuffer();
46:
47: public void createBill(){
48:
49: double totalAmount = 0;
50: int renterPoint = 0;
51:
52:
53: for (Rental _rental : rentals) {
54:
55: totalAmount += getRentalPrice(_rental);
56: renterPoint += getRentalPoint(_rental);;
57: createSingleBill(_rental);
58: }
59:
60: addStatistics(totalAmount,renterPoint);
61:
62: }
63:
64: private double getRentalPrice(Rental _rental){
65: int type = _rental.getMovice().getType();
66: double thisAmount = 0;
67:
68: switch (type) {
69: case Movice.CHILDREN:
70: thisAmount += MOVICE_CHILDREN_PRICE;
71: if(_rental.getDaysRental() > MOVICE_CHILDREN_DEADLINE){
72: thisAmount += (_rental.getDaysRental() - MOVICE_CHILDREN_DEADLINE) * MOVICE_CHILDREN_DELAY_PRICE;
73: }
74: break;
75: case Movice.NEW:
76: thisAmount += _rental.getDaysRental() * MOVICE_NEW_PRICE;
77: break;
78: case Movice.NORMAL:
79: thisAmount += MOVICE_NORMAL_PRICE;
80: if(_rental.getDaysRental() > MOVICE_NORMAL_DEADLINE){
81: thisAmount += (_rental.getDaysRental() - MOVICE_NORMAL_DEADLINE) * MOVICE_NORMAL_DEALY_PRICE;
82: }
83: break;
84: default:
85: break;
86: }
87: return thisAmount;
88: }
89:
90: private int getRentalPoint(Rental _rental){
91: int thisPoint = 0;
92: thisPoint ++;
93: if(_rental.getMovice().getType() == Movice.NEW && _rental.getDaysRental() > POINT_ADD_MIN_DAY){
94: thisPoint ++;
95: }
96: return thisPoint;
97: }
98:
99: private void createSingleBill(Rental _rental){
100: billInfo.append(BOOKNAME_STRING + _rental.getMovice().getName() + CHAT_T_STRING);
101: billInfo.append(PRICE_STRING + getRentalPrice(_rental) + CHAT_T_STRING);
102: billInfo.append(POINT_STRING + getRentalPoint(_rental) + CHAT_T_STRING);
103: billInfo.append(DAY_STRING + _rental.getDaysRental() + CHAT_N_STRING);
104: }
105:
106: private void addStatistics(double totalAmount,int renterPoint){
107: billInfo.append(TOTLEAMOUNT_STRING + totalAmount + CHAT_N_STRING);
108: billInfo.append(TOTLEPOINT_STRING + renterPoint + CHAT_N_STRING);
109: System.out.println(billInfo);
110: }
111:
112: }
C. 依赖情节,我们发现getRentalPrice(),getRentalPoint()都和租赁有关,和顾客没有关系,因为我们需要把其移到对应的类中去,修改为Customer类以及Rental类为:
public class Customer03 {
private String name;
private static final String BOOKNAME_STRING = "书名:";
private static final String PRICE_STRING = "价格:";
private static final String POINT_STRING = "积分:";
private static final String DAY_STRING = "天数";
private static final String TOTLEAMOUNT_STRING = "总价格:";
private static final String TOTLEPOINT_STRING = "总积分";
private static final String CHAT_T_STRING = "/t";
private static final String CHAT_N_STRING = "/n";
public String getName() {
return name;
}
private List rentals = new ArrayList();
public Customer03(String _name){
name = _name;
}
public void addMovice(Rental03 _rental03){
rentals.add(_rental03);
}
/**
* <获得用户租赁的碟片的价格和积分,生成账单>
* <1. 获得单个租赁碟片的价格 >
* <2. 获得单个碟片的积分 >
* <3. 按规程生成账单>
*/
private StringBuffer billInfo = new StringBuffer();
public void createBill(){
double totalAmount = 0;
int renterPoint = 0;
for (Rental03 _rental : rentals) {
totalAmount += _rental.getRentalPrice();
renterPoint += _rental.getRentalPoint();;
createSingleBill(_rental);
}
addStatistics(totalAmount,renterPoint);
}
private void createSingleBill(Rental03 _rental){
billInfo.append(BOOKNAME_STRING + _rental.getMovice().getName() + CHAT_T_STRING);
billInfo.append(PRICE_STRING + _rental.getRentalPrice() + CHAT_T_STRING);
billInfo.append(POINT_STRING + _rental.getRentalPoint() + CHAT_T_STRING);
billInfo.append(DAY_STRING + _rental.getDaysRental() + CHAT_N_STRING);
}
private void addStatistics(double totalAmount,int renterPoint){
billInfo.append(TOTLEAMOUNT_STRING + totalAmount + CHAT_N_STRING);
billInfo.append(TOTLEPOINT_STRING + renterPoint + CHAT_N_STRING);
System.out.println(billInfo);
}
}
Rental 类修改:
public class Rental03 {
private Movice movice;
private int daysRental;
private static final int MOVICE_CHILDREN_PRICE = 2;
private static final int MOVICE_CHILDREN_DEADLINE = 2;
private static final double MOVICE_CHILDREN_DELAY_PRICE = 1.5;
private static final int MOVICE_NEW_PRICE = 3;
private static final double MOVICE_NORMAL_PRICE = 1.5;
private static final int MOVICE_NORMAL_DEADLINE = 3;
private static final double MOVICE_NORMAL_DEALY_PRICE = 1.5;
private static final int POINT_ADD_MIN_DAY = 1;
public Rental03(Movice _movice,int _daysRental){
movice = _movice;
daysRental = _daysRental;
}
public Movice getMovice() {
return movice;
}
public int getDaysRental() {
return daysRental;
}
public double getRentalPrice(){
int type = getMovice().getType();
double thisAmount = 0;
switch (type) {
case Movice.CHILDREN:
thisAmount += MOVICE_CHILDREN_PRICE;
if(getDaysRental() > MOVICE_CHILDREN_DEADLINE){
thisAmount += (getDaysRental() - MOVICE_CHILDREN_DEADLINE) * MOVICE_CHILDREN_DELAY_PRICE;
}
break;
case Movice.NEW:
thisAmount += getDaysRental() * MOVICE_NEW_PRICE;
break;
case Movice.NORMAL:
thisAmount += MOVICE_NORMAL_PRICE;
if(getDaysRental() > MOVICE_NORMAL_DEADLINE){
thisAmount += (getDaysRental() - MOVICE_NORMAL_DEADLINE) * MOVICE_NORMAL_DEALY_PRICE;
}
break;
default:
break;
}
return thisAmount;
}
public int getRentalPoint(){
int thisPoint = 0;
thisPoint ++;
if(getMovice().getType() == Movice.NEW && getDaysRental() > POINT_ADD_MIN_DAY){
thisPoint ++;
}
return thisPoint;
}
}
D. 我们发现Rental类的getRentalPrice() 跟影片的类型和影片的价格有关,因此其更应该放到Movice类里面去,修改Rental类和Movice类为。通过迁移,租赁价格的修改都集中到了Movice类中。
public class Rental05 {
private Movice05 movice;
private int daysRental;
private static final int POINT_ADD_MIN_DAY = 1;
public Rental05(Movice05 _movice,int _daysRental){
movice = _movice;
daysRental = _daysRental;
}
public Movice05 getMovice() {
return movice;
}
public int getDaysRental() {
return daysRental;
}
public double getRentalPrice(){
return getMovice().getTotalPrice(getDaysRental());
}
public int getRentalPoint(){
int thisPoint = 0;
thisPoint ++;
if(getMovice().getType() == Movice.NEW && getDaysRental() > POINT_ADD_MIN_DAY){
thisPoint ++;
}
return thisPoint;
}
}
Movice类
public class Movice05 {
public static final int NORMAL = 0;
public static final int CHILDREN = 1;
public static final int NEW = 2;
private static final int MOVICE_CHILDREN_PRICE = 2;
private static final int MOVICE_NEW_PRICE = 3;
private static final double MOVICE_NORMAL_PRICE = 1.5;
private static final int MOVICE_CHILDREN_DEADLINE = 2;
private static final double MOVICE_CHILDREN_DELAY_PRICE = 1.5;
private static final int MOVICE_NORMAL_DEADLINE = 3;
private static final double MOVICE_NORMAL_DEALY_PRICE = 1.5;
private String name = "";
private int type = 0;
private int thisAmount = 0;
public int getTotalPrice(int daysRental) {
if(getType() == CHILDREN){
thisAmount += MOVICE_CHILDREN_PRICE;
if(daysRental > MOVICE_CHILDREN_DEADLINE){
thisAmount += (daysRental - MOVICE_CHILDREN_DEADLINE) * MOVICE_CHILDREN_DELAY_PRICE;
}
}else if(getType() == NORMAL){
thisAmount += MOVICE_NORMAL_PRICE ;
if(daysRental > MOVICE_NORMAL_DEADLINE){
thisAmount += (daysRental - MOVICE_NORMAL_DEADLINE) * MOVICE_NORMAL_DEALY_PRICE;
}
}else if(getType() == NEW){
thisAmount += MOVICE_NEW_PRICE * daysRental;
}
return thisAmount;
}
public String getName() {
return name;
}
public int getType() {
return type;
}
public Movice05(int _type,String _name) {
name = _name;
type = _type;
}
}
D. 到这里,我们发现惊悚的Switch类还没有处理掉。通过我们分析Switch主要是对不同的电影类型进行不同的处理,因此我们可以考虑抽取一个超级的电影类,不同的电影类型继承该类来解决Switch的问题。
public abstract class MoviceSuper {
/** 影片的价格 **/
public int price ;
/** 影片的积分 **/
public int point;
/** 一步影片可以租多少天 **/
public int rentalFreeDays;
/** 超过租期了付的价钱**/
public double delayDayPrice ;
/** 电影的名称**/
public String name;
public String getName() {
return name;
}
public MoviceSuper(String _name){
name = _name;
}
/**
* <获得租赁影片的价钱>
* <总价格 = 单个影片的价格 + 延迟时每天应付的价格>
*
* @param daysRental :租赁的天数
* @return
*/
public abstract double getRentalPrice(int daysRental);
public abstract int getRentalPoint();
}
package com.chapter01;
public class MoviceChild extends MoviceSuper{
private static final int POINT = 1;
private static final int PRICE = 2;
private static final int DEADLINE = 2;
private static final double DELAY_PRICE = 1.5;
public MoviceChild(String name) {
super(name);
// TODO Auto-generated constructor stub
price = PRICE;
rentalFreeDays = DEADLINE;
delayDayPrice = DELAY_PRICE;
point = POINT;
}
@Override
public int getRentalPoint() {
// TODO Auto-generated method stub
return point;
}
@Override
public double getRentalPrice(int daysRental) {
// TODO Auto-generated method stub
double thisAmount = 0;
thisAmount += price;
if(daysRental > rentalFreeDays){
thisAmount += (daysRental - rentalFreeDays) * delayDayPrice;
}
return thisAmount;
}
}
package com.chapter01;
public class MoviceNew extends MoviceSuper{
private static final int PRICE = 3;
private static final int POINT = 2;
public MoviceNew(String name) {
super(name);
price = PRICE;
delayDayPrice = PRICE;
rentalFreeDays = 0;
point = POINT;
}
@Override
public int getRentalPoint() {
// TODO Auto-generated method stub
return point;
}
@Override
public double getRentalPrice(int daysRental) {
// TODO Auto-generated method stub
return daysRental * price;
}
}
package com.chapter01;
public class MoviceNormal extends MoviceSuper{
private static final int PRICE = 2;
private static final int DEADLINE = 3;
private static final double DEALY_PRICE = 1.5;
private static final int POINT = 1;
public MoviceNormal(String name) {
super(name);
price = PRICE;
delayDayPrice = DEALY_PRICE;
rentalFreeDays = DEADLINE;
point = POINT;
}
@Override
public int getRentalPoint() {
// TODO Auto-generated method stub
return point;
}
@Override
public double getRentalPrice(int daysRental) {
double thisAmount = 0;
thisAmount += price ;
if(daysRental > price){
thisAmount += (daysRental - price) * delayDayPrice;
}
return thisAmount;
}
}
通过我们一小步一小步的重构,让我们的程序更加优美,适应变化性更强。