我是靠谱客的博主 彪壮皮卡丘,最近开发中收集的这篇文章主要介绍重构,改善既有代码的设计(实战篇)前言题目初步解法重构实战总结,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

目录

前言

题目

初步解法

1.Movie类

2.Rental类

3.Customer类

4.谈谈初步解法的问题

重构实战

1. 重构第一步

2.分解并重组statements

2.1. Extract Method(提取函数)

2.2. Move Method(搬移函数)

2.3.提炼“积分计算”代码

2.4.去除临时变量(Replace Temp with Query以查询取代临时变量)

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

3.1. Replace type code with state/strategy(以state/strategy取代类型码)

3.2.Move Method(搬移函数)

3.3. Replace Conditional with Polymorphism(以多态取代表达式)

总结


前言

程序员必懂的代码重构(理论篇)一文介绍了代码重构是什么、常用的重构手法和代码中的“坏味道”。But talk is cheap. Show me the code,本文将从实战的角度来谈谈代码重构,共分为:题目、初步解法、重构实战和总结四部分。

本篇blog是《重构,改善代码既有代码的设计》(密码: ab5g)一文的读书笔记,读书笔记与书一起食用效果更佳哦。欢迎点赞、收藏、评论三连~,谢谢大家。


题目

该程序为影片出租店用的程序,目的是计算每位顾客的消费金额并打印详单。你需要1.根据租赁时间和影片类型(普通片、儿童片和新片三类)计算费用;2.除了计算费用之外,需要计算积分。积分会根据是否是新片而有所不同。

初步解法

跟着笔者思路一起来看,按照功能可以拆分为Movie类、Rental类和Customer类。

1.Movie类

功能:该类主要记录类型、价格和标题等,是单纯数据类。

/**
 * Movie记录类型、价格和标题等,单纯数据类。
 * @author kevinhe
 */
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;
    }

    public int getPriceCode() {
        return priceCode;
    }

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

    public String getTitle() {
        return title;
    }
}

2.Rental类

功能:表示某位顾客租了一部影片,表示行为。

/**
 * Rental表示某位顾客租了一部影片,表示行为。
 * @author kevinhe
 */
class Rental {
    private Movie movie;
    private int daysRented;

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

    public int getDaysRented() {
        return daysRented;
    }

    public Movie getMovie() {
        return movie;
    }
}

3.Customer类

功能:表示顾客,有数据和相应的访问函数。

/**
 * Customer表示顾客,有数据和相应的访问函数
 *
 * @author kevinhe
 */
public class Customer {
    private String name;
    //Vector 类实现了一个动态数组。和 ArrayList 很相似,但是两者是不同的;
    //1.Vector 是同步访问的。2.Vector 包含了许多传统的方法,这些方法不属于集合框架。
    //Vector 主要用在事先不知道数组的大小,或者只是需要一个可以改变大小的数组的情况。
    private Vector rentals = new Vector();

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

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

    public String getName() {
        return name;
    }

    /**
     * 提供一个用于生成详单的函数
     */
    public String statement() {
        double totalAmount = 0;
        //常客计算积分时使用
        int frequentRenterPoints = 0;
        Enumeration enumeration = rentals.elements();
        String result = "Rental Record for " + getName() + "n";
        while (enumeration.hasMoreElements()) {
            //总金额
            double thisAmount = 0;
            Rental each = (Rental) rentals.elements();
            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;
            }
            frequentRenterPoints++;
            //如果是新书,另算积分呢
            if (each.getMovie().getPriceCode() == Movie.NEW_RELEASE && each.getDaysRented() >= 1) {
                frequentRenterPoints++;
            }
            result += "t" + each.getMovie().getTitle() + "t" + String.valueOf(thisAmount) + "n";
            totalAmount += thisAmount;
        }
        result += "Amount owed is " + String.valueOf(totalAmount) + "n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
        return result;
    }
}

4.谈谈初步解法的问题

  • 不符合面向对象的精神。
    • statement() 做的实在过多了,如果改成HTML格式网页详单输出;或者计费标准发生变化,大量重复statement的代码非常恶心;
    • 假设用户希望改变影片分类规则,进而影响到积分的计算的方式,那么HTML网页显示和现在的显示方式会很难保持修改一致性,很容易修改出bug。
  • 建议:“如果它没坏,就不要动它”可能是不可取的,如果你需要为程序添加一个特性,发现代码结构无法让你很方便达到目的,那么是时候重构了。

重构实战

