您的位置:首页 > 其它

重构:第一个案例

2011-06-07 15:07 591 查看
在写重构的学习笔记之前,首先我们需要向伟大的软件设计师MartinFowler致敬,是他带给了我们重构的思想,敏捷的思想。

重构--改善既有代码的设计。意味着对现有运行中的代码进行新的修改、设计。这对很多项目经理来说是不可思议的,因为他们一直奉行的是软件业的一句经典“如果代码可以运行,就不要去修改它”在这条“真理”的引导下,当出现新的功能,新的BUG的时候,后续的程序员总是在原有的基础上修修补补,导致代码越来越庞大,业务逻辑越来越不明了,到最后维护的人员终于看不懂代码逻辑了,程序员开始抓狂了,白头发开始白了,职业病来了,项目死了。曾经在CSDN上流传着这样几个关于代码注释的笑话。1.//这段代码的实现逻辑,作为开发者的我已经不知道为什么这样设计了,请不要试图去理解这段代码并去修改它2.//如果你试图修改这段代码,但却导致了系统其他地方的BUG,请在下面的计数器上加一,以提醒下一位程序员不要动试图去修改它的念头。

什么时候我们的代码需要重构了?

我在看一本UI设计书《写给大家看的设计书》中提到,要学会设计其实很简单,主要是掌握3把斧!a.你需要知道哪里需要修改b.你需要知道该怎么样去修改c.实践、动手去修改它。我们学习并利用重构也是一样,首先你的知道代码中的坏味道,其实你的知道怎样去掉这些坏味道,最后动手去修改它。

首先我们通过一个简单的例子来给大家分享重构的过程和乐趣。题目是这样的:这是一个影片出租店德程序,计算每一位顾客的消费金额并打印详单。操作者告诉程序,顾客租了那些影片,租期多长,程序根据租期多长以及影片的类型算出费用。影片分为三类:普通片、儿童片、新片。除了计算费用外,还要为顾客计算积分,不同类型的积分不同。

先看一个不优秀的代码设计:依据题意,我们定义3个类Customer(顾客)Rental(租赁)Movice(电影)









Customer类

publicclassCustomer{

[code]
/**顾客的姓名**/

privateStringname;


publicStringgetName(){

returnname;

}


/**租赁的所有影片和租期**/

privateListrentals=newArrayList();


publicCustomer(String_name){

name=_name;

}


/**添加租赁影片的租赁关系**/

publicvoidaddMovice(Rental_rental){

rentals.add(_rental);

}


/**生成账单**/

publicvoidcreateBill(){


doubletotalAmount=0;

intrenterPoint=0;

StringbillInfo="";


for(Rental_rental:rentals){

doublethisAmount=0;

intthisPoint=0;

inttype=_rental.getMovice().getType();


switch(type){

caseMovice.CHILDREN:

thisAmount+=2;

if(_rental.getDaysRental()>2){

thisAmount+=(_rental.getDaysRental()-2)*1.5;

}

break;

caseMovice.NEW:

thisAmount+=_rental.getDaysRental()*3;

break;

caseMovice.NORMAL:

thisAmount+=1.5;

if(_rental.getDaysRental()>3){

thisAmount+=(_rental.getDaysRental()-3)*1.5;

}

break;

default:

break;

}


thisPoint++;

if(type==Movice.NEW&&_rental.getDaysRental()>1){

thisPoint++;

}


totalAmount+=thisAmount;

renterPoint+=thisPoint;

billInfo+="书名:"+_rental.getMovice().getName()+"/t"+

"价格:"+thisAmount+"/t"+

"积分:"+thisPoint+"/t"+

"天数:"+_rental.getDaysRental()+"/n";

}


billInfo+="本次总价:"+totalAmount+"/n"+

"本次积分:"+renterPoint;


System.out.println(billInfo);

}


}

[/code]

Rental类


publicclassRental{

/**租赁的影片**/
privateMovicemovice;

/**影片的租期**/
privateintdaysRental;

publicRental(Movice_movice,int_daysRental){
movice=_movice;
daysRental=_daysRental;
}

publicMovicegetMovice(){
returnmovice;
}

publicintgetDaysRental(){
returndaysRental;
}
}

