《重构》笔记----案例

最近在读重构,第一章的案例感觉比较经典,提取出来便于经常查阅

案例: 影片出租店,计算顾客消费金额并打印详单,操作员录入顾客租的影片、租期,程序根据影片类型 计算出费用。影片分三类:普通片、儿童片和新片,除了费用 还有积分计算

案例中有三个对象:影片(Movie),租赁(Rental) 和 顾客(Customer),初始uml和代码如下:

class Movie {

    public static final int REGULAR = 0;
    public static final int NEW_RELEASE = 1;
    public static final int CHILDREN = 2;

    private String title;
    private int priceCode;

    public Movie(String title, int priceCode) {
        this.title = title;
        this.priceCode = priceCode;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getPriceCode() {
        return priceCode;
    }

    public void setPriceCode(int priceCode) {
        this.priceCode = priceCode;
    }
}
class Rental {

    private Movie movie;
    private int dayRented;

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

    public Movie getMovie() {
        return movie;
    }

    public void setMovie(Movie movie) {
        this.movie = movie;
    }

    public int getDayRented() {
        return dayRented;
    }

    public void setDayRented(int dayRented) {
        this.dayRented = dayRented;
    }
}
class Customer {
    private String name;
    private Vector rentals = new Vector();

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

    public String getName() {
        return name;
    }

    public Vector getRentals() {
        return rentals;
    }

    public void addRentals(Rental rental) {
        this.rentals.add(rental);
    }

    public String statement() {
        double totalAmount = 0;
        int frequentRenterPointers = 0;
        Enumeration rentalEnumeration = rentals.elements();
        String result = "Rental Records for " + getName() + "\n";

        while (rentalEnumeration.hasMoreElements()) {
            double thisAmount = 0;
            Rental each = rentalEnumeration.nextElement();

            // determine amounts for each line
            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() * 3;
                    break;
                case Movie.CHILDREN:
                    thisAmount += 1.5;
                    if (each.getDayRented() > 3) {
                        thisAmount += (each.getDayRented() - 3) * 1.5;
                    }
                    break;
            }

            // add frequent renter points
            frequentRenterPointers++;
            // add bonus for two day new release rental
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDayRented() > 1) {
                frequentRenterPointers++;
            }

            //show figures for this rental
            result += "\t" + each.getMovie().getTitle() + "\t" + thisAmount + "\n";
            totalAmount += thisAmount;

        }

        // add footer lines
        result += "Amount owed is " + totalAmount + "\n";
        result += "You earned " + frequentRenterPointers + " frequent renter points";
        return result;
    }
}

    代码评价:设计不符合面向对象,实现起来快速随性,不方便后期扩展

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

    重构前先检查是否有一套可靠的测试机制,这些测试必须有子午检测能力

    一、分解重组segment()

    找出segment中逻辑泥团,运用Extract Method,提炼到一个独立函数中,提炼规则:

    1. 任何一个不会被修改的变量都可以被当成传入的新参数。

    2、如果只有一个变量会被修改,可以把他当做返回值。

    文中提炼的是segment 中 switch部分:each为参数,thisAmount为返回值

            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() * 3;
                    break;
                case Movie.CHILDREN:
                    thisAmount += 1.5;
                    if (each.getDayRented() > 3) {
                        thisAmount += (each.getDayRented() - 3) * 1.5;
                    }
                    break;
            }

    提取后变量更名  

    方法名: amountFor(),   变量名: each->aRental,  thisAmount->result,

    任务一个傻瓜都能写出计算机可以理解的代码,唯有写出人容易理解的代码,才是优秀的程序员

    二、搬移 "金额计算" 代码

    多数情况下方法应该放在他所使用数据的归属对象内,amountFor() 在Customer中,用的是Rental中的信息,这一步运用 Move Method 将 amountFor()移至到Rental中,更名为getCharge(),测试没问题后,修改所有引用点

    三、提炼"常客积分计算"代码

    积分计算和影片的种类和租赁的天数有关,此处Extract Method重构手法将积分计算移至Rental中

    方法名:getFrequentRenterPoints()

    提炼后编译、测试,重构是小步前进,降低犯错概率

    四、去除临时变量

    临时变量只在所属方法中有效,它使得方法变得冗长而复杂,此处用查询方法替换 totalAmount 和 frequentRentalPoints(Replace Temp with Query)

    用getTotalCharge() 取代 totalAmount;

    用getFrequentRentalPoints() 取代 frequentRentalPoints

    将变量提到查询方法中增加了while循环,重构时可先不考虑性能问题,优化时统一处理

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

    Rental 中 getCharge中的 switch用到 Movie中获取影片类型的方法,按照 (二)的规则,把getCharge()再做一次搬移,租赁天数getDaysRented()作为参数传入。

    这个方法需要两项数据:租赁天数和影片类型,之所以选择迁移到Movie,是因为影片类型的变化带有不稳定倾向,为了尽量控制它造成的影响。

    同样把根据影片类型变化的积分计算getFrequentRenterPoints() 也迁移到Movie中

到现在为止重构的类图:

    六、终于·····我们来到了继承

    Movie有三种影片类型,他们以不同的方式回答相同的问题,我们可以建立Movie的三个子类,每个都有自己的计费法。这样用多态取代Switch。

    一部影片类型可能在生命周期内发生改变,一个对象却不能在生命周期内修改自己所属的类型。所以不能直接建立Movie的子类,这里可以用State模式:

  《重构》笔记----案例_第1张图片

    首先用 Replace Type Code with State/Strategy,将与类型相关的行为搬至State模式内,然后运用Move Method 将 switch语句移到Price类。最后用Replace Conditional with Polymorphism去掉switch。

    第一步针对类型代码使用 Self Encapsulate Field, 确保任何时候都通过取值函数和设置函数来访问类型代码,包括自身的构造函数。

    然后新建Price类,加入一个抽象方法 abstrace int getPriceCode();创建三个子类:ChildrensPrice,NewReleasePrice,RegularPrice,并在所有子类加上具体的的方法。

    最后修改priceCode的get和set方法;通过Price.getPriceCode()方法获取,设置时根据参数判断类型创建具体Price子类对象。

   类创建完毕,把跟priceCode相关的方法迁移至 Price类

    getCharge() 对应每一种priceCode的取值都不一样,所有在父类Price中创建 abstract double getCharge(int daysRented);在子类中实现getCharge();

    getFrequentRenterPoints(),只对应新片有特殊处理,其他两种是一样的,所以在父类中留下一个已经定义的方法,即默认行为,新片Price类中增加一个覆写方法

    引入state模式,去掉了原getCharge()和getFrequentRenterPoints()方法中priceCode的判断,使得修改任何和价格相关行为都会比较方便。

《重构》笔记----案例_第2张图片

 

你可能感兴趣的:(《重构》笔记----案例)