1. 重构第一步

   重构之前,检查是否有一套可靠测试机制,这些测试必须要足够自动化。

  • 为即将修改的代码建立一组可靠的测试环境,即需要可靠的测试。由于statement是输出是字符串,那么假设有顾客,各租不同影片,产生报表字符串,看新字符串和符合预期的字符串是否一致。
  • 测试需足够自动化,若新/参考字符串一致,那么OK,如果不一致,则显示问题字符串行号,测试能够自我校验,否则大把时间的对比无疑会降低开发速度。 

2.分解并重组statements

  长长的函数需要大卸八块,代码块越小,代码的移动和处理也就越轻松。将较小代码块移至更合适的类,降低代码重复使新函数更容易撰写。

2.1. Extract Method(提取函数)

1.找出逻辑泥团并运用Extract Method方法。本例中的switch语句需提炼至独立函数。找出函数内局部变量和参数。each(未被修改,可以当成参数传入新的函数)和thisAmount(会被修改,格外小心,如果只有一个变量修改,可以将其作为返回值)。那么将新函数返回值返回给thisAmount是可行的。

2.重构技术以微小的步伐修改程序,如果你犯下错误,很容易也能发现它。好的代码应该清楚表达自己的功能,变量名称是代码清晰的关键,唯有写出人类容易理解的代码,才是好的程序员。

/**
 * 提供一个用于生成详单的函数
 */
public String statement() {
    .​...
    while (enumeration.hasMoreElements()) {
        //总金额
        double thisAmount = 0;
        Rental each = (Rental) rentals.elements();
        thisAmount = amountFor(each);
        ....
    }
    .​...
}


/**
 * 金额计算
 * @param aRental
 * @return
 */
private double amountFor(Rental aRental) {
    //注意double、int类型之间的转换。
    double result = 0;
    switch (aRental.getMovie().getPriceCode()) {
        case Movie.REGULAR:
            result += 2;
            //优惠力度
            if (aRental.getDaysRented() > 2) {
                result += (aRental.getDaysRented() - 2) * 1.5;
            }
            break;
        case Movie.NEW_RELEASE:
            //果然还是新书最贵啊
            result += aRental.getDaysRented() * 3;
            break;
        case Movie.CHILDRENS:
            result += 1.5;
            if (aRental.getDaysRented() > 3) {
                result += (aRental.getDaysRented() - 3) * 1.5;
            }
            break;
    }
    return result;
}

2.2. Move Method(搬移函数)

1.观察amountFor函数,使用了Rental类的信息却没有使用来自Customer类的信息,函数是应该放在它所使用的数据的对象内的,所以amountFor应该要放到Rental类而非Customer类,调整代码以使用新类。

2.本例较为简单,只有一个地方使用了新函数,通常来说,你得在可能运用该函数的所有类中查一遍。此时customer类中使用each.getCharge()替代了amountFor(each)方法。此时发现thisAmount变得多余了。使用Replace Temp with Query(以查询取代临时变量)将thisAmount去掉。

补充知识点:尽量去除一部分不必要的临时变量,临时变量会导致大量参数传来传去,长函数容易跟丢,引发bug。

class Rental {
    ....
    /**
     * 金额计算
     * @return
     */
    public double getCharge() {
        //注意double、int类型之间的转换。
        double result = 0;
        switch (getMovie().getPriceCode()) {
            case Movie.REGULAR:
                result += 2;
                //优惠力度
                if (getDaysRented() > 2) {
                    result += (getDaysRented() - 2) * 1.5;
                }
                break;
            case Movie.NEW_RELEASE:
                //果然还是新书最贵啊
                result += getDaysRented() * 3;
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (getDaysRented() > 3) {
                    result += (getDaysRented() - 3) * 1.5;
                }
                break;
        }
        return result;
    }
}
public class Customer {
    ....
    /**
     * 提供一个用于生成详单的函数
     */
    public String statement() {
        ....
        while (enumeration.hasMoreElements()) {
            ...
            result += "t" + each.getMovie().getTitle() + "t" 
                 + String.valueOf(each.getCharge()) + "n";
            totalAmount += each.getCharge();
        }
        .​..
    }
}

2.3.提炼“积分计算”代码

积分计算因影片种类而有所不同,针对“积分计算”代码运用Extract Method重构手法。局部变量each,另一个临时变量是frequentRenterPoints(这个参数在使用之前已初始化,但提炼出的函数并未读取该值,因此无需传入,只需作为新函数的返回值累加上去即可)。