Movic类

publicclassMovice{

publicstaticfinalintNORMAL=0;
publicstaticfinalintCHILDREN=1;
publicstaticfinalintNEW=2;

/**影片的名称**/
privateStringname;

/**影片的类型**/
privateinttype;

publicStringgetName(){
returnname;
}

publicintgetType(){
returntype;
}

publicMovice(int_type,String_name){
name=_name;
type=_type;
}
}


朋友们,从上面的代码,你们找到了那些代码的坏味道了?

1.DuplicatedCode(重复代码):单我需要创建另外一种账单的打印方式:比如按照XML的格式打印时候,我需要另外写一个函数,然后重复前面获取租赁电影的价钱和积分。

2.LongMethod(过长的方法):Customer类的createBill功能不单一,方法过长

3.Customer类过多的魔鬼数字和字符,导致后续的字符和参数的替换不方便

4.SwitchStatements(Switch惊悚现身):Customer通过Switch来判断影片的类型,随着影片的类型增多,Switch的判断必然增多

5.发散式变化:单我的影片价格调整,积分调整的时候,我需要在Customer生成不同账单的函数中去修改。

6.依赖情节:这是一种“讲数据和对数据的操作行为包装在一起的技术”,有一种经典的气味是:函数对某个类的兴趣高过对自己所处的类的兴趣。

7.语法错误:代码语法的漏洞

如果你能发现以上的代码坏味道,甚至更多,那恭喜你,你已经开始进入了重构的大门。接下来我们通过重构来一步步优化代码。请记住:重构代码讲究一小步一小步的修改,测试。不要一开始就对整个结构进行调整,修改。

A.通过分析代码的坏味道,我们发现第3点:魔鬼数字是最好修改的。替换Customer类中的魔鬼数字得到新的Customer类为:红色部分是我们添加的常量定义,替换到魔鬼数字和字符


publicclassCustomer01{

[code]privateStringname;
privatestaticfinalStringBOOKNAME_STRING="书名:";

privatestaticfinalStringPRICE_STRING="价格:";

privatestaticfinalStringPOINT_STRING="积分:";

privatestaticfinalStringDAY_STRING="天数";

privatestaticfinalStringTOTLEAMOUNT_STRING="总价格:";

privatestaticfinalStringTOTLEPOINT_STRING="总积分";

privatestaticfinalStringCHAT_T_STRING="/t";

privatestaticfinalStringCHAT_N_STRING="/n";


privatestaticfinalintMOVICE_CHILDREN_PRICE=2;

/**儿童片租赁后可以使用的天数**/

privatestaticfinalintMOVICE_CHILDREN_DEADLINE=2;

/**超过租赁天数后,应付的价钱**/

privatestaticfinaldoubleMOVICE_CHILDREN_DELAY_PRICE=1.5;


privatestaticfinalintMOVICE_NEW_PRICE=3;


privatestaticfinaldoubleMOVICE_NORMAL_PRICE=1.5;

privatestaticfinalintMOVICE_NORMAL_DEADLINE=3;

privatestaticfinaldoubleMOVICE_NORMAL_DEALY_PRICE=1.5;


privatestaticfinalintPOINT_ADD_MIN_DAY=1;


publicStringgetName(){

returnname;

}


privateListrentals=newArrayList();


publicCustomer01(String_name){

name=_name;

}


publicvoidaddMovice(Rental_rental){

rentals.add(_rental);

}



publicvoidcreateBill(){


doubletotalAmount=0;

intrenterPoint=0;

StringBufferbillInfo=newStringBuffer();


for(Rental_rental:rentals){

doublethisAmount=0;

intthisPoint=0;

inttype=_rental.getMovice().getType();


switch(type){

caseMovice.CHILDREN:

thisAmount+=MOVICE_CHILDREN_PRICE;

if(_rental.getDaysRental()>MOVICE_CHILDREN_DEADLINE){

thisAmount+=(_rental.getDaysRental()-MOVICE_CHILDREN_DEADLINE)*MOVICE_CHILDREN_DELAY_PRICE;

}

break;

caseMovice.NEW:

thisAmount+=_rental.getDaysRental()*MOVICE_NEW_PRICE;

break;

caseMovice.NORMAL:

thisAmount+=MOVICE_NORMAL_PRICE;

if(_rental.getDaysRental()>MOVICE_NORMAL_DEADLINE){

thisAmount+=(_rental.getDaysRental()-MOVICE_NORMAL_DEADLINE)*MOVICE_NORMAL_DEALY_PRICE;

}

break;

default:

break;

}


thisPoint++;

if(type==Movice.NEW&&_rental.getDaysRental()>POINT_ADD_MIN_DAY){

thisPoint++;

}


totalAmount+=thisAmount;

renterPoint+=thisPoint;


billInfo.append(BOOKNAME_STRING+_rental.getMovice().getName()+CHAT_T_STRING);

billInfo.append(PRICE_STRING+thisAmount+CHAT_T_STRING);

billInfo.append(POINT_STRING+thisPoint+CHAT_T_STRING);

billInfo.append(DAY_STRING+_rental.getDaysRental()+CHAT_N_STRING);


}


billInfo.append(TOTLEAMOUNT_STRING+totalAmount+CHAT_N_STRING);

billInfo.append(TOTLEPOINT_STRING+renterPoint+CHAT_N_STRING);

System.out.println(billInfo);

}

}

