• 第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个问题,在看了后面的内容能得到解答。

  • 相关阅读:
    storm环境搭建
    环境变量
    vmware tools安装及使用其实现与宿主机共享文件夹
    关于mysql中的count()函数
    centos7配置jdk8
    linux常用命令(随时更新中)
    ES中对索引的相关操作
    linux centos7下源码 tar安装mysql5.7.23(5.7以上均可试用)
    安装linux虚拟机配置静态ip(桥接模式,外部机器能够访问)
    安装linux虚拟机配置静态ip(NAT模式)
  • 原文地址:https://www.cnblogs.com/wencheng9012/p/13479540.html
Copyright © 2020-2023  润新知