class Rental {
     ...
    /**
     * 计算常客积分
     * @return
     */
    public int getFrequentRenterPoints() {
        //如果是新书,另算积分呢
        if (getMovie().getPriceCode() == Movie.NEW_RELEASE && getDaysRented() >= 1) {
            return 2;
        } else {
            return 1;
        }
    }
}
public class Customer {
    ....
    /**
     * 提供一个用于生成详单的函数
     */
    public String statement() {
        double totalAmount = 0;
        //常客计算积分时使用
        int frequentRenterPoints = 0;
        Enumeration enumeration = rentals.elements();
        String result = "Rental Record for " + getName() + "n";
        while (enumeration.hasMoreElements()) {
            Rental each = (Rental) rentals.elements();
            //计算常客积分
            frequentRenterPoints += each.getFrequentRenterPoints();
            result += "t" + each.getMovie().getTitle() + "t" + String.valueOf(each.getCharge()) + "n";
            totalAmount += each.getCharge();
        }
        result += "Amount owed is " + String.valueOf(totalAmount) + "n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
        return result;
    }
}

2.4.去除临时变量(Replace Temp with Query以查询取代临时变量)

临时变量会造成冗长复杂的函数,使用Replace Temp with Query(以查询取代临时变量)方法,以查询函数替代totalAmount和frequentRentalPoints临时变量。任何函数均可调用,促成干净设计、减少冗长函数。

public class Customer {
    ...
    /**
     * 提供一个用于生成详单的函数
     */
    public String statement() {
        //常客计算积分时使用
        Enumeration enumeration = rentals.elements();
        String result = "Rental Record for " + getName() + "n";
        while (enumeration.hasMoreElements()) {
            Rental each = (Rental) rentals.elements();
            result += "t" + each.getMovie().getTitle() + "t" + String.valueOf(each.getCharge()) + "n";
        }
        result += "Amount owed is " + String.valueOf(getTotalCharge()) + "n";
        result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points";
        return result;
    }

    /**
     * 获取总积分
     * @return
     */
    private double getTotalFrequentRenterPoints() {
        int result = 0;
        Enumeration enumeration = rentals.elements();
        while (enumeration.hasMoreElements()) {
            Rental each = (Rental) rentals.elements();
            result += each.getFrequentRenterPoints();
        }
        return result;
    }

    /**
     * 获取总金额
     * @return
     */
    private double getTotalCharge() {
        double result = 0;
        Enumeration enumeration = rentals.elements();
        while (enumeration.hasMoreElements()) {
            Rental each = (Rental) rentals.elements();
            result +=  each.getCharge();
        }
        return result;
    }
}

重构带来了性能问题,原本while执行一次,但新版本执行三次,降低了性能。重构时可不必担心这些,优化时你需要考虑。现在Customer类的代码可以调用这些查询函数了。如果没有查询函数,你必须得看懂Rental类,并自行循环。程序编写和维护难度大大增加。这时再编写html-statement就简单一些了。

/**
 * 生成HTML详单的函数
 * @return
 */
public String htmlStatement() {
    //常客计算积分时使用
    Enumeration enumeration = rentals.elements();
    String result = "HTML:Rental Record for " + getName() + "n";
    while (enumeration.hasMoreElements()) {
        Rental each = (Rental) rentals.elements();
        result += "t" + each.getMovie().getTitle() + "t" + String.valueOf(each.getCharge()) + "n";
    }
    result += "Amount owed is " + String.valueOf(getTotalCharge()) + "n";
    result += "On this rental You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points";
    return result;
}

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

用户准备修改影片分类规则。费用计算和常客积分计算也会因此而发生改变。首当其冲的就是Rental类getCharge中的switch...case...语句,除非迫不得已,switch..case..应当作用于自己的数据上,而非别人的数据上(这样会有风险)。getCharge移到Movie类中去会更好,传入的是租期长度而非影片类型,因为系统可能会加入新影片类型,不稳定,因此在Movie对象内计算费用。同样的手法处理常客积分函数。

public class Movie {
   ...
    /**
     * 根据影片类型获取费用
     * @param daysRented
     * @return
     */
    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 * 3;
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (daysRented > 3) {
                    result += (daysRented - 3) * 1.5;
                }
                break;
        }
        return result;
    }

    public int getFrequentRenterPoints(int daysRented) {
        //如果是新书,另算积分呢
        if (getPriceCode() == Movie.NEW_RELEASE && daysRented >= 1) {
            return 2;
        } else {
            return 1;
        }
    }
}
class Rental {
    private Movie movie;
    private int daysRented;
    .​..
    /**
     * 金额计算
     * @return
     */
    public double getCharge() {
        return movie.getCharge(daysRented);
    }

    /**
     * 计算常客积分
     * @return
     */
    public int getFrequentRenterPoints() {
        return movie.getFrequentRenterPoints(daysRented);
    }
}

多态取代switch语句,多态设计时不要直接继承Movie,而是通过Price间接去处理,一部影片可以在生命周期内修改自己的分类,但一个对象却不能再生命周期内修改自己所属的类,使用state模式叭。为了引入State模式重构,我们首先使用Replace type code with state/strategy(以state/strategy取代类型码)将与类型相关的行为搬移至state模式中,运用Move Method(搬移函数)方法将switch语句移动至price类中,最后运用Replace Conditional with Polymorphism(以多态取代表达式)去掉switch语句。

3.1. Replace type code with state/strategy(以state/strategy取代类型码)

针对类型码使用Self Encapsulate Field(自封装字段),确保任何时候都通过取/设值函数来访问类型代码,构造函数依旧可以直接访问价格代码。

新建Price类,并提供类型相关的行为,为此,加入抽象函数,并在所有子类中加上对应的具体操作。

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 Price price;

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

    public int getPriceCode() {
        return price.getPriceCode();
    }

    public void setPriceCode(int arg) {
        switch (arg) {
            case Movie.REGULAR:
                price = new RegularPrice();
                break;
            case Movie.NEW_RELEASE:
                price = new NewReleasePrice();
                break;
            case Movie.CHILDRENS:
                price = new ChildrenPrice();
                break;
            default:
                throw new IllegalArgumentException("Incorrect Price Code");
        }
    }
    ....
    }
 }
abstract class Price {
    abstract  int getPriceCode();
}
public class ChildrenPrice extends Price{
    @Override
    int getPriceCode() {
        return Movie.CHILDRENS;
    }
}
public class NewReleasePrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.NEW_RELEASE;
    }
}
public class RegularPrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.REGULAR;
    }
}

3.2.Move Method(搬移函数)

将Movie中的getCharge方法下沉至Price方法中。 

abstract class Price {
    abstract int getPriceCode();
    /**
     * 根据影片类型获取费用
     * @param daysRented
     * @return
     */
    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 * 3;
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (daysRented > 3) {
                    result += (daysRented - 3) * 1.5;
                }
                break;
        }
        return result;
    }
}
public class Movie {
    private Price price;
    .​..

    public int getPriceCode() {
        return price.getPriceCode();
    }
    ....
}

3.3. Replace Conditional with Polymorphism(以多态取代表达式)

一次取出getPriceCode的一个case分支,在对应的类建立覆盖函数。同样的方法处理getFrequentRenterPoints方法。

/**
 * 新建Price类,并提供类型相关的行为,为此,加入抽象函数,并在所有子类中加上对应的具体操作。
 */
abstract class Price {
    /**
     * 获取影片类型code码
     * @return
     */
    abstract  int getPriceCode();
    /**
     * 根据影片类型获取费用
     * @param daysRented
     * @return
     */
    abstract double getCharge(int daysRented);

    /**
     * 如果是新书,采用复写的方法,在超类中留下一个已定义的函数,使之成为一种默认行为。
     * @param daysRented
     * @return
     */
    int getFrequentRenterPoints(int daysRented) {
        return 1;
    }
}
public class RegularPrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.REGULAR;
    }

    @Override
    public double getCharge(int daysRented) {
        double result = 2;
        //优惠力度
        if (daysRented > 2) {
            result += (daysRented - 2) * 1.5;
        }
        return result;
    }
}
public class NewReleasePrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.NEW_RELEASE;
    }

    @Override
    public double getCharge(int daysRented) {
        //果然还是新书最贵啊
        return daysRented * 3;
    }

    @Override
    public int getFrequentRenterPoints(int daysRented) {
        return (daysRented > 1) ? 2 : 1;
    }
}
public class ChildrenPrice extends Price{
    @Override
    int getPriceCode() {
        return Movie.CHILDRENS;
    }

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

 引入State设计模式很值,修改影片分类结构/改变费用计价规则/改变积分规则都会容易很多了。

总结

代码重构就到聊到这里啦,送给大家两个小建议:1.读完笔记之后,在项目中实战吧。写出优雅、易维护、类责任更明确的代码;2.把握重构的节奏,小步慢跑,即:测试、小修改、测试、小修改..这种节奏使得重构快速且安全。欢迎互相交流~

最后

以上就是彪壮皮卡丘为你收集整理的重构,改善既有代码的设计(实战篇)前言题目初步解法重构实战总结的全部内容,希望文章能够帮你解决重构,改善既有代码的设计(实战篇)前言题目初步解法重构实战总结所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(52)

评论列表共有 0 条评论

立即
投稿
返回
顶部