[/code]

B.过长的方法:我们发现Customer类的createBill()方法过长,通过分析该方法后,我们发现该方法主要做了以下几件事情:1.依次获得单个租赁碟片的价格2.依次获得单个碟片的积分3.按规程生成账单因此我们通过抽取业务逻辑形成方法的方式修改Customer类的createBill()方法,同时我们发现String使用的错误,当添加多个字符串的时候,需要使用StringBuffer。结果如下:红色部分为修改的代码


publicclassCustomer02{

[code]privateStringname;
privatestaticfinalStringBOOKNAME_STRING="书名:";

privatestaticfinalStringPRICE_STRING="价格:";

privatestaticfinalStringPOINT_STRING="积分:";

privatestaticfinalStringDAY_STRING="天数";

privatestaticfinalStringTOTLEAMOUNT_STRING="总价格:";

privatestaticfinalStringTOTLEPOINT_STRING="总积分";

privatestaticfinalStringCHAT_T_STRING="/t";

privatestaticfinalStringCHAT_N_STRING="/n";


privatestaticfinalintMOVICE_CHILDREN_PRICE=2;

privatestaticfinalintMOVICE_CHILDREN_DEADLINE=2;

privatestaticfinaldoubleMOVICE_CHILDREN_DELAY_PRICE=1.5;


privatestaticfinalintMOVICE_NEW_PRICE=3;


privatestaticfinaldoubleMOVICE_NORMAL_PRICE=1.5;

privatestaticfinalintMOVICE_NORMAL_DEADLINE=3;

privatestaticfinaldoubleMOVICE_NORMAL_DEALY_PRICE=1.5;


privatestaticfinalintPOINT_ADD_MIN_DAY=1;


publicStringgetName(){

returnname;

}


privateListrentals=newArrayList();


publicCustomer02(String_name){

name=_name;

}


publicvoidaddMovice(Rental_rental){

rentals.add(_rental);

}


/**

*<获得用户租赁的碟片的价格和积分,生成账单>

*<1.获得单个租赁碟片的价格>

*<2.获得单个碟片的积分>

*<3.按规程生成账单>

*/


privateStringBufferbillInfo=newStringBuffer();


publicvoidcreateBill(){


doubletotalAmount=0;

intrenterPoint=0;



for(Rental_rental:rentals){


totalAmount+=getRentalPrice(_rental);

renterPoint+=getRentalPoint(_rental);;

createSingleBill(_rental);

}


addStatistics(totalAmount,renterPoint);


}


privatedoublegetRentalPrice(Rental_rental){

inttype=_rental.getMovice().getType();

doublethisAmount=0;


switch(type){

caseMovice.CHILDREN:

thisAmount+=MOVICE_CHILDREN_PRICE;

if(_rental.getDaysRental()>MOVICE_CHILDREN_DEADLINE){

thisAmount+=(_rental.getDaysRental()-MOVICE_CHILDREN_DEADLINE)*MOVICE_CHILDREN_DELAY_PRICE;

}

break;

caseMovice.NEW:

thisAmount+=_rental.getDaysRental()*MOVICE_NEW_PRICE;

break;

caseMovice.NORMAL:

thisAmount+=MOVICE_NORMAL_PRICE;

if(_rental.getDaysRental()>MOVICE_NORMAL_DEADLINE){

thisAmount+=(_rental.getDaysRental()-MOVICE_NORMAL_DEADLINE)*MOVICE_NORMAL_DEALY_PRICE;

}

break;

default:

break;

}

returnthisAmount;

}


privateintgetRentalPoint(Rental_rental){

intthisPoint=0;

thisPoint++;

if(_rental.getMovice().getType()==Movice.NEW&&_rental.getDaysRental()>POINT_ADD_MIN_DAY){

thisPoint++;

}

returnthisPoint;

}


privatevoidcreateSingleBill(Rental_rental){

billInfo.append(BOOKNAME_STRING+_rental.getMovice().getName()+CHAT_T_STRING);

billInfo.append(PRICE_STRING+getRentalPrice(_rental)+CHAT_T_STRING);

billInfo.append(POINT_STRING+getRentalPoint(_rental)+CHAT_T_STRING);

billInfo.append(DAY_STRING+_rental.getDaysRental()+CHAT_N_STRING);

}


privatevoidaddStatistics(doubletotalAmount,intrenterPoint){

billInfo.append(TOTLEAMOUNT_STRING+totalAmount+CHAT_N_STRING);

billInfo.append(TOTLEPOINT_STRING+renterPoint+CHAT_N_STRING);

System.out.println(billInfo);

}


}

