欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

重构初体验——影片出租店例子

程序员文章站 2022-05-06 22:50:17
...

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

好代码是不断重构出来的,让我们先来看一个案例。

需求:

这是一个影片出租店用的程序,计算每一位顾客的消费金额并打印详单。操作者告诉程序:顾客租了哪些影片、租期多长,程序便根据租赁时间和影片类型计算出费用。影片分为三类:普通片、儿童片和新片。除了计算费用,还要为常客计算积分,具体的租赁用户积分规则为:

 租赁规则
* 价格计算规则:
* 普通片儿 —— 起步价2¥,超过2天的部分每天每部电影收费1.5元
* 新片儿    —— 每天每部3元
* 儿童片    —— 起步价2¥,超过3天的部分每天每部电影收费1.5元
*
* 积分计算规则:
* 每借一部电影积分加1,新片每部加2

原始程序有三个类,分别为Moive,Rental,Customer类。

重构初体验——影片出租店例子

/**
 * @Author: dyf
 * @Date: 2019/9/19 16:15
 * @Description: 电影类
 */
public class Movie {
    public static final int CHILDRENS = 2;
    public static final int REGULAR = 0;
    public static final int NEW_REALEASE = 1;

    private String title;
    private int priceCode;

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

    public String getTitle() {
        return title;
    }

    public int getPriceCode() {
        return priceCode;
    }

    public void setPriceCode(int priceCode) {
        this.priceCode = priceCode;
    }
}
**
 * @Author: dyf
 * @Date: 2019/9/19 16:17
 * @Description: 租赁记录
 */
public class Rental {
    private Movie movie;
    private int daysRented;

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

    public Movie getMovie() {
        return movie;
    }

    public int getDaysRented() {
        return daysRented;
    }
}
public class Customer {
    private String name;
    private Vector rentals = new Vector();

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

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

    public String getName() {
        return name;
    }

    public String statement(){
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentalss = rentals.elements();
        String result = "Rental Record for" + getName() + "\n";
        while(rentalss.hasMoreElements()){
            double thisAmount = 0;
            Rental each = (Rental) rentalss.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_REALEASE:
                    thisAmount += each.getDaysRented() * 3;
                    break;
                case Movie.CHILDRENS:
                    thisAmount += 1.5;
                    if(each.getDaysRented() > 3)
                        thisAmount += (each.getDaysRented() - 3) * 1.5;
                    break;
            }

            //每部电影添加积分
            frequentRenterPoints ++;
            //新片每部电影加2分
            if((each.getMovie().getPriceCode() == Movie.NEW_REALEASE) && each.getDaysRented() > 1)
                frequentRenterPoints++;

            //打印每一条租借记录
            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 enter points";
        return result;
    }

}

看到这个起始程序,就想到了自己日常写的面条代码,只为完成功能,不符合面向对象精神。假如用户希望新增一个详单打印statment2(),后面又更改了计费标准,后面又希望改变影片分类规则,后面又......那么这样的代码,是无法满足快速高效的修改的。

下面是修改后的类图:

重构初体验——影片出租店例子

代码及测试代码:

public class Movie {
    public static final int CHILDRENS = 2;
    public static final int REGULAR = 0;
    public static final int NEW_REALEASE = 1;

    private String title;
    private int priceCode;
    private Price price;


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

    public String getTitle() {
        return title;
    }

    public int getPriceCode() {
        return priceCode;
    }

    public void setPriceCode(int arg) {
        switch (arg){
            case REGULAR:
                price = new RegularPrice();
                break;
            case CHILDRENS:
                price = new ChildrensPrice();
                break;
            case NEW_REALEASE:
                price = new NewReleasePrice();
                break;
            default:
                throw new IllegalArgumentException("Incorrect Price Code");
        }
    }


    public double getCharge(int daysRented){
        return price.getCharge(daysRented);
    }


    public int getFrequentRenterPoints(int daysRented) {
        return price.getFrequentRenterPoints(daysRented);
    }
}
public class Rental {
    private Movie movie;
    private int daysRented;

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

    public Movie getMovie() {
        return movie;
    }

    public int getDaysRented() {
        return daysRented;
    }

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

    public int getFrequentRenterPoints(){
        return movie.getFrequentRenterPoints(daysRented);
    };
}
public class Customer {
    private String name;
    private Vector rentals = new Vector();

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

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

    public String getName() {
        return name;
    }

    public String statement(){
        Enumeration rentalss = rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while(rentalss.hasMoreElements()){
            Rental each = (Rental) rentalss.nextElement();
            //我喜欢尽量去除这一类临时变量。虽然计算了两次,但是很容易被优化
            result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
        }
        //add footer lines
        result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
        result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent enter points";
        return result;
    }


//    private double amountFor(Rental rental){
//        double thisAmount = 0;
//        //最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是别人的数据上使用
//        //这暗示getCharge()应该移动到Movie类里面去
//        switch (rental.getMovie().getPriceCode()){
//            case Movie.REGULAR:
//                thisAmount += 2;
//                if(rental.getDaysRented() > 2)
//                    thisAmount += (rental.getDaysRented() - 2) * 1.5;
//                break;
//            case Movie.NEW_REALEASE:
//                thisAmount += rental.getDaysRented() * 3;
//                break;
//            case Movie.CHILDRENS:
//                thisAmount += 1.5;
//                if(rental.getDaysRented() > 3)
//                    thisAmount += (rental.getDaysRented() - 3) * 1.5;
//                break;
//        }
//        return thisAmount;
//    }

    public double getTotalCharge(){
        double result = 0;
        Enumeration rentalss = rentals.elements();
        while(rentalss.hasMoreElements()){
            Rental each = (Rental) rentalss.nextElement();
            result += each.getCharge();
        }
        return result;
    }

    public int getTotalFrequentRenterPoints(){
        int result = 0;
        Enumeration rentalss = rentals.elements();
        while(rentalss.hasMoreElements()){
            Rental each = (Rental) rentalss.nextElement();
            result += each.getFrequentRenterPoints();
        }
        return result;
    }
}
/**
 * @Author: dyf
 * @Date: 2019/9/23 11:12
 * @Description:
 * 我们有数种影片类型,它们以不同的方式回答相同的问题。
 */
abstract class Price {
    abstract  int getPriceCode();

    abstract double getCharge(int daysRented);

    int getFrequentRenterPoints(int daysRented) {
        return 1;
    }
}

class NewReleasePrice extends Price {

    @Override
    int getPriceCode() {
        return Movie.NEW_REALEASE;
    }

    double getCharge(int daysRented){
        return daysRented * 3;
    }

    int getFrequentRenterPoints(int daysRented) {
        return (daysRented > 1) ? 2 : 1;
    }
}

class RegularPrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.REGULAR;
    }

    double getCharge(int daysRented){
        double result = 2;
        if(daysRented > 2)
            result += (daysRented - 2) * 1.5;
        return result;
    }
}

class ChildrensPrice extends Price {

    @Override
    int getPriceCode() {
        return Movie.CHILDRENS;
    }

    double getCharge(int daysRented){
        double result = 1.5;
        if(daysRented > 3)
            result += (daysRented - 3) * 1.5;
        return result;
    }
}

测试代码:

/**
 * @Author: dyf
 * @Date: 2019/9/23 16:01
 * @Description:
 * 需求描述:
 *
 * 有三种类型的电影,顾客可以进行租赁
 *
 * 租赁规则
 * 价格计算规则:
 * 普通片儿 —— 起步价2¥,超过2天的部分每天每部电影收费1.5元
 * 新片儿    —— 每天每部3元
 * 儿童片    —— 起步价1.5¥,超过3天的部分每天每部电影收费1.5元
 *
 * 积分计算规则:
 * 每借一部电影积分加1,新片每部加2
 */
public class CustomerTest {
    @Test
    public void test_allMovie_2days_rent(){
        Customer c2 = new Customer("2天顾客");
        setRentDays(c2, 2);
        System.out.println(c2.statement());
        assertEquals(9.5, c2.getTotalCharge(), 0.00);//delta是个精度值
        assertEquals(4, c2.getTotalFrequentRenterPoints());
    }

    @Test
    public void test_allMovie_3days_rent(){
        Customer c3 = new Customer("3天顾客");
        setRentDays(c3, 3);
        System.out.println(c3.statement());
        assertEquals(14, c3.getTotalCharge(), 0.00);//delta是个精度值
        assertEquals(4, c3.getTotalFrequentRenterPoints());
    }

    @Test
    public void test_allMovie_4days_rent(){
        Customer c4 = new Customer("4天顾客");
        setRentDays(c4, 4);
        System.out.println(c4.statement());
        assertEquals(20, c4.getTotalCharge(), 0.00);//delta是个精度值
        assertEquals(4, c4.getTotalFrequentRenterPoints());
    }

    @Test
    public void test_2new1regularMovie_5days_rent(){
        Customer c = new Customer("5天2部新片1部普通影片顾客");
        c.addRental(new Rental(new Movie("普通片儿",0), 5));
        c.addRental(new Rental(new Movie("新上映片",1), 5));
        c.addRental(new Rental(new Movie("新上映片",1), 5));
        System.out.println(c.statement());
        assertEquals(36.5, c.getTotalCharge(), 0.00);//delta是个精度值
        assertEquals(5, c.getTotalFrequentRenterPoints());
    }

    private void setRentDays(Customer c, int days){
        c.addRental(new Rental(new Movie("普通片儿",0), days));
        c.addRental(new Rental(new Movie("新上映片",1), days));
        c.addRental(new Rental(new Movie("儿童片儿",2), days));
    }
}

总结:

通过这个例子感受重构的魅力,此例用到了数个重构方法:Extract Method(提取方法),Move Method(移动方法),Replace Conditional with polymorphism(有条件的用多态取代),Self Encapsulate Field(安全压缩变量),Replace Type Code with State/Strategy(策略模式),重构方法实在太多,具体可看《重构》一书中讲解,我认为还是实践才能记得住。

1、复习重构的定义:

重构是在不改变软件可观察行为的前提下改善其内部结构。对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

2、何时重构?

事不过三,三则重构

  1. 重复性工作,既有的代码无法帮助你轻松添加新特性时

  2. 修补bug时,排查逻辑困难

  3. code review 可以让他人来复审代码检查是否具备可读性,可理解性

  4. 太多的代码无注释,已然连自己都无法快速理清代码逻辑

3、重构的衡量指标

  • 数量: 代码的行数

  • 质量: 代码复杂度,耦合度,可读性,架构依赖复杂度等

  • 成本: 花费的时间

  • 回报(成果): 支持后续功能的快速叠加,解决现有因代码设计问题无法优化的矛盾等

4、一些需重构代码的坏味道

1)Duplicated Code(重复代码)

2)  Long Method(过长函数) 

遵循一条原则: 每当感觉需要注释来说明什么的时候,可以尝试将需要说明的东西写进一个函数中

3)Large Class(过大的类) 

这种情况容易出现冗余代码。比如如果类内出现了多个变量带有相同的前缀或者后缀,这意味着你可以考虑把他们提炼到某个组件内,或者考虑这个组件是否可以成为子类,使用提炼类的手法来重构。

4) Lazy Class(冗赘类)

当子类没有做足够的工作的时候,或者说在可见的预期内,不会有新的情况出现,考虑将类内联化。

相关标签: 重构