在写重构的学习笔记之前,首先我们需要向伟大的软件设计师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; } }
通过我们一小步一小步的重构,让我们的程序更加优美,适应变化性更强。