[/code]

C.依赖情节,我们发现getRentalPrice(),getRentalPoint()都和租赁有关,和顾客没有关系,因为我们需要把其移到对应的类中去,修改为Customer类以及Rental类为:

publicclassCustomer03{
privateStringname;
privatestaticfinalStringBOOKNAME_STRING="书名:";
privatestaticfinalStringPRICE_STRING="价格:";
privatestaticfinalStringPOINT_STRING="积分:";
privatestaticfinalStringDAY_STRING="天数";
privatestaticfinalStringTOTLEAMOUNT_STRING="总价格:";
privatestaticfinalStringTOTLEPOINT_STRING="总积分";
privatestaticfinalStringCHAT_T_STRING="/t";
privatestaticfinalStringCHAT_N_STRING="/n";

publicStringgetName(){
returnname;
}

privateListrentals=newArrayList();

publicCustomer03(String_name){
name=_name;
}

publicvoidaddMovice(Rental03_rental03){
rentals.add(_rental03);
}

/**
*<获得用户租赁的碟片的价格和积分,生成账单>
*<1.获得单个租赁碟片的价格>
*<2.获得单个碟片的积分>
*<3.按规程生成账单>
*/

privateStringBufferbillInfo=newStringBuffer();

publicvoidcreateBill(){

doubletotalAmount=0;
intrenterPoint=0;

for(Rental03_rental:rentals){

totalAmount+=_rental.getRentalPrice();
renterPoint+=_rental.getRentalPoint();;
createSingleBill(_rental);
}

addStatistics(totalAmount,renterPoint);

}

privatevoidcreateSingleBill(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);
}

privatevoidaddStatistics(doubletotalAmount,intrenterPoint){
billInfo.append(TOTLEAMOUNT_STRING+totalAmount+CHAT_N_STRING);
billInfo.append(TOTLEPOINT_STRING+renterPoint+CHAT_N_STRING);
System.out.println(billInfo);
}

}


Rental类修改:

