您的位置:首页 > 其它

重构,第一个案例(三)

2017-08-06 13:19 405 查看
知识点的梳理:
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性;

重构的节奏:测试,小修改,测试,小修改,测试,小修改...重要的事情说三遍;
  

案例描述

此实例是一个影片出租店使用的程序,计算每一位顾客的消费金额并打印详单;操作者告诉程序:顾客租了哪些影片,租期多长,程序便根据租赁时间和影片类型算出费用。影片分为三类:普通片,儿童片和新片。除了计算费用,还要位常客计算积分,积分会根据租片种类是否位新片而有不同;

此实例提到的类:



Movie(影片)与Rental(租赁)

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 int getPriceCode(){
return
_priceCode;
}
public void setPriceCode(int
arg){
_priceCode =arg;
}
public String getTitle(){
return
_title;
}
}

public class Rental {

private Movie
_movie;
private int
_daysRented;
public Rental(Movie
movie ,
int daysRented){
_movie =
movie;
_daysRented =
daysRented;
}
public int getDaysRented(){
return
_daysRented;
}
public Movie getMovie(){
return
_movie;
}
}

  

Customer(顾客)

import java.util.Enumeration;

import java.util.Vector;

  

public class Customer {

private String
_name;
private Vector
_rentals =
new Vector();
public Customer(String
name){
_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 += 1.5;
if(each.getDaysRented() > 3){
thisAmount += (each.getDaysRented() - 3) * 1.5;
}
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 null;
}
}

Customer提供了一个用于生成详单的函数,下图是这个函数带来的交互过程:
 



  

该段代码的问题:
不符合面向对象,代码复用率低;
Customer中的statement()函数实在是~~~你懂的~

重构第一步

为即将修改的代码建立一组可靠的测试环境;
重构时,要依赖测试,让它告诉我们是否引入了BUG;

分解并重组statement():
代码块愈小,愈容易管理;

statement的第一个问题是swtich()语句,将它单独作为一个函数较好;

重构时,当我们提炼一个函数,我们必须知道可能出现的错误。所以在提炼之前,要先想出安全做法,避免引入BUG;
首先要找出这段函数中的局部变量和参数,each和thisAmount;前者会被修改,后者不会;任何不会被修改的变量都可以当成参数传入新的函数;
如果只有变量会被修改,可以把它当作返回值;
thisAmount是个临时变量,其值在每次循环起始处被设为0,并且在switch语句之前不会被改变,可以把新函数的返回值赋给它;

代码重构:

import java.util.Enumeration;

import java.util.Vector;

  

public class Customer {

private String
_name;
private Vector
_rentals =
new Vector();
public Customer(String
name){
_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();
thisAmount = amountFor(each);
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;
}
private double amountFor(Rental
aRental) {
double
result = 0;
switch(aRental.getMovie().getPriceCode()){
case Movie.REGULAR:
result += 2;
if(aRental.getDaysRented() > 2){
result += (aRental.getDaysRented() -2 ) * 1.5;
}
break;
case Movie.NEW_RELEASE:
result += 1.5;
if(aRental.getDaysRented() > 3){
result += (aRental.getDaysRented() - 3) * 1.5;
}
break;
case Movie.CHILDRENS:
result +=1.5;
if(aRental.getDaysRented() >3){
result += (aRental.getDaysRented() -3) * 1.5;
}
break;
}
return
result;
}
}

仔细观察amountFor()函数,你会发现这个类使用了Rental类的信息,却没有它的所在类Customer类的信息;

大多数情况下,函数应该放在它所使用的数据的所属对象内,所以amountFor()应该移到Rental类去;

public class Rental {

private Movie
_movie;
private int
_daysRented;
public Rental(Movie
movie ,
int daysRented){
_movie =
movie;
_daysRented =
daysRented;
}
public int getDaysRented(){
return
_daysRented;
}
public Movie getMovie(){
return
_movie;
}
double getCharge() {
double
result = 0;
switch(getMovie().getPriceCode()){
case Movie.REGULAR:
result += 2;
if(getDaysRented() > 2){
result += (getDaysRented() -2 ) * 1.5;
}
break;
case Movie.NEW_RELEASE:
result += 1.5;
if(getDaysRented() > 3){
result += (getDaysRented() - 3) * 1.5;
}
break;
case Movie.CHILDRENS:
result +=1.5;
if(getDaysRented() >3){
result += (getDaysRented() -3) * 1.5;
}
break;
}
return
result;
}
}

//为了适应新类,要去掉参数,同时变更函数名称

改变Customer.amountFor()函数内容。使它委托调用新函数即可:

public class Customer {

//。。。
private double amountFor(Rental
aRental){
return
aRental.getCharge();
}
}

实际上,由于重构后的函数,只有一条语句,那么我们完全可以废弃它,直接将此语句挪出来:
public class Customer {

private String
_name;
private
Vector _rentals =
new Vector();
public Customer(String
name){
_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();
thisAmount =
each.getCharge();
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;
}
}

重构后,各个类的状态:



现在看来局部变量thisAmount有些多余了。它用来接受each.getCharge()的执行结果,然后就不再有任何改变。

public class Customer {

private String
_name;
private Vector
_rentals =
new Vector();
public Customer(String
name){
_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()){
Rental
each = (Rental)rentals.nextElement();
frequentRenterPoints ++;
if((each.getMovie().getPriceCode() == Movie.NEW_RELEASE)
&& each.getDaysRented() > 1){
frequentRenterPoints ++;
}
result +=
"\t" +
each.getMovie().getTitle() +
"\t" + String.valueOf(each.getCharge()) +
"\n";
totalAmount +=
each.getCharge();
}
result +=
" Amount owed is " + String.valueOf(totalAmount) +
"\n";
result +=
" You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points ";
return result;
}
}

临时变量有时会引发许多问题,会导致大量参数传来传去,所以在可以省略的时候,要尽量省略;

在此实例中,去掉局部变量thisAmount而使用each.getCharge()函数,会导致费用被计算两次。但是在Rental类中会被优化。如果代码有合理的组织和管理,优化就会有很好的效果;

提炼"常客积分计算"代码
可以把积分计算责任放在Rental类身上。

public class Customer {

//...
public String statement(){
double
totalAmount = 0;
int
frequentRenterPoints = 0;
Enumeration
rentals =
_rentals.elements();
String
result =
"Rental Record for " + getName() +
"\n";
while(rentals.hasMoreElements()){
Rental
each = (Rental)rentals.nextElement();
frequentRenterPoints +=
each.getFrequentRenterPoints();
result +=
"\t" +
each.getMovie().getTitle() +
"\t" + String.valueOf(each.getCharge()) +
"\n";
totalAmount +=
each.getCharge();
}
result +=
" Amount owed is " + String.valueOf(totalAmount) +
"\n";
result +=
" You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points ";
return result;
}
}

public class Rental {

//。。。
int getFrequentRenterPoints() {
if((getMovie().getPriceCode() == Movie.NEW_RELEASE)
&& getDaysRented() > 1){
return 2;
}
else{
return 1;
}
}
}

  

重构前后的UML对比,左边为原始图,右边为重构图





去除临时变量
在Customer中,有totalAmount和frequentRenterPoints两个临时变量。它们都是用来从Customer对象相关的Rental对象中获得某个总量。现在利用查询函数(queryMethod)来取代totalAmount和frequentRentalPoints这两个临时变量。利用Customer中的getTotalCharge()取代totalAmount:

public class Customer {

//。。。
public String statement(){
int
frequentRenterPoints = 0;
Enumeration
rentals =
_rentals.elements();
String
result =
"Rental Record for " + getName() +
"\n";
while(rentals.hasMoreElements()){
Rental
each = (Rental)rentals.nextElement();
frequentRenterPoints +=
each.getFrequentRenterPoints();
result +=
"\t" +
each.getMovie().getTitle() +
"\t" + String.valueOf(each.getCharge()) +
"\n";
}
result +=
" Amount owed is " + String.valueOf(getTotalCharge()) +
"\n";
result +=
" You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points ";
return
result
;
}
private double getTotalCharge(){
double
result = 0;
Enumeration
rentals =
_rentals.elements();
while(rentals.hasMoreElements()){
Rental
each = (Rental)rentals.nextElement();
result +=
each.getCharge();
}
return
result;
}
}

totalAmount在循环内部被复制,我们要把循环复制到查询函数中:
public class Customer {

//...
public String statement(){
int
frequentRenterPoints = 0;
Enumeration
rentals =
_rentals.elements();
String
result =
"Rental Record for " + getName() +
"\n";
while(rentals.hasMoreElements()){
Rental
each = (Rental)rentals.nextElement();
frequentRenterPoints +=
each.getFrequentRenterPoints();
result +=
"\t" +
each.getMovie().getTitle() +
"\t" + String.valueOf(each.getCharge()) +
"\n";
}
result +=
" Amount owed is " + String.valueOf(getTotalCharge()) +
"\n";
result +=
" You earned " + String.valueOf(getTotaIFrequentRenterPoints()) +
" frequent renter points ";
return
result;
}
private int getTotaIFrequentRenterPoints(){
int
result = 0;
Enumeration
rentals =
_rentals.elements();
while(rentals.hasMoreElements()){
Rental
each = (Rental)rentals.nextElement();
result +=
each.getFrequentRenterPoints();
}
return
result;
}
//...
}

此步骤后的UML交互图:





在Customer类中加入新的功能代码:

public String htmlStatement(){

Enumeration
rentals =
_rentals.elements();
String
result =
"<H1>Rentals for<EN>" + getName() +
"</EN></H1><P>\n";
while(rentals.hasMoreElements()){
Rental
each = (Rental)rentals.nextElement();
result +=
each.getMovie().getTitle() +
": " + String.valueOf(each.getCharge()) +
"<BR>\n";
}
result +="<P>You owe<EM>" + String.valueOf(getTotalCharge()) +
"</EM><P>\n";
result +="On this rental you earend<EM>" + String.valueOf(getTotaIFrequentRenterPoints())
+ "</EM> frequent renter points<P>";
return
result;
}

运用多态取代与价格相关的条件逻辑
switch:最好不要在另一个对象的属性基础上运用switch语句。如果非要使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用;在getCharge中,使用Movie中的getPriceCode方法。所以我们最好把getCharge函数移到Movie中,同时该方法又需要Rental的_daysRented数据,将租期长度作为参数传递进去。
计算费用时需要两项数据:租期长度和影片类型。在这里我们是将Rental中的租期长度传给了Movie,之所以这样做的原因使该实例可能发生变化的是加入新影片类型,这种变化会带来不稳定倾向。如果影片类型有所变化,我们应该尽量控制它造成的影响,所以选在Movie对象内计算费用;

public class Movie {

//。。。
double getCharge(int
daysRented) {
double
result = 0;
switch(getPriceCode()){
case Movie.REGULAR:
result += 2;
if(daysRented > 2){
result += (daysRented -2 ) * 1.5;
}
break;
case Movie.NEW_RELEASE:
result += 1.5;
if(daysRented > 3){
result += (daysRented - 3) * 1.5;
}
break;
case Movie.CHILDRENS:
result +=1.5;
if(daysRented >3){
result += (daysRented -3) * 1.5;
}
break;
}
return
result;
}
}

将计算方法放入Movie类后,然后修改Rental的getCharge(),让它使用这个新函数:
public class
Rental {

private Movie
_movie;
private int
_daysRented;
public Rental(Movie
movie ,
int daysRented){
_movie =
movie;
_daysRented =
daysRented;
}
public int getDaysRented(){
return
_daysRented;
}
public Movie getMovie(){
return
_movie;
}
int getFrequentRenterPoints() {
if((getMovie().getPriceCode() == Movie.NEW_RELEASE)
&& getDaysRented() > 1){
return 2;
}
else{
return 1;
}
}
double getCharge(){
return
_movie.getCharge(_daysRented);
}
}

处理完成getCharge()后,Rental中的getFrequentRenterPoints()也存在这样的问题,使用了Movie中的priceCode参数,所以将这个函数搬移至Movie:

public class Rental {

//...
int getFrequentRenterPoints() {
return
_movie.getFrequentRenterPoints(_daysRented);
}
double getCharge(){
return
_movie.getCharge(_daysRented);
}
}

