重构,改善既有代码的设计(实战篇)

news/2024/11/26 0:27:44/

目录

前言

题目

初步解法

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.把握重构的节奏,小步慢跑,即:测试、小修改、测试、小修改..这种节奏使得重构快速且安全。欢迎互相交流~
————————————————
版权声明:本文为CSDN博主「张云瀚」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_38244174/article/details/118858159


http://www.ppmy.cn/news/378736.html

相关文章

计算机室内设计绘图,室内设计中手绘和电脑制图的比较

邓蓝 摘 要:过去十余年随着电脑的兴起和普及,设计师们曾沉迷于挑战电脑效果图的制作,然而复古似乎是每个时代不变的主题,如今人们又开始对手绘效果图感兴趣了。人们追求新事物达到一定程度时又反过来怀念以前的旧事物,这其中除了人性的复杂当然也离不开时代的发展与客观需…

室内设计优美语句_关于室内设计的名言

关于室内设计的名言 1、做设计的不必显得高人一等,都是社会工种罢了; 2、将设计融于人性,将家居带入悠闲自在的情境。 3、站在客户的角度,充分思考为什么找你做设计的理由。 4、你是设计师,不是战斗机,需要…

中国最贵海报设计师!黄海究竟凭什么?

今年第九届北京国际电影节的海报 因为丑出了新高度,上了热搜 甚至在历届北影节那些一言难尽的海报中 这一届都是最丑的 本花忍不住想问 第九张确定不是三年模拟五年高考? 为什么北影节对金人的执念这么深? 相比之下 隔壁上海国际电影节…

Mocha Pro:Track 模块

Track(跟踪)模块中提供了几组选项,进行适当设置之后再实施跟踪,可以得到更好的跟踪结果。 ◆ ◆ ◆ 模块选项说明 Input 输入 Clip 剪辑 选择要跟踪的素材。 --Input 输入 --Layer Below 图层下方 Track Individual Fields 跟…

C++冷知识:Lambda表达式

什么是Lambda表达式 所谓lambda表达式,是一种为了更方便实现回调和简单逻辑的函数写法,应用于C 11的新特性中。 Lambda表达式是一种匿名函数,它可以作为参数传递给其他函数或方法。它通常用于函数式编程,可以简化代码并提高代码…

Monorepo vs. Microrepo: 选择适合你的代码仓库策略

简介 在软件开发领域,选择合适的代码仓库策略对于优化协作、可扩展性和代码质量至关重要。Monorepo和Microrepo是两种流行的方法,它们提供了各自的优势和考虑因素。本文将探讨这两种策略的特点,解释为何不同的公司选择不同的选项,…

ASP.NET教务平台—学籍管理模块开发与设计(源代码+论文)

教务平台之学籍管理模块是一个典型的教务信息管理系统(MIS),其开发主要包括后台数据库的建立和前端应用程序的开发两个方面。对于后台数据库要求实现数据的完整性、一致性和安全性;对于前台应用程序开发则要求模块功能完备、界面友好、易使用等特点。 教务平台之学籍管理模块…

Windows 10 美式键盘消失 解决方案

进入注册表编辑器(regedit)。前往 计算机\HKEY_CURRENT_USER\Keyboard Layout\Preload。如果有名为 “2” 的值,修改其内容为 00000409。如果没有该项,先右键新建一个字符串值。重启电脑。 如果该文对你有帮助,请在下…