publicclassRental03{

privateMovicemovice;
privateintdaysRental;

privatestaticfinalintMOVICE_CHILDREN_PRICE=2;
privatestaticfinalintMOVICE_CHILDREN_DEADLINE=2;
privatestaticfinaldoubleMOVICE_CHILDREN_DELAY_PRICE=1.5;

privatestaticfinalintMOVICE_NEW_PRICE=3;

privatestaticfinaldoubleMOVICE_NORMAL_PRICE=1.5;
privatestaticfinalintMOVICE_NORMAL_DEADLINE=3;
privatestaticfinaldoubleMOVICE_NORMAL_DEALY_PRICE=1.5;

privatestaticfinalintPOINT_ADD_MIN_DAY=1;

publicRental03(Movice_movice,int_daysRental){
movice=_movice;
daysRental=_daysRental;
}

publicMovicegetMovice(){
returnmovice;
}

publicintgetDaysRental(){
returndaysRental;
}

publicdoublegetRentalPrice(){
inttype=getMovice().getType();
doublethisAmount=0;

switch(type){
caseMovice.CHILDREN:
thisAmount+=MOVICE_CHILDREN_PRICE;
if(getDaysRental()>MOVICE_CHILDREN_DEADLINE){
thisAmount+=(getDaysRental()-MOVICE_CHILDREN_DEADLINE)*MOVICE_CHILDREN_DELAY_PRICE;
}
break;
caseMovice.NEW:
thisAmount+=getDaysRental()*MOVICE_NEW_PRICE;
break;
caseMovice.NORMAL:
thisAmount+=MOVICE_NORMAL_PRICE;
if(getDaysRental()>MOVICE_NORMAL_DEADLINE){
thisAmount+=(getDaysRental()-MOVICE_NORMAL_DEADLINE)*MOVICE_NORMAL_DEALY_PRICE;
}
break;
default:
break;
}
returnthisAmount;
}

publicintgetRentalPoint(){
intthisPoint=0;
thisPoint++;
if(getMovice().getType()==Movice.NEW&&getDaysRental()>POINT_ADD_MIN_DAY){
thisPoint++;
}
returnthisPoint;
}
}


D.我们发现Rental类的getRentalPrice()跟影片的类型和影片的价格有关,因此其更应该放到Movice类里面去,修改Rental类和Movice类为。通过迁移,租赁价格的修改都集中到了Movice类中。

publicclassRental05{

privateMovice05movice;
privateintdaysRental;

privatestaticfinalintPOINT_ADD_MIN_DAY=1;

publicRental05(Movice05_movice,int_daysRental){
movice=_movice;
daysRental=_daysRental;
}

publicMovice05getMovice(){
returnmovice;
}

publicintgetDaysRental(){
returndaysRental;
}

publicdoublegetRentalPrice(){
returngetMovice().getTotalPrice(getDaysRental());
}

publicintgetRentalPoint(){
intthisPoint=0;
thisPoint++;
if(getMovice().getType()==Movice.NEW&&getDaysRental()>POINT_ADD_MIN_DAY){
thisPoint++;
}
returnthisPoint;
}
}


Movice类

publicclassMovice05{

publicstaticfinalintNORMAL=0;
publicstaticfinalintCHILDREN=1;
publicstaticfinalintNEW=2;

privatestaticfinalintMOVICE_CHILDREN_PRICE=2;
privatestaticfinalintMOVICE_NEW_PRICE=3;
privatestaticfinaldoubleMOVICE_NORMAL_PRICE=1.5;

privatestaticfinalintMOVICE_CHILDREN_DEADLINE=2;
privatestaticfinaldoubleMOVICE_CHILDREN_DELAY_PRICE=1.5;

privatestaticfinalintMOVICE_NORMAL_DEADLINE=3;
privatestaticfinaldoubleMOVICE_NORMAL_DEALY_PRICE=1.5;

privateStringname="";
privateinttype=0;
privateintthisAmount=0;

publicintgetTotalPrice(intdaysRental){
if(getType()==CHILDREN){
thisAmount+=MOVICE_CHILDREN_PRICE;
if(daysRental>MOVICE_CHILDREN_DEADLINE){
thisAmount+=(daysRental-MOVICE_CHILDREN_DEADLINE)*MOVICE_CHILDREN_DELAY_PRICE;
}
}elseif(getType()==NORMAL){
thisAmount+=MOVICE_NORMAL_PRICE;
if(daysRental>MOVICE_NORMAL_DEADLINE){
thisAmount+=(daysRental-MOVICE_NORMAL_DEADLINE)*MOVICE_NORMAL_DEALY_PRICE;
}
}elseif(getType()==NEW){
thisAmount+=MOVICE_NEW_PRICE*daysRental;
}
returnthisAmount;
}

publicStringgetName(){
returnname;
}

publicintgetType(){
returntype;
}

publicMovice05(int_type,String_name){
name=_name;
type=_type;
}
}


