重构改善既有代码的设计----赏析(一)

租赁影片

程序说明:顾客租了哪些影片,租期多长,根据租赁时间和影片类型算出费用和积分。

1.1重构的第一步

重构的第一步永远相同:为即将修改的代码建立一组可靠的测试环境。
重构之前,首先要检查自己是否有一套可靠的测试机制。这些测试必须有自我检验的能力。可靠的测试可以避免重构过程中引入新的bug。

1.2分解并重组statement()

找到逻辑谜团,运用Extract Method(110)把逻辑谜团提炼到独立的函数中。
原代码如下有3个类。

package com.it.taikang.bean;

/**
 * 租赁订单
 * Created by DongGege on 2019/8/27.
 */
public class Rental {
    private Movie _movie ; // 影片
    private int _daysRented; // 租赁天数

    public Rental(Movie _movie, int _daysRented) {
        this._movie = _movie;
        this._daysRented = _daysRented;
    }

    public Movie getMovie() {
        return _movie;
    }

    public int getDaysRented() {
        return _daysRented;
    }

}

package com.it.taikang.bean;

/**
 * 影片
 * Created by DongGege on 2019/8/27.
 */
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) {
        this._title = _title;
        this._priceCode = _priceCode;
    }

    public int getPriceCode() {
        return _priceCode;
    }

    public void setPriceCode(int _priceCode) {
        this._priceCode = _priceCode;
    }

    public String getTitle() {
        return _title;
    }

}
	





package com.it.taikang.bean;
import java.util.Enumeration;
import java.util.Vector;
/**
 * 顾客
 * Created by jh on 2019/8/27.
 */
public class Customer {
    private String _name; // 顾客名字
    private Vector _rentals = new Vector();  // 租赁订单数组
    public Customer(String name) {
        super();
        this._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 += each.getDaysRented()*3;
                break;
            case Movie.CHILDRENS:
                thisAmount += 1.5;
                if(each.getDaysRented() > 3 ){
                    thisAmount += (each.getDaysRented() - 3) * 1.5;
                }
                break;
        }

            // 增加积分
            frequentRenterPoints ++;
            // 新片+租赁时间达2天  积分+1
            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 eared "+String.valueOf(frequentRenterPoints)+"frequent renter points";

        return result;

    }

}

修改后:

/**
 * 生成订单
 * @return
 */
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 ++;
        // 新片+租赁时间达2天  积分+1
        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 eared "+String.valueOf(frequentRenterPoints)+"frequent renter points";

    return result;

}

private double amountFor(Rental each) {
    double thisAmount =0;
    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 += each.getDaysRented()*3;
            break;
        case Movie.CHILDRENS:
            thisAmount += 1.5;
            if(each.getDaysRented() > 3 ){
                thisAmount += (each.getDaysRented() - 3) * 1.5;
            }
            break;
    }
    return thisAmount;
}

修改变量名称

好的代码应该清楚表达出字自己的功能,变量名称是代码清晰的关键。

private double getCharge (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+= aRental.getDaysRented()*3;
            break;
        case Movie.CHILDRENS://儿童片
            result+= 1.5;
            if(aRental.getDaysRented() > 3 ){
                result+= (aRental.getDaysRented() - 3) * 1.5;
            }
            break;
    }
    return result;
}

1.4函数应该放在它所使用的数据所属的对象中

运用Move Method(142) 把函数移动到只与函数本身相关的类中。
因为顾客中的getCharge ()只使用了来自Rental类的信息,却没有使用来自

Customer类的信息,所以顾客租金的计算应该移动到Rental类中去。

去掉旧函数getCharge ,直接调用新函数getCharge。Customer类改为 :

package com.it.taikang.bean;

/**
 * 租赁订单
 * Created by DongGege on 2019/8/27.
 */
public class Rental {
    private Movie _movie ; // 影片
    private int _daysRented; // 租赁天数

    public Rental(Movie _movie, int _daysRented) {
        this._movie = _movie;
        this._daysRented = _daysRented;
    }

    public Movie getMovie() {
        return _movie;
    }

    public int getDaysRented() {
        return _daysRented;
    }

     public 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+= getDaysRented()*3;
                break;
            case Movie.CHILDRENS:
                result+= 1.5;
                if(getDaysRented() > 3 ){
                    result+= (getDaysRented() - 3) * 1.5;
                }
                break;
        }
        return result;
    }
}

原顾客类中getCharge 方法直接调用计算租金方法即可,Customer 类改为:

package com.it.taikang.bean;
import java.util.Enumeration;
import java.util.Vector;

/**
 * 顾客
 * Created by jh on 2019/8/27.
 */
public class Customer {

    private String _name; // 顾客名字
    private Vector _rentals = new Vector();  // 租赁订单数组

    public Customer(String name) {
        super();
        this._name = name;
    }

    public void addRental(Rental arg){
        _rentals.addElement(arg);
    }

    public String getName() {
        return _name;
    }

    /**
     * 生成订单
     * @return
     */
    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 ++;
            // 新片+租赁时间达2天  积分+1
            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 eared "+String.valueOf(frequentRenterPoints)+"frequent renter points";

        return result;

    }
}

1.5 尽量去掉临时变量

thisAmount 是个多余的临时变量,直接去掉。

Customer 改为:
/**
 * 生成订单
 * @return
 */
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 ++;
        // 新片+租赁时间达2天  积分+1
        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 eared "+String.valueOf(frequentRenterPoints)+"frequent renter points";

    return result;

}

1.6提炼常客积分计算代码(使用方法同1.2、1.3、1.4)

把积分计算方法放到Rental类中,写为常客积分计算方法(getFrequentRenterPoints),并改变Customer类中积分计算代码 。

1.6.1 分解并充组

Customer代码修改如下:
/**
 * 生成订单
 * @return
 */
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 eared "+String.valueOf(frequentRenterPoints)+"frequent renter points";

    return result;

}
Rental类代码修改如下:
增加常客积分计算方法
// 常客积分计算
int getFrequentRenterPoints(){
    // 增加积分
    int frequentRenterPoints =0;
    frequentRenterPoints++;
    // (新片+租赁时间达2天  积分+1 )
    if(getMovie().getPriceCode() == Movie.NEW_RELEASE && getDaysRented() > 1){
        frequentRenterPoints ++;
    }
    return frequentRenterPoints;
}

1.6.2去除临时变量

临时变量会导致大量参数的传递,没有必要。运用Replace with Query(120),并利用查询函数取代临时变量。
由于类中任何函数都可以调用上述查询函数,所以它能够促成较干净的设计,而减少冗长复杂的函数;
去掉statement方法中的2个临时变量:totalAmount 和 frequentRenterPoints 。抽离出对应计算方法,并调用。

Customer 类改为:

/**
 * 顾客
 * Created by DongGege on 2019/8/27.
 */
public class Customer ……

    /**
     * 生成订单
     * @return
     */
    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();
            // 本次租赁记录说明
            result += "\t"+each.getMovie().getTitle()+"\t"+ String.valueOf(each.getCharge ())+"\n";
        }
        // 页脚
        result +="Amount owed is "+ String.valueOf(getTotalCharge())+"\n";
        result +="You eared "+String.valueOf(getTotaIFrequentRenterPoints())+"frequent renter points";

        return result;

    }

    double getTotaIFrequentRenterPoints(){
        //获取总金额
        double result = 0; // 总租金
        Enumeration rentals = _rentals.elements();
        while( rentals.hasMoreElements()){
            Rental each = (Rental)rentals.nextElement();
            result += each.getFrequentRenterPoints();
        }
        return  result;
    }

    double getTotalCharge(){
        //获取总金额
        double result = 0; // 总租金
        Enumeration rentals = _rentals.elements();
        while( rentals.hasMoreElements()){
            Rental each = (Rental)rentals.nextElement();
            result += each.getCharge ();
        }
        return  result;
    }

本步改写作用说明:
Replace Temp whith Query(120);
这里虽然从1个循环变为3个,但是多了2个查询函数。

  1. 使得Customer 类中的任何代码都可以调用这些查询函数。

  2. 若系统其它部分需要这些信息,也可以轻松地将查询函数加入 Customer 类接口。而若没有这些查询波函数,其它函数就必须了解 Rental 类,并自行建立循环

1.7运用多态取代与价格相关的条件逻辑

什么是多态:父类型的引用指向子类型的对象。用一句比较通俗的话:同一操作作用于不同的对象,可以产生不同的效果,这就是多态。

最好不要在另一个对象的属性基础上运用switch语句,应该在对象自己的数据上使用。
本文中价格明显应该是影片中的属性,所以移动 getCharge ,getFrequentRenterPoints 方法到Movie 类中去。把会根据影片类型的变化而变化的东西放在影片类中。

Movie 类改为:
package com.it.taikang.bean;

/**
 * 影片
 * Created by DongGege on 2019/8/27.
 */
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) {
        this._title = _title;
        this._priceCode = _priceCode;
    }

    public int getPriceCode() {
        return _priceCode;
    }

    public void setPriceCode(int _priceCode) {
        this._priceCode = _priceCode;
    }

    public String getTitle() {
        return _title;
    }
    public 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+= daysRented*3;
                break;
            case Movie.CHILDRENS:
                result+= 1.5;
                if(daysRented > 3 ){
                    result+= (daysRented - 3) * 1.5;
                }
                break;
        }
        return result;
    }
    // 常客积分计算
    int getFrequentRenterPoints(int daysRented){
        // 增加积分
        int frequentRenterPoints =0;
        frequentRenterPoints++;
        // (新片+租赁时间达2天  积分+1 )
        if(getPriceCode() == Movie.NEW_RELEASE && daysRented > 1){
            frequentRenterPoints ++;
        }
        return frequentRenterPoints;
    }

}
Renta类修改为:
package com.it.taikang.bean;

import java.util.Enumeration;

/**
 * 租赁订单
 * Created by DongGege on 2019/8/27.
 */
public class Rental {
    private Movie _movie ; // 影片
    private int _daysRented; // 租赁天数

    public Rental(Movie _movie, int _daysRented) {
        this._movie = _movie;
        this._daysRented = _daysRented;
    }

    public Movie getMovie() {
        return _movie;
    }

    public int getDaysRented() {
        return _daysRented;
    }

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

}

继承

分析:
本例中有多种影片类型,他们一不同的方式回答相同的问题。这看起来应该是子类的工作,我们可以选择用Replace Type Code with Subclasses建立多个Movie子类来计算自己的费用。但是深入分析一下,一步影片可以在生命周期内修改自己的分类,一个对象却不能在在生命周期内修改自己所属的类。
这时候我们可以选择状态模式或者策略模式来解决这个问题。

state模式和Strategy模式非常相似,因此你无论选择其中哪一个,重构过程都是相同的。[选择哪一个模式]并非问题关键所在,你只需要选择更适合特定情境的模式就行了。如果你打算完成本项重构之后再以Replace Conditional with Polymorphism简化一个算法,那么选择Strategy模式比较合适;如果你打算搬移与状态相关的数据,并且认为对象的状态会改变,就应该选择使用State模式。

作法

  1. 用Self Encapsulate Field 将type code自我封装起来

  2. 新建一个Class,根据type code的用途为它命名,这就是一个state object.

  3. 为这个新建的class添加subclass,每个subclass对应一种type code。
    比起逐一添加,一次性加入所有必要的subclass可能更简单些。

  4. 在super class中建立一个抽象的查询函数,用以返回type code;在每个subclass中覆写该函数,返回确切的type code。

  5. 编译

  6. 在source class中建立一个值域,用以保存新建的state object。

  7. 调整source class中负责查询type code的函数,将查询动作转发给state object。

  8. 调整source class中为type code设值的函数,将一个恰当的state object subclass赋值给[保存state object]的那个值域。

  9. 编译、测试。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值