第1章 重构,第一个案例

 初始设计与实现

1.需求:

(1)大体是设计一个影片出租店的程序,计算每一位顾客的消费金额并打印详单。

(2)首先操作者会告诉程序,顾客租了哪些影片,租期多长,程序便根据租赁时间和影片类型算出费用。

(3)还有要知道影片分为三类:普通片,儿童片和新片。

(4)最后除了计算费用,还要为常客计算积分,积分会根据租片种类是否为新片而有不同

2.结构图:

3.代码实现:

Movie类,影片:

@Data
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;
    }
}

Rental类,租赁:

@Data
public class Rental {
    private Movie movie;
    private int dayRented;

    public Rental(Movie movie, int daysRented) {
        this.movie = movie;
        this.dayRented = daysRented;
    }
}

Customer类,顾客类:

@Data
public class Customer {
    private String name;
    private Vector rentals = new Vector();

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

    public void addRental(Rental arg) {
        rentals.add(arg);
    }

    public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentalElement = rentals.elements();
        String result = "Rental Record for " + getName() + "
";
        while (rentalElement.hasMoreElements()) {
            double thisAmount = 0;
            Rental each = (Rental) rentalElement.nextElement();
            //计算总额
            switch (each.getMovie().getPriceCode()) {
                case Movie.REGULAR:
                    thisAmount += 2;
                    if (each.getDayRented() > 2) {
                        thisAmount += (each.getDayRented() - 2) * 1.5;
                    }
                    break;
                case Movie.NEW_RELEASE:
                    thisAmount += each.getDayRented();
                    break;
                case Movie.CHILDRENS:
                    thisAmount += 1.5;
                    if (each.getDayRented() > 3) {
                        thisAmount += (each.getDayRented() - 3) * 1.5;
                    }
                    break;
                default:
                    break;
            }
            //增加积分
            frequentRenterPoints++;
            //add bonus for a two day new release rental
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDayRented() > 1) {
                frequentRenterPoints++;
            }
            //展示租赁详情
            result += "	" + each.getMovie().getTitle() + "
";
            totalAmount += thisAmount;
        }
        result += "Amount owed is " + String.valueOf(totalAmount) + "
";
        result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
        return result;
    }
}

分析并重构

对上述代码进行分析

1.分析:

   这段代码statement()方法很长,做了很多其他类应该完成的事,违背了单一职责原则,开放封闭原则等,灵活性和扩展性都比较差,也不方便复用,但是能满足目前的需求。

2.重构的必要性:

   可能你心里想着:“不管怎么说,它运行得很好,只要没坏,就不要动它”。但实际上虽然它没坏,但是它造成了伤害,它让你的生活比较难过,因为当客户有其他新的需求时(如客户想改变影片分类规则,但还没决定怎么改,只是决定了几套方案,一旦决定就要迅速改完),就很难完成客户所需要的修改,所以重构是很有必要的。

小笔记:如果你发现需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,是特性的添加比较容易进行,然后再添加特性。

对上述代码进行重构

1.重构第一步:

  重构的第一步永远相同,为即将修改的代码建立一组可靠的测试环境。

小笔记:重构之前,首先检查自己是否有一套可靠的测试机制,这些测试必须有自我检验能力

2.首先分解重组statement()的switch判断逻辑:

(1)先将switch判断当一个方法提出来amountFor(Rental each),计算总额时,直接传参,调用刚提出的计算总额方法即可得到thisAmount,这次改动后最好先做一次测试,避免后续改动过多增加测试难度。 

thisAmount = amountFor(each);

 小笔记:重构技术就是以微小的步伐修改程序,如果你发现错误,很容易就能发现它。

(2)改变amountFor()里的变量名称。

将 each 改为 aRental
将 thisAmount 改为 result

问:改名值得么?

答:绝对值得,好的代码有良好的表达,和好的清晰度,改完之后记得先测一下。

小笔记:任何一个傻瓜都能写出计算机理解的代码,唯有写出人类可以理解的代码,才是优秀的程序员。

(3)观察amountFor(Rental aRental)方法时,发现传参时Rental类型参数,和Customer类无关,所以要调整位置,将这个方法放到Rental,方法名叫:getCharge(Rental aRental),然后将Customer类改成如下,并重新测试编译。

Rental类:

public double getCharge() {
        double result = 0;
        //算出总额
        switch (getMovie().getPriceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (getDayRented() > 2) {
                    result += (getDayRented() - 2) * 1.5;
                }
                break;
            case Movie.NEW_RELEASE:
                result += getDayRented();
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (getDayRented() > 3) {
                    result += (getDayRented() - 3) * 1.5;
                }
                break;
            default:
                break;
        }
        return result;
    }

Customer类:

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

疑问:为什么这里不直接调用getCharge()方法,而是先调用amountFor(),再通过amountFor()调用getCharge()呢?

答:这就是下一步要做的事情,但不能直接就先调用新的方法。

(4)先迁移成为新方法,测试没问题后,再删除旧方法

将调用的amountFor()替换为thisAmount = each.getCharge();

(5)替换成 each.getCharge() 后发现,thisAmount 也没了用处,因为它除了赋值没其他作用,而且值在后面也不会有改变,于是将 thisAmount 替换成each.getCharge(),修改后及时测试。

小习惯:可以尽量取消一些临时变量,像上面这种临时变量,被传来传去容易跟丢也没有必要(这里调用了两次计算总额的方法,后续说明怎么优化)

3.然后重构statement()的常客积分的计算

(1)观察发现常客积分的计算也是只与Rental有关,所以讲计算的方法提到Rental类中。

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

将Customer类中提炼为:

//计算积分
frequentRenterPoints += each.getFrequentRenterPoints();

(2)同样因为要算总的积分,所以也可以像计算总额一样单独提出来:

private int getTotalFrequentRenterPoints() {
    int result = 0;
    Enumeration rentalElement = rentals.elements();
    while (rentalElement.hasMoreElements()) {
        Rental each = (Rental)rentalElement.nextElement();
        result += each.getFrequentRenterPoints();
    }
    return result;
}

3.马上要修改影片分类规则,但具体怎么做还未决定,需要再进行重构

(1)思路:现在对程序进行修改,肯定是愚蠢的,应该进入积分计算和常客积分计算中,把因条件而异的代码替换掉,这样才能为将来的改变镀上一层保护膜。

(2)先改变switch语句,将getChange()方法移动到Movie里,原因是本系统可能发生的变化是加入新影片的影响,这种变化带有不稳定倾向,所以为尽量控制它的影响,就在Movie里计算费用

  疑问(未解决):为什么在Movie里计算费用就可以控制影响?

  于是先将getChange()移动到Moive类中:

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

  再改变Rental类里相应代码:

public double getCharge() {
    return movie.getCharge(dayRented);
}

(3)以相同手法处理常客积分计算

Movie类:

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

Rental类:

public int getFrequentRenterPoints() {
    return movie.getFrequentRenterPoints(dayRented);
}

(4)使用状态模式来设计Movie类

这里先了解下实现思路,之后看完重构后再细看第一章...

随记:

1.代码块越小,代码功能就越容易管理,代码的处理和移动就余越轻松。

2.还不太能get到为什么要将Rental类的逻辑迁移到Movie里,虽然按照后面的结果,通过状态模式来拆开Movie里getCharge()的逻辑,在知道了后续实现的前提下我觉得将getCharge()的逻辑迁移到Movie里是没问题的,但要我根据文中所说因为可能新做影片类别,就直接要迁移这个方法到Movie里,我是不能get到这个点的,直接用三种影片算价方式继承Rental就可以吧,这样就只用改变一个类,就算后续有新加影片,或者重新定义怎么分片,Movie类也只是配置参数就行,不用大改。

3.还有另一点我也没有想清楚,和第2点也是相关的,就是为什么不能用继承的方式,文中说:“一部影片可以在生命周期内修改自己的分类,一个对象却不能在自己的生命周期修改所属类”,这句话也没有理解。

4.希望第2第3个问题,在看了后面的内容能得到解答。

原文地址:https://www.cnblogs.com/wencheng9012/p/13479540.html