D.到这里,我们发现惊悚的Switch类还没有处理掉。通过我们分析Switch主要是对不同的电影类型进行不同的处理,因此我们可以考虑抽取一个超级的电影类,不同的电影类型继承该类来解决Switch的问题。

publicabstractclassMoviceSuper{

/**影片的价格**/
publicintprice;

/**影片的积分**/
publicintpoint;

/**一步影片可以租多少天**/
publicintrentalFreeDays;

/**超过租期了付的价钱**/
publicdoubledelayDayPrice;

/**电影的名称**/
publicStringname;

publicStringgetName(){
returnname;
}

publicMoviceSuper(String_name){
name=_name;
}

/**
*<获得租赁影片的价钱>
*<总价格=单个影片的价格+延迟时每天应付的价格>
*
*@paramdaysRental:租赁的天数
*@return
*/
publicabstractdoublegetRentalPrice(intdaysRental);

publicabstractintgetRentalPoint();
}


packagecom.chapter01;

publicclassMoviceChildextendsMoviceSuper{

privatestaticfinalintPOINT=1;
privatestaticfinalintPRICE=2;
privatestaticfinalintDEADLINE=2;
privatestaticfinaldoubleDELAY_PRICE=1.5;

publicMoviceChild(Stringname){
super(name);
//TODOAuto-generatedconstructorstub
price=PRICE;

rentalFreeDays=DEADLINE;

delayDayPrice=DELAY_PRICE;

point=POINT;
}

@Override
publicintgetRentalPoint(){
//TODOAuto-generatedmethodstub
returnpoint;
}

@Override
publicdoublegetRentalPrice(intdaysRental){
//TODOAuto-generatedmethodstub
doublethisAmount=0;
thisAmount+=price;
if(daysRental>rentalFreeDays){
thisAmount+=(daysRental-rentalFreeDays)*delayDayPrice;
}
returnthisAmount;
}

}

packagecom.chapter01;

publicclassMoviceNewextendsMoviceSuper{

privatestaticfinalintPRICE=3;

privatestaticfinalintPOINT=2;

publicMoviceNew(Stringname){
super(name);

price=PRICE;

delayDayPrice=PRICE;

rentalFreeDays=0;

point=POINT;
}

@Override
publicintgetRentalPoint(){
//TODOAuto-generatedmethodstub
returnpoint;
}

@Override
publicdoublegetRentalPrice(intdaysRental){
//TODOAuto-generatedmethodstub

returndaysRental*price;
}

}

packagecom.chapter01;

publicclassMoviceNormalextendsMoviceSuper{

privatestaticfinalintPRICE=2;

privatestaticfinalintDEADLINE=3;

privatestaticfinaldoubleDEALY_PRICE=1.5;

privatestaticfinalintPOINT=1;

publicMoviceNormal(Stringname){
super(name);

price=PRICE;

delayDayPrice=DEALY_PRICE;

rentalFreeDays=DEADLINE;

point=POINT;
}

@Override
publicintgetRentalPoint(){
//TODOAuto-generatedmethodstub
returnpoint;
}

@Override
publicdoublegetRentalPrice(intdaysRental){
doublethisAmount=0;
thisAmount+=price;
if(daysRental>price){
thisAmount+=(daysRental-price)*delayDayPrice;
}
returnthisAmount;
}

}


通过我们一小步一小步的重构,让我们的程序更加优美,适应变化性更强。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: