再读经典重构、重构与模式、修改代码的艺术《一》

最近想把重构领域的三本经典书籍(重构-Martin Fowler、重构与模式-Joshua Kerievsky 、修改代码的艺术-Michael C. Feathers)做一次重读,在温故知新的基础上每周与团队伙伴们一起分享讨论。
备注:博文上的相关代码均引用上述三本书籍,版权归三位大师所有。
这里写图片描述
这里写图片描述
这里写图片描述

什么是重构(Refactoring)?

所谓重构是这样一个过程:【在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构】。重构是一种有纪律的、经过训练的、有条不紊的程序整理方法,可以将整理过程中不小心引入错误的机率降到最低。本质上说,重构就是在代码写好之后改进它的设计

重构,第一个案例

案例是一个影片出租店用的程序,计算每一位顾客的消费金额并打印报表。操作者告诉程序:顾客租了哪些影片、租期多长,程序便根据租赁时间和影片类型算出费用。影片分为三类:普通片、儿童片和新片。除了计算费用,还要为常客计算点数;点数会随着租片种类是否为新片而有不同。
下图为程序的类图:
这里写图片描述

Movie(影片)

Movie是一个简单的data class。

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

Rental(租赁)

Rental class表示某个顾客租了一部影片

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(顾客)

Customer class用来表示顾客。

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

Customer还提供了一个用以制造报表的函数,下图显示这个函数带来的交互过程。
这里写图片描述

    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();
            //determine amounts for each line
            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;
            }

            // add frequent renter points(累加常客积点)
            frequentRenterPoints++;
            // add bonus for a two day new release rental
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE)
                    && each.getDaysRented() > 1)
                frequentRenterPoints++;

            // show figures for this rental(显示此笔租借数据)
            result += "\t" + each.getMovie().getTitle() + "\t" +
                    String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;
        }

        // add footer lines(结尾打印)
        result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) +
                " frequent renter points";
        return result;
    }

对此起始程序的评价

  • 做的事情太多,它做了很多原本应该由其他class完成的事情
  • 无法重用,如:增加Html格式报表
  • 不易扩展,如:用户希望改变影片分类规则,但是还没有决定怎么改。设想的方案都会影响顾客消费金额和常客积点德计算方式
  • 总之程序缺乏面向对象的设计,代码僵化、不易扩展

如果发现自己需要为程序添加一个特性,而代码结构使你无法很方便地那么做,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。

重构的第一步

  1. 为即将修改的代码建议一组可靠的测试环境(运行迅速、可重复运行、自动化)
    建立测试环境-Github
  2. 分解并重组statement()
    -找出代码的逻辑泥团并运用Extract Method
    -搬移金额计算代码
    这里写图片描述

    -提炼常客积点计算代码
    这里写图片描述

    这里写图片描述
    -去除临时变量
    这里写图片描述

    这里写图片描述

  3. 运用多态取代与价格相关的条件逻辑
    -getCharge()应该移到Movie class
    为什么选择【将租期长度传给Moive对象】而不是【将影片类型传给Rental对象】?因为本系统可能发生的变化是加入新影片类型,这种变化带有不稳定倾向。如果影片类型有所变化,希望掀起最小的涟漪,所以选择在Movie对象内计算费用。
    -以相同手法处理常客积点计算,这样就把根据影片类型而变化的所有东西,都放到了影片类型所属的class中

  4. 来到继承,引入State/Strategy模式
    这里写图片描述
    -使用Replace Type Code with State/Strategy
    -对getCharge()实施Move Method
    -运用Replace Conditional with Polymorphism

  5. 重构后的UML
    这里写图片描述
    这里写图片描述

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页