第一章 重构,第一个案例

第一章最前面的书中前面的话说得很有道理,一本授之以渔的书,开场就来历史、原理性的东西,很难勾起继续阅读的欲望,写书是这样,开会做分享亦然,所以作者精挑细选了一个代码规模不是很大并且能告诉我们很多重构的道理。

案例说明

这是一个非常简单的案例,展示了一个影片出租店用的程序,计算每一位顾客的消费金额并打印详单的模块,同时还需要计算每一位客人的积分。给出一个 UML 图,是最初版本。

抽象了三个实体:影片(片名、片类型)、租赁(影片、租赁天数)、顾客(姓名、租赁清单),代码详见下面的三个类以及 MainClass。

public class Customer {
    private String name;
    private Vector<Rental> rentals = new Vector<Rental>();

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

    public String getName() {
        return name;
    }
    public Vector<Rental> getRentals() {
        return rentals;
    }

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

    public String statement() {
        double totalAmount = 0;
        int frequentRenterPointers = 0;
        Enumeration<Rental> 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;
    }
}

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

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

public class MainClass {

    public static void main(String[] args) {
        // 三部影片
        Movie movie1 = new Movie("复仇者联盟", Movie.NEW_RELEASE);
        Movie movie2 = new Movie("超能陆战队", Movie.CHILDREN);
        Movie movie3 = new Movie("澳门风云", Movie.REGULAR);

        //两名顾客
        Customer customer1 = new Customer("Jack");
        Customer customer2 = new Customer("Stary");

        //顾客租约
        Rental rental1 = new Rental(movie1, 5);
        Rental rental2 = new Rental(movie2, 3);
        Rental rental3 = new Rental(movie3, 3);
        customer1.addRentals(rental1);
        customer1.addRentals(rental2);
        customer2.addRentals(rental2);
        customer2.addRentals(rental3);

        String statement1 = customer1.statement();
        String statement2 = customer2.statement();
        System.out.println(statement1);
        System.out.println("-----------------------");
        System.out.println(statement2);

    }
}

 

重构前提是不改变源程序的输出结果,原始的运行结果如下:

Rental Records for Jack
    复仇者联盟   15.0
    超能陆战队   1.5
Amount owed is 16.5
You earned 3 frequent renter points
-----------------------
Rental Records for Stary
    超能陆战队   1.5
    澳门风云    3.5
Amount owed is 5.0

Customer提供了 statement 方法来输出租赁清单信息 
对这个例子,从功能需求上说,程序代码是没有什么问题,可是在用户要提出新的需求,比如,① 现在不仅仅要在console输出,需要能输出成 HTML 串显示成网页。针对这个需求,只能再编写一个全新的 htmlStatement(), 大量重复 statement()的行为。 ② 如果计费标准发生改变怎么办?statement 和 htmlStatement 方法都需要修改,并确保修改的一致性。后续还需要再修改,又得剪剪贴贴,复制粘贴的行为是造成一定的潜在的威胁。 
军规1:如果你发现自己需要为程序添加一个特性,而代码结构使你无法方便达成目的,那就重构那个程序,使特性的添加比较容易进行,然后再添加特性。

重构第一步

重构的第一步就是要为即将修改的代码建立一组可靠的测试环境,这些测试是必要的,避免因为重构带来新的 bug。后面将会单独介绍构建测试机制。 
军规2:重构之前,首先检查自己是否有一套可靠的测试机制,这些测试必须有自我检验的能力。

分解并重组 statement()

v01 中 statement 方法就是那种很长的代码,属于代码坏味道的一种(Long Method),需要分解长函数,把较小的代码移至更合适的地方。重构 statement 可以分 以下几个步骤: 
① 提炼 Switch 语句,提炼到单独的函数中比较合适。 
这一段只有两个临时变量,thisAmount 和 each,直接利用 idea 的 Refactor->Extract->Method 即可完成方法的提取。这里希望把 thisAmount 作为它的返回值,each 传入即可。提取成 amountFor 函数,然后修改函数中不好的变量名,如 thisAmount->result, 参数 each->rental。使用 idea 提供的 Refactor->Rename 即可快速重命名,清晰易懂的变量名称是代码能否容易理解的关键之所在。 
军规3:任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。 
原 statement 方法重构成如下:

public class Customer {
    .....
    public String statement() {
         double totalAmount = 0;
         int frequentRenterPointers = 0;
         Enumeration<Rental> 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
             thisAmount = amountFor(each);

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

    private double amountFor(Rental rental) {
        double result = 0;
        switch (rental.getMovie().getPriceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (rental.getDayRented() > 2) {
                    result += (rental.getDayRented() - 2) * 1.5;
                }
                break;
            case Movie.NEW_RELEASE:
                result += rental.getDayRented() * 3;
                break;
            case Movie.CHILDREN:
                result += 1.5;
                if (rental.getDayRented() > 3) {
                    result += (rental.getDayRented() - 3) * 1.5;
                }
                break;
        }
        return result;
    }
}

② 将 amountFor 移至合适的位置 
可以发现 amountFor 函数使用了来自 rental 类的信息,却没有使用来自 Customer 中的任何信息,可以考虑将 amountFor 移至 Rental 中。用一个更适合的名字 getCharge()来命名这个 amountFor。 使用 idea 帮你完成:Refactor->Move(将可见性设置为 public),同时改一下名称。 修改类如下:

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

public class Customer {
    ...
    public String statement() {
        double totalAmount = 0;
        int frequentRenterPointers = 0;
        Enumeration<Rental> 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
            thisAmount = each.getCharge();

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

}

③ 清理变量 thisAmount 
发现变量 thisAmount 只是接受了一下 getCharge(),后面没有作任何变化,可以采用以查询替换变量原则去除变量 thisAmount,暂时先不讨论两次计算带来的性能开销,这个完全可以优化掉。修改之后的如下:

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

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

            // 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" + each.getCharge() + "\n";
            totalAmount += each.getCharge();

        }

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

④ 提炼“常客积分计算”代码 
可以发现,积分计算逻辑只跟影片种类有关系,似乎有理由将计算责任移到 Rental 类中,可以先对计算积分部分采用Refactor->Extract->Method,然后采用 Refactor->Move 移至 Rental 中,后面采用一样的方式替换掉变量。重构之后的如下:

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

        while (rentalEnumeration.hasMoreElements()) {
            Rental each = rentalEnumeration.nextElement();
            frequentRenterPointers += each.getFrequentRenterPointers();

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

        }

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

}

public class Rental {
    ...
    public int getFrequentRenterPointers() {
        // add bonus for two day new release rental
        if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDayRented() > 1) {
            return 2;
        }
        return 1;
    }
}

⑤ 去除totalAmount和frequentRenterPointers 
经过上述重构之后,发现临时变量依旧是一个灾难(这里只是比较少而已,可以保留,为了演示重构手法,忽略过于重构的嫌疑!),就需要再使用用查询代替临时变量(Replace Temp With Query)的手法来清理这两个变量,也方便后面添加 htmlStatement。重构之后添加 htmlStatement 之后:

public class Customer {
    ...
    public String statement() {
        Enumeration<Rental> rentalEnumeration = rentals.elements();
        String result = "Rental Records for " + getName() + "\n";

        while (rentalEnumeration.hasMoreElements()) {
            Rental each = rentalEnumeration.nextElement();
            // show figures for this rental
            result += "\t" + each.getMovie().getTitle() + "\t" + each.getCharge() + "\n";
        }

        // add footer lines
        result += "Amount owed is " + getTotalCharge() + "\n";
        result += "You earned " + getTotalFrequentRenterPointers() + " frequent renter points";
        return result;
    }

    public String htmlStatement() {
        Enumeration<Rental> rentalEnumeration = rentals.elements();
        String result = "<h1>Rental Records for " + getName() + "</h1>\n";
        result += "<table border=‘1‘><tr><td>Movie Name</td><td>Charge</tr>\n";
        while (rentalEnumeration.hasMoreElements()) {
            Rental each = rentalEnumeration.nextElement();
            // show figures for this rental
            result += "\t<tr><td>" + each.getMovie().getTitle() + "</td><td>" + each.getCharge() + "</td></tr>\n";
        }

        // add footer lines
        result += "\t<tr><td colspan=‘2‘>Total Charge:" + getTotalCharge() + "</td></tr>\n";
        result += "\t<tr><td colspan=‘2‘>Total Pointers:" + getTotalFrequentRenterPointers() + "</td></tr>\n";
        result += "</table>";
        return result;
    }

    private double getTotalCharge() {
        double result = 0;
        Enumeration<Rental> rentalEnumeration = rentals.elements();
        while (rentalEnumeration.hasMoreElements()) {
            result += rentalEnumeration.nextElement().getCharge();
        }
        return result;
    }

    private double getTotalFrequentRenterPointers() {
        double result = 0;
        Enumeration<Rental> rentalEnumeration = rentals.elements();
        while (rentalEnumeration.hasMoreElements()) {
            result += rentalEnumeration.nextElement().getFrequentRenterPointers();
        }
        return result;
    }

}

public class MainClass {
    ...
    public static void main(String[] args) {
        // 三部影片
        Movie movie1 = new Movie("复仇者联盟", Movie.NEW_RELEASE);
        Movie movie2 = new Movie("超能陆战队", Movie.CHILDREN);
        Movie movie3 = new Movie("澳门风云", Movie.REGULAR);

        //两名顾客
        Customer customer1 = new Customer("Jack");
        Customer customer2 = new Customer("Stary");

        //顾客租约
        Rental rental1 = new Rental(movie1, 5);
        Rental rental2 = new Rental(movie2, 3);
        Rental rental3 = new Rental(movie3, 3);
        customer1.addRentals(rental1);
        customer1.addRentals(rental2);
        customer2.addRentals(rental2);
        customer2.addRentals(rental3);

        System.out.println("---------Console 形式展示--------------");
        System.out.println(customer1.statement());
        System.out.println("-----------------------");
        System.out.println(customer2.statement());

        System.out.println("----------HTML 形式展示-------------");
        System.out.println(customer1.htmlStatement());
        System.out.println(customer2.htmlStatement());

    }
}

修改之后类图如下: 
 
好了,重构到现在可以方便地添加以 HTML 展示了,革命尚未成功,如果修改影片的分类规则该怎么做?添加一个新的分类规则就得修改费用计算方式和积分计算方式了,还需继续努力。

时间: 2024-10-14 18:30:38

第一章 重构,第一个案例的相关文章

iOS 6 By Tutorials ---第一章--【第一弹】-【翻译】

iOS 6 By Tutorials(pdf 文档)  By the raywenderlich.com Tutorial Team 备注:本人没有怎么翻译过技术型的文章,慢慢翻之.---这本书总共是27章, Chapter 1:Introduction  --第一章:入门介绍 iOS 6 introduces an abundance of great new APIs and technologies that all iOS developers should learn – from A

重构_改善既有代码的设计第一章重构例子

/** * */ package statedesignmodel; import java.util.Enumeration; import java.util.Vector; /** * @author Administrator * */ public class Customer { private String _name; private Vector<Rental> _rentals = new Vector<Rental>(); public Customer(St

第一章 重构

libs工具包结构: activity activity基类封装 net 网络底层封装 cache 数据缓存,图片缓存 ui 自定义控件 主项目包结构: activity (activity下面可以按照模块进行划分) adapter 所有适配器 entity 所有实体类 db sqlite逻辑封装类 engine 业务相关类 ui 自定义控件 utils 公用的方法 interfaces 接口 listener listener接口,以on开头 使用fastjson注意点: 加了符号Annota

Vue 第一章练习 列表的案例

知识点: 1.全局过滤器:时间格式化 2.some ,indexOf ,filter等函数使用 代码如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <!-- 新 Bootstrap 核心 CSS 文件 --> <link href="http

第一章 安装CentOS 7系统

第一章 系统安装 第一节 安装CentOS7 软件--软件选择--默认是以最小的方式安装 初次安装为体验CentOS7桌面,软件选择安装GNOME桌面 创建自定义分区 (整块硬盘大小30GB) /boot 500MB swap  2048MB /     10GB 剩余的磁盘保留,需要时在进行分区 用户设置 设置root超级管理员密码 创建普通用户并为该用户创建密码 CentOS7安装完成,重新进入系统. 登录CentOS 第二节 设置IP地址

【读书笔记】《编译原理》第一章 引论

第一章 引论 第一章 引论 1 语言处理器 2 一个编译器的结构 3 程序设计语言发展历程 5 编译技术的应用 1.1 语言处理器 编译器compiler:将源程序翻译成目标程序,生成目标代码快速,错误诊断效果差. 解释器interpreter:用户提供源程序和输入,产生输出,较慢,错误诊断效果好. java语言处理:Java源程序->字节码bytecode->虚拟机解释执行 语言处理系统:源程序--预处理器preprocessor--经过预处理的源程序--编译器--目标汇编程序(便于输出调试

重构--第一个案例

1.1 起点 实例非常简单.这是一个影片出租店的程序,计算每一位顾客的消费金额并打印详单.操作者告诉程序:顾客租了哪些影片.租期多长,程序便根据租赁时间和影片类型算出费用.影片分为三类:普通片.儿童片和新片.除了计算费用,还要为常客计算积分,积分会根据租片种类是否为新片而有所不同. 我会逐一列出这些类的代码. Movie(影片) public class Movie { public static final int CHILDRENS = 2; public static final int

读《大型网站技术架构:核心原理与案例分析》第一章:大型网站架构演化

写在前面 从开始写代码到现在,已经做了好几个项目了,BS和CS的都有,一直都以一个码农自居.但,作为一个进步的程序员,都有一个成为架构师的理想.于是,在平时的工作中,也积极的去看各种书籍,看园子里面的精品文章.希望,在这条追逐梦想的道路上,能够留下点点滴滴,也算是对知识的一种巩固,一些分享. 读书感受   快下班的时候,看了该书的第一章.算是对网站的架构演化有了一些认识. (1)初始网站的架构:一台服务器,应用程序,数据库,文件都在一台服务器上面.LMAP足矣. (2) 二级网站的架构:应用服务

软件工程第一章至十一章汇总

第一章软件软件是计算机程序,规程及运行计算机系统可能需要的文档和数据.软件分为通用软件和定制软件.软件的特性:1.复杂性2.不可见性3.不断变化4.大多数软件仍然是定制的,而不是通过已有的构件组装而成.软件于二十世纪50~60年代,70年代,80年代,90年代至今进行发展.在此过程中遇到一些危机:1.软件的开发成本和进度难以估计,延迟交付甚至取消项目的现象屡见不鲜.2.软件存在着错误多,性能低,不可靠,不安全等质量问题.3.软件的成本在计算机系统的整个成本中所占的比例越来越大.4.软件的维护极其