public class Movie {

//...
double getCharge(int
daysRented) {
double
result = 0;
switch(getPriceCode()){
case Movie.REGULAR:
result += 2;
if(daysRented > 2){
result += (daysRented -2 ) * 1.5;
}
break;
case Movie.NEW_RELEASE:
result += 1.5;
if(daysRented > 3){
result += (daysRented - 3) * 1.5;
}
break;
case Movie.CHILDRENS:
result +=1.5;
if(daysRented >3){
result += (daysRented -3) * 1.5;
}
break;
}
return
result;

int getFrequentRenterPoints(int
daysRented) {
if((getPriceCode() == Movie.NEW_RELEASE)
&& daysRented > 1){
return 2;
}
else{
return 1;
}
}
}

实现继承
因为有数种影片类型,它们以不同的方式回答相同的问题。这很像子类的工作,建立Movie的三个子类,每个都有自己的计费法:



可用多态来取代Switch语句。但在这里,一部影片可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。但是,我们可以利用"状态模式"。如果这样做的话,类看起来就像下图:



为了引入"状态模式",我们需要将与类型相关的行为搬移至"状态模式"内。之后将switch语句移动Price类。
首先,要针对类型代码使用"自封装字段",确认任何时候都通过取值函数和设置函数来访问类型代码。多数访问操作来自其他类,它们已经在使用取值函数。

看看Movie的构造函数,它仍然直接访问价格代码:

public Movie(String
title,int
priceCode){

_title =title;
_priceCode =
priceCode;
}

修改它,使用set函数为它设值:

public Movie(String
title,int
priceCode){

_title =title;
setPriceCode(priceCode);
}

新建Price类,在其中添加一个抽象函数,提供类型相关的行为。所有的子类中也要加上对应的具体函数:

public abstract class Price {

abstract int getPriceCode();
}

public class ChildrensPrice
extends Price {

@Override
int getPriceCode() {
return Movie.CHILDRENS;
}
}

public class NewReleasePrice
extends Price {

@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
}

public class RegularPrice
extends Price {

@Override
int getPriceCode() {
return Movie.REGULAR;
}
}

现在我们要在Movie类中的设值函数中,做出类型判断。修改Movie类内的"价格代号"访问函数(也就是setter和getter),让它们使用新类:
引入了"状态模式",就需要要使用"状态对象"。所以在Movie类中,引入Price对象,不再保存一个_priceCode变量:
同时修改getPriceCode(),使用抽象类的方法,做到多态;

public int getPriceCode(){

return
_price.getPriceCode();
}

private Price
_price;

public void setPriceCode(int
arg){

switch(arg){
case
REGULAR
:
_price =
new RegularPrice();
break;
case
CHILDRENS
:
_price =
new ChildrensPrice();
break;
case
NEW_RELEASE
:
_price =
new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}

现在要移动getCharge()。将Movie中的代码权责,移交给"状态对象"Price

public class Movie {

//。。。
double getCharge(int
daysRented) {
return
_price.getCharge(daysRented);
}
//...
}

public abstract class Price {

abstract int getPriceCode();
double getCharge(int
daysRented) {
double
result = 0;
switch(getPriceCode()){
case Movie.REGULAR:
result += 2;
if(daysRented > 2){
result += (daysRented -2 ) * 1.5;
}
break;
case Movie.NEW_RELEASE:
result += 1.5;
if(daysRented > 3){
result += (daysRented - 3) * 1.5;
}
break;
case Movie.CHILDRENS:
result +=1.5;
if(daysRented >3){
result += (daysRented -3) * 1.5;
}
break;
}
return
result;
137e4
}
}

用多态来取代条件表达式switch.每次取出一个case分支,在相应的类建立一个覆盖函数。先从RegularPrice开始:

public class RegularPrice
extends Price {

@Override
int getPriceCode() {
return Movie.REGULAR;
}
double getCharge(int
daysRented){
double
result = 2;
if(daysRented > 2){
result += (daysRented -2) * 1.5;
}
return
result;
}
}

public class ChildrensPrice
extends Price {

@Override
int getPriceCode() {
return Movie.CHILDRENS;
}
double getCharge(int
daysRented){
double
result = 1.5;
if(daysRented > 3){
result += (daysRented -3) * 1.5;
}
return
result;
}
}

public class NewReleasePrice
extends Price {

@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
double getCharge(int
daysRented){
return
daysRented * 3;
}
}

  

将case分解完成后,修改Price中的getCharge函数:

public abstract class Price {

abstract int getPriceCode();
abstract double getCharge(int
daysRented);
}

现在运用同样的手法来处理Movie中的getFrequentRenterPoints(),将权责移交给Price类:

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;
setPriceCode(priceCode);
}
public int getPriceCode(){
return
_price.getPriceCode();
}
private Price
_price;
public void setPriceCode(int
arg){
switch(arg){
case
REGULAR
:
_price =
new RegularPrice();
break;
case
CHILDRENS
:
_price =
new ChildrensPrice();
break;
case
NEW_RELEASE
:
_price =
new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}
public String getTitle(){
return
_title;
}
  

double getCharge(int
daysRented) {
return
_price.getCharge(daysRented);
}
int getFrequentRenterPoints(int
daysRented) {
return
_price.getFrequentRenterPoints(daysRented);
}
}

public abstract class Price {

abstract int getPriceCode();
abstract double getCharge(int
daysRented);
int getFrequentRenterPoints(int
daysRented) {
if((getPriceCode() == Movie.NEW_RELEASE)
&& daysRented > 1){
return 2;
}
else{
return 1;
}
}
}

  

getFrequentRenterPoints()函数涉及的内容与子类NewReleasePrice相关,所以将该函数与NewReleasePrice类相关的部分抽离出来,单独重写放在NewReleasePrice类中。而与该类无关的函数部分还保留在Price中:

public class NewReleasePrice
extends Price {

@Override
int getPriceCode() {
return Movie.NEW_RELEASE;
}
double getCharge(int
daysRented){
return
daysRented * 3;
}
int getFrequentRenterPoints(int
daysRented){
return (daysRented >1) ? 2:1;
}
}

public abstract class Price {

abstract int getPriceCode();
abstract double getCharge(int
daysRented);
int getFrequentRenterPoints(int
daysRented) {
return 1;
}
}

引入"状态模式"的好处:
对修改任何与价格有关的行为,或是添加新的定价标准,或者加入其他取决于价格的行为,程序的修改会容易很多;

UML图:



类图:

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