精华内容
下载资源
问答
  • 此博客编写书中观后收获 ...3.提供常客积分制度,积分对影片类型和租期一定要求 要求:计算顾客的消费金额并打印租赁详单 Movie(影片) /** * 影片类,分为不同类型价格不同 * * @author lune * @create 2017-1
    案例描述如下:
    1.影片租赁店目前提供普通影片,新片,儿童影片三种类型影片供顾客租赁,不同影片类型拥有不同价格码
    2.计费规则按照影片类型和租期有所不同
    3.提供常客积分制度,积分对影片类型和租期有一定要求
    要求:计算顾客的消费金额并打印租赁详单
    Movie(影片)
    /**
     * 影片类,分为不同类型价格不同
     *
     * @author lune
     * @create 2017-11-14 17:40
     */
    public class Movie {
        public static final int REGULAR = 0;            //普通影片
        public static final int NEW_RELEASE = 1;        //新片
        public static final int CHILDRENS = 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 int getPriceCode() {
            return priceCode;
        }
    
        public void setPriceCode(int priceCode) {
            this.priceCode = priceCode;
        }
    }
    Rental(租赁)
    /**
     * 租赁类,用于绑定某个顾客租赁的影片,包含影片名和租期
     *
     * @author lune
     * @create 2017-11-14 17:35
     */
    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;
        }
    }
    Customer(顾客)
    /**
     * 顾客类,可进行多种租赁,需要打印详单
     *
     * @author lune
     * @create 2017-11-14 17:35
     */
    public class Customer {
        private String name;           //顾客名
        private Vector rentals = new Vector();  //租赁列表
    
        public Customer(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        //添加租赁信息
        public void addRental(Rental rental) {
            rentals.addElement(rental);
        }
    
        //打印详单方法
        public String statement() {
            double totalAmount = 0;     //总金额
            int frequentRenterPoints = 0;  //积分点
            Enumeration items = rentals.elements(); //用户所有租赁列表
            String result = getName() + " 的租赁详单如下 :" + "\n";
            //循环遍历租赁影片,计算消费金额
            while (items.hasMoreElements()) {
                double thisAmount = 0;  //当前单个租赁金额
                Rental each = (Rental) items.nextElement();
    
                //租赁计费规则
                switch (each.getMovie().getPriceCode()) {
                    case Movie.REGULAR:   //普通片,起步价为2元,租期超过2天的部分每天1.5元
                        thisAmount += 2;
                        if (each.getDaysRented() > 2)
                            thisAmount += (each.getDaysRented() - 2) * 1.5;
                        break;
                    case Movie.NEW_RELEASE: //新片,每天3元
                        thisAmount += each.getDaysRented() * 3;
                        break;
                    case Movie.CHILDRENS:  //儿童片,起步价1.5元,租期超过3天的部分每天1.5元
                        thisAmount += 1.5;
                        if (each.getDaysRented() > 3)
                            thisAmount += (each.getDaysRented() - 3) * 1.5;
                        break;
                }
                frequentRenterPoints++;     //每借一张加1个积分点
                //积分累加条件:新版本的片子,借的时间大于1天
                if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE)
                        && each.getDaysRented() > 1) {
                    frequentRenterPoints++;
                }
                //添加详单
                result += "\t" + each.getMovie().getTitle() + "\t" +
                        "\t"
                        + String.valueOf(thisAmount) + "\n";
                totalAmount += thisAmount;
    
            }
            //添加脚注
            result += "总金额为: \t" + String.valueOf(totalAmount) + "\n";
            result += "您本次消费获取: " + String.valueOf(frequentRenterPoints)
                    + " 个积分点";
            return result;
        }
    
    }
    
    其中Statement()方法实现了根据 计费规则 计算顾客消费金额以及 打印租赁详单 的功能。打印详单直接采用生成字符串的形式,statement()主要功能:打印详单,消费金额计算,积分计算。
    如下建立测试类:
    /**
     * 主方法,用于运行顾客租赁影片程序
     *
     * @author lune
     * @create 2017-11-14 17:35
     */
    public class Run {
        public static void main(String[] args){
            Movie movie1 = new Movie("福尔摩斯",0);
            Movie movie2 = new Movie("雷神   ",1);
            Movie movie3 = new Movie("熊出没 ",2);
            Rental ren1 = new Rental(movie1,8);
            Rental ren2 = new Rental(movie2,3);
            Rental ren3 = new Rental(movie3,5);
            Customer cus = new Customer("lune");
            cus.addRental(ren1);
            cus.addRental(ren2);
            cus.addRental(ren3);
            System.out.println(cus.statement());
        }
    }
    确实是能完成并且能顺利打印,结果运行如下:

    即使很顺利的完成了基本功能,但是这样设计代码存在极大的隐患,在更多大型的项目中如果只是简单的实现功能,在后期需求变更很容易就出现代码大量修改甚至重写,根本原因就是前期代码设计不合理。
    例如本案例,重新观察案例描述:

    红色标注的地方都有一个共同点,就是具有不确定性因素,即可能会变化的地方,考虑如下情况:
    规则一.不仅采用字符串格式打印,还需要提供以html格式打印
    除了statement()方法,还需要提供一个htmlStatement()方法,而且不能复用任何代码,需要将statement()中的代码大部分复制,修改其中打印的部分,因此计算消费和积分代码将 重复 出现。
    规则二.在第1种情况的基础上,需求决定修改,更改计费规则。
    因此,不仅要修改statement()中的计费部分代码,还需要在htmlStatement()中修改部分代码, 此时思考 ,为什么不将计费部分代码提取出来,这样就不需要在两个打印方法中进行重复的修改。
    规则三.在第1种情况的基础上,修改影片分类规则。
    影片分类的改变,将会影响积分的计算方式,因此又要进行重复的修改。
    在这里因为有两种打印方法,一旦计费和积分需求改变,就不得不进行两次修改,从而保证两种打印方法的一致。在大型项目中有很多类似的情况,往往两个方法核心相同,只是不同的方式体现,如果不将方法中容易变动的部分提取,就经常进行修改,一旦疏忽,将会导致只修改其中一个方法,从而导致不一致。
    下面进行重构。
    1.建立测试环境
    第一步永远是建立测试环境,因为重构的规则是在不改变原有代码的功能基础上,优化代码使之设计合理。因此,每一步重构都需要进行测试,不能导致输出和原有代码输出不一致,此案例就采用上述Run类。
    2.提取易变动、可复用的部分代码
    在statement()方法中计费代码和积分计算代码容易因为需求而变动,并且可以在多种打印方法中进行复用,因此最好单独提取出来。
    修改后的statement()部分如下:
    //循环遍历租赁影片,计算消费金额
            while (items.hasMoreElements()) {
                double thisAmount = 0;  //当前单个租赁金额
                Rental each = (Rental) items.nextElement();
                //抽取计费代码
                thisAmount = amountFor(each);
                //抽取积分计算代码
                frequentRenterPoints += getFrequentRenterPoints(each);
    
                //添加详单
                result += "\t" + each.getMovie().getTitle() + "\t" +
                        "\t"
                        + String.valueOf(thisAmount) + "\n";
                totalAmount += thisAmount;
    
            }
    //租赁计费规则
        private double amountFor(Rental rental){
            double thisAmount = 0;
            switch (rental.getMovie().getPriceCode()) {
                case Movie.REGULAR:   //普通片,起步价为2元,租期超过2天的部分每天1.5元
                    thisAmount += 2;
                    if (rental.getDaysRented() > 2)
                        thisAmount += (rental.getDaysRented() - 2) * 1.5;
                    break;
                case Movie.NEW_RELEASE: //新片,每天3元
                    thisAmount += rental.getDaysRented() * 3;
                    break;
                case Movie.CHILDRENS:  //儿童片,起步价1.5元,租期超过3天的部分每天1.5元
                    thisAmount += 1.5;
                    if (rental.getDaysRented() > 3)
                        thisAmount += (rental.getDaysRented() - 3) * 1.5;
                    break;
            }
            return thisAmount;
        }
    //积分计算规则
        private int getFrequentRenterPoints(Rental rental){
    
            //积分累加条件:新版本的片子,借的时间大于1天
            if ((rental.getMovie().getPriceCode() == Movie.NEW_RELEASE)
                    && rental.getDaysRented() > 1) {
                return 2;
            }
            return 1;     //每借一张加1个积分点
        }
    修改后要进行测试。
    规则三:明确模块职责
    很明显发现amountFor(rental)和getFrequentRenterPoints(rental)方法完全使用来自Rental类的信息,没有使用来自Customer类的信息。并且从现实生活中来看,计算消费金额以及计算顾客积分点 并不是顾客需要做的事情 ,因此,根据职责分配,这两个方法明显放错了位置。
    修改后如下:
    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 amountFor(){
            double thisAmount = 0;
            switch (getMovie().getPriceCode()) {
                case Movie.REGULAR:   //普通片,起步价为2元,租期超过2天的部分每天1.5元
                    thisAmount += 2;
                    if (getDaysRented() > 2)
                        thisAmount += (getDaysRented() - 2) * 1.5;
                    break;
                case Movie.NEW_RELEASE: //新片,每天3元
                    thisAmount += getDaysRented() * 3;
                    break;
                case Movie.CHILDRENS:  //儿童片,起步价1.5元,租期超过3天的部分每天1.5元
                    thisAmount += 1.5;
                    if (getDaysRented() > 3)
                        thisAmount += (getDaysRented() - 3) * 1.5;
                    break;
            }
            return thisAmount;
        }
    
        //积分计算规则
        public int getFrequentRenterPoints(){
    
            //积分累加条件:新版本的片子,借的时间大于1天
            if ((getMovie().getPriceCode() == Movie.NEW_RELEASE)
                    && getDaysRented() > 1) {
                return 2;
            }
            return 1;     //每借一张加1个积分点
        }
    }
    修改后要进行测试,此时发现连入参都省略了。
    再思考,计费方法真的应该放在Rental类中实现吗?先看下面规则四
    规则四:运用多态取代switch
    public double amountFor(){
            double thisAmount = 0;
            switch (getMovie().getPriceCode()) {
                case Movie.REGULAR:   //普通片,起步价为2元,租期超过2天的部分每天1.5元
                    thisAmount += 2;
                    if (getDaysRented() > 2)
                        thisAmount += (getDaysRented() - 2) * 1.5;
                    break;
                case Movie.NEW_RELEASE: //新片,每天3元
                    thisAmount += getDaysRented() * 3;
                    break;
                case Movie.CHILDRENS:  //儿童片,起步价1.5元,租期超过3天的部分每天1.5元
                    thisAmount += 1.5;
                    if (getDaysRented() > 3)
                        thisAmount += (getDaysRented() - 3) * 1.5;
                    break;
            }
            return thisAmount;
        }
    很明显的发现,Rental中的amountFor()方法采用switch语句,条件为
    getMovie().getPriceCode(),也就是说Rental类的方法,以Movie类的属性作为switch语句中的判断,可以理解为,Rental类受Movie类易变化的属性影响。这很明显不是我们想要的结果,因为我们要做的是尽量降低类与类之间的耦合。
    因此 switch使用的规则 为:最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用。
    影片类型是可能变化的,因此不应该将Moive对象中易变的影片类型传给Rental对象,根据switch的规则,应该在Movie对象中使用,因为变化的是Moive对象中的影片类型。因此将计费方法挪到Movie类(规则三提到的位置错误)。将Rental对象的租期传给Moive对象。
    Movie类中的getCharge()方法:
    public double getCharge(int daysRented){
            double thisAmount = 0;
            switch (getPriceCode()) {
                case REGULAR:   //普通片,起步价为2元,租期超过2天的部分每天1.5元
                    thisAmount += 2;
                    if (daysRented > 2)
                        thisAmount += (daysRented - 2) * 1.5;
                    break;
                case NEW_RELEASE: //新片,每天3元
                    thisAmount += daysRented * 3;
                    break;
                case CHILDRENS:  //儿童片,起步价1.5元,租期超过3天的部分每天1.5元
                    thisAmount += 1.5;
                    if (daysRented > 3)
                        thisAmount += (daysRented - 3) * 1.5;
                    break;
            }
            return thisAmount;
        }
    Rental类中的amountFor()方法:
    public double amountFor(){
            return getMovie().getCharge(daysRented);
        }
    说道这里,回归到规则三的主题"运用多态取代Switch", 多种影片类型,每种拥有自己特有的计费方式,因此完全可以建立Moive的多个子类,每种实现自己特定的计费方法,终于可以开始说继承了,如下图:

    这么做其实是不行的,如果现在有一个movie对象,是新片,过了一段时间就要更改类型,改为普通片怎么办?一个影片在自己的生命周期内是可以修改自己的分类的,但是一个对象却不能改变自己所属的类。这时需要用到"状态模式",如下图:

    实质就是定义了一个State(状态)接口,在这个接口内,影片的不同状态(类型)都有一个对应的方法,实现这个接口便拥有了不同的影片类型,因此,只需要在movie对象中引用price对象,修改引用就可以随时改变自己的分类。开始动手修改代码:
    此时,movie对象不再需要priceCode来指定自己的分类,而是通过price对象来表示,因此在Price中指定getPriceCode()方法。
    public interface Price {
        int getPriceCode();
        double getCharge(int daysRented);
    }
    class RegularPrice implements Price {
    
        @Override
        public int getPriceCode() {
            return Movie.REGULAR;
        }
    
        @Override
        public double getCharge(int daysRented) {
            double thisAmount = 2;
            if (daysRented > 2)
                thisAmount += (daysRented - 2) * 1.5;
            return thisAmount;
        }
    }
    
    public class NewReleasePrice implements Price {
        @Override
        public int getPriceCode() {
            return Movie.NEW_RELEASE;
        }
    
        @Override
        public double getCharge(int daysRented) {
            return daysRented * 3;
        }
    }
    public class ChildrenPrice implements Price {
        @Override
        public int getPriceCode() {
            return Movie.CHILDRENS;
        }
    
        @Override
        public double getCharge(int daysRented) {
            double thisAmount = 1.5;
            if (daysRented > 3)
                thisAmount += (daysRented - 3) * 1.5;
            return thisAmount;
        }
    }
    再看Movie类:
        private Price price;
        public Movie(String title,int priceType) {
            this.title = title;
            setPriceType(priceType);
        }
        public int getPriceType() {
            return price.getPriceCode();
        }
        public void setPriceType(int priceType) {
            switch (priceType) {
                case REGULAR:
                    price = new RegularPrice();
                    break;
                case CHILDRENS:
                    price = new ChildrenPrice();
                    break;
                case NEW_RELEASE:
                    price = new NewReleasePrice();
                    break;
            }
        }
        public double getCharge(int daysRented){
            return price.getCharge(daysRented);
        }
    在构造函数中指定类型码,通过类型码创建不同的price对象,进行不同的计费操作,当要获取影片类型时,通过price对象获取,改变影片类型则重新创建不同的price对象。
    第一个案例就到这里,如有不足,欢迎指出!
    重要说明:此博客与《重构:改善既有代码的设计》中的有些许区别,部分修改并没有列出来,主要是此博客主要描写博主对案例的重要总结,因此部分省略。例如本案例中重构后的局部变量thisAmount和frequentRenterPoints可以省略,Customer类可以不删掉amountFor()方法,调用Rental类中中的方法具体实现计费。有兴趣的朋友可以看原书《重构:改善既有代码的设计》









    展开全文
  • 关注“Java后端技术全栈”回复“面试”获取全套大厂面试资料“这段代码太烂了,我要重构一把”。——相信每个程序员都过这样的心理活动。重构是软件开发中必不可少的一个阶段。因为经历过一段时...

    关注Java后端技术全栈

    回复“面试”获取全套大厂面试资料

    “这段代码太烂了,我要重构一把”。

    ——相信每个程序员都有过这样的心理活动。

    重构是软件开发中必不可少的一个阶段。

    因为经历过一段时间的紧张迭代,软件中必然会出现各种因为赶进度或者不规范操作遗留下来的问题文件和代码,若不及时清理,后续一定会造成更多的开销。例如:

    1. 不再使用的 function,dead condition。

    2. 各种姿势 copy & paste 的代码。

    3. 不再合适的模块划分。

    以及,落后的技术栈。

    其实无论基于什么原因,程序员重构的热情总是无休无止的~   

    最近很多小伙伴问我要一些 重构 的相关资料,于是我翻箱倒柜,找到了这本 重构 领域的经典著作——《重构:改善既有代码的设计》。

    资料介绍

    该书清晰地揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖掘代码以求改善。书中给出了70多个可行的重构,每个重构都介绍了一种经过验证的代码变换手法的动机和技术。

    书中提出的重构准则将帮助读者一次一小步地修改代码,从而减少了开发过程中的风险。

    如何获取?

    1. 识别二维码并关注公众号「Java后端技术全栈」;

    2. 在公众号后台回复关键字「333」。

    展开全文
  • 前言: 捧读像这一类的书对于自己来说总带着一些神圣感,感谢自己并没有被这么宏大的主题吓退,看完了这里分享输出一下自己的笔记。 一、理解重构 什么是重构?...重构(动词): 使用一系列重构手法,...

    1240

    前言: 捧读像这一类的书对于自己来说总带着一些神圣感,感谢自己并没有被这么宏大的主题吓退,看完了这里分享输出一下自己的笔记。

    一、理解重构


    什么是重构?

    按书中 P45 中的说法,重构这个概念被分成了动词和名词的方面被分别阐述:

    • 重构(名词): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
    • 重构(动词): 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

    在过去的几十年时间里,重构这个词似乎被用来代指任何形式的代码清理,但上面的定义所指的是一种特定的清理代码的方式。重构的关键在于运用大量微小且保持软件行为的步骤,一步一步达成大规模的修改。

    每一次的重构要么很小,要么包含了若干个小步骤,即使重构没有完成,也应当可以在任何时刻停下来,所以如果有人说它们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们做的事不是重构。

    与性能优化的区别

    重构与性能优化有很多相似的地方:两者都需要修改代码,并且两者都不会改变程序的整体功能。

    两者的差别在于起目的:

    • 重构是为了让代码 “更容易理解,更容易修改”。这可能使程序运行得更快,也可能使程序运行的更慢。
    • 性能优化则只关心程序是否运行的更快。对于最终得到的代码是否容易理解和维护就不知道了。

    为什么重构?

    重构不是包治百病的灵丹妙药,也绝对不是所谓的“银弹”。重构只是一种工具,能够帮助你始终良好的控制代码而已。使用它,可能基于下面的几个目的。

    这里有一个有意思的科普(引用自百度百科:没有银弹
    ):在民俗传说里,所有能让我们充满梦靥的怪物之中,没有比狼人更可怕的了,因为它们会突然地从一般人变身为恐怖的怪兽,因此人们尝试着查找能够奇迹似地将狼人一枪毙命的银弹。我们熟悉的软件项目也有类似的特质(以一个不懂技术的管理者角度来看),平常看似单纯而率直,但很可能一转眼就变成一只时程延误、预算超支、产品充满瑕疵的怪兽,所以,我们听到了绝望的呼唤,渴望有一种银弹,能够有效降低软件开发的成本,就跟电脑硬件成本能快速下降一样。

    1. 改进软件的设计

    当人们只为短期目的而修改代码时,他们经常没有完全理解架构的整体设计。于是代码逐渐失去了自己的结构。程序员越来越难以通过阅读代码来理解原来的设计。代码结构的流失有累积效应。越难看出代码所代表的设计企图,就越难以保护其设计,于是设计就腐败得越快。

    完成同样一件事,设计欠佳的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同的语句做同样的事情,因此改进设计的一个重要方向就是消除重复代码。消除重复代码,我就可以确定所有事物和行为在代码中只表述一次,这正是优秀设计的根本。

    2. 使软件更容易理解

    所谓程序设计,很大程度上就是与计算机对话:我编写代码告诉计算机做什么,而它的响应是按照我的指示精确行动。一言以蔽之,我所做的就是填补“我想要它做什么”和“我告诉它做什么”之间的缝隙。编程的核心就在于“准确说出我想要的”。

    然而别忘了,除计算机之外,源码还有其他读者,并且很大概率还是几个月后的自己,如何更清晰地表达我想要做的,这可能就需要一些重构的手法。

    这里我联想到了软件设计的 KISS 原则:KISS 原则,Keep It Simple and Stupid ,简单的理解这句话就是,要把一个系统做的连白痴都会用。

    3. 帮助找到 BUG

    对代码的理解,可以帮助找到系统中存在的一些 BUG。搞清楚程序结构的同时,也可以对自己的假设做一些验证,这样一来 BUG 想不发现都难。

    Kent Beck 经常形容自己的一句话是:“我不是一个特别好的程序员,我只是一个有着一些特别好的习惯的还不错的的程序员。”重构能够帮助我们更有效地写出健壮的代码。

    4. 提高编程速度

    听起来可能有些反直觉,因为重构可能会花大量的时间改善设计、提高阅读性、修改 BUG,难道不是在降低开发速度嘛?

    软件开发者交谈时的故事:一开始他们进展很快,但如今想要添加一个新功能需要的时间就要长得多。他们需要花越来越多的时间去考虑如何把新功能塞进现有的代码库,不断蹦出来的bug修复起来也越来越慢。代码库看起来就像补丁摞补丁,需要细致的考古工作才能弄明白整个系统是如何工作的。这份负担不断拖慢新增功能的速度,到最后程序员恨不得从头开始重写整个系统。

    下面这幅图可以描绘他们经历的困境。

    1240

    但有些团队的境遇则截然不同。他们添加新功能的速度越来越快,因为他们能利用已有的功能,基于已有的功能快速构建新功能。

    1240

    两种团队的区别就在于软件的内部质量。需要添加新功能时,内部质量良好的软件让我可以很容易找到在哪里修改、如何修改。良好的模块划分使我只需要理解代码库的一小部分,就可以做出修改。如果代码很清晰,我引入 BUG 的可能性就会变小,即使引入了 BUG,调试也会容易得多。理想情况下,代码库会逐步演化成一个平台,在其上可以很容易地构造与其领域相关的新功能。

    这种现象被作者称为“设计耐久性假说”:通过投入精力改善内部设计,我们增加了软件的耐久性,从而可以更长时间地保持开发的快速。目前还无法科学地证明这个理论,所以说它是一个“假说”。

    20年前,行业的陈规认为:良好的设计必须在开始编程之前完成,因为一旦开始编写代码,设计就只会逐渐腐败。重构改变了这个图景。现在我们可以改善已有代码的设计,因此我们可以先做一个设计,然后不断改善它,哪怕程序本身的功能也在不断发生着变化。由于预先做出良好的设计非常困难,想要既体面又快速地开发功能,重构必不可少。

    什么时候重构?

    • 三次法则:
      第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。

    什么时候不应该重构?

    重构并不是必要,当然也有一些不那么需要重构的情况:

    • 不需要修改,那些丑陋的代码能隐藏在一个 API 之下。 只有当我需要理解其工作原理时,对其进行重构才会有价值;
    • 重写比重构容易。 这可能就需要良好的判断力和丰富的经验才能够进行抉择了。

    二、重构的几种姿势


    预备性重构:让添加新功能更容易

    重构的最佳时机就在添加新功能之前。在动手添加新功能之前,我会看看现有的代码库,此时经常会发现:如果对代码结构做一点微调,我的工作会容易得多。也许已经有个函数提供了我需要的大部分功能,但有几个字面量的值与我的需要略有冲突。如果不做重构,我可能会把整个函数复制过来,修改这几个值,但这就会导致重复代码—如果将来我需要做修改,就必须同时修改两处(更麻烦的是,我得先找到这两处)。而且,如果将来我还需要一个类似又略有不同的功能,就只能再复制粘贴一次,这可不是个好主意。

    这就好像我要往东去100公里。我不会往东一头把车开进树林,而是先往北开20公里上高速,然后再向东开100公里。后者的速度比前者要快上3倍。如果有人催着你“赶快直接去那儿”,有时你需要说:“等等,我要先看看地图,找出最快的路径。”这就是预备性重构于我的意义。

    ——Jessica Kerr

    修复bug时的情况也是一样。在寻找问题根因时,我可能会发现:如果把3段一模一样且都会导致错误的代码合并到一处,问题修复起来会容易得多。或者,如果把某些更新数据的逻辑与查询逻辑分开,会更容易避免造成错误的逻辑纠缠。用重构改善这些情况,在同样场合再次出现同样bug的概率也会降低。

    帮助理解的重构:使代码更易懂

    我需要先理解代码在做什么,然后才能着手修改。这段代码可能是我写的,也可能是别人写的。一旦我需要思考“这段代码到底在做什么”,我就会自问:能不能重构这段代码,令其一目了然?我可能看见了一段结构糟糕的条件逻辑,也可能希望复用一个函数,但花费了几分钟才弄懂它到底在做什么,因为它的函数命名实在是太糟糕了。这些都是重构的机会。

    看代码时,我会在脑海里形成一些理解,但我的记性不好,记不住那么多细节。正如 Ward Cunningham 所说,通过重构,我就把脑子里的理解转移到了代码本身。随后我运行这个软件,看它是否正常工作,来检查这些理解是否正确。如果把对代码的理解植入代码中,这份知识会保存得更久,并且我的同事也能看到。

    重构带来的帮助不仅发生在将来——常常是立竿见影。是我会先在一些小细节上使用重构来帮助理解,给一两个变量改名,让它们更清楚地表达意图,以方便理解,或是将一个长函数拆成几个小函数。当代码变得更清晰一些时,我就会看见之前看不见的设计问题。如果不做前面的重构,我可能永远都看不见这些设计问题,因为我不够聪明,无法在脑海中推演所有这些变化。Ralph Johnson说,这些初步的重构就像扫去窗上的尘埃,使我们得以看到窗外的风景。在研读代码时,重构会引领我获得更高层面的理解,如果只是阅读代码很难有此领悟。有些人以为这些重构只是毫无意义地把玩代码,他们没有意识到,缺少了这些细微的整理,他们就无法看到隐藏在一片混乱背后的机遇。

    捡垃圾式重构

    帮助理解的重构还有一个变体:我已经理解代码在做什么,但发现它做得不好,例如逻辑不必要地迂回复杂,或者两个函数几乎完全相同,可以用一个参数化的函数取而代之。这里有一个取舍:我不想从眼下正要完成的任务上跑题太多,但我也不想把垃圾留在原地,给将来的修改增加麻烦。如果我发现的垃圾很容易重构,我会马上重构它;如果重构需要花一些精力,我可能会拿一张便笺纸把它记下来,完成当下的任务再回来重构它。

    当然,有时这样的垃圾需要好几个小时才能解决,而我又有更紧急的事要完成。不过即便如此,稍微花一点工夫做一点儿清理,通常都是值得的。正如野营者的老话所说:至少要让营地比你到达时更干净。如果每次经过这段代码时都把它变好一点点,积少成多,垃圾总会被处理干净。重构的妙处就在于,每个小步骤都不会破坏代码——所以,有时一块垃圾在好几个月之后才终于清理干净,但即便每次清理并不完整,代码也不会被破坏。

    有计划的重构和见机行事的重构

    上面的例子——预备性重构、帮助理解的重构、捡垃圾式重构——都是见机行事的:我并不专门安排一段时间来重构,而是在添加功能或修复 BUG 的同时顺便重构。这是我自然的编程流的一部分。不管是要添加功能还是修复 BUG,重构对我当下的任务有帮助,而且让我未来的工作更轻松。这是一件很重要而又常被误解的事:重构不是与编程割裂的行为。你不会专门安排时间重构,正如你不会专门安排时间写 if 语句。我的项目计划上没有专门留给重构的时间,绝大多数重构都在我做其他事的过程中自然发生。

    还有一种常见的误解认为,重构就是人们弥补过去的错误或者清理肮脏的代码。当然,如果遇上了肮脏的代码,你必须重构,但漂亮的代码也需要很多重构。在写代码时,我会做出很多权衡取舍:参数化需要做到什么程度?函数之间的边界应该划在哪里?对于昨天的功能完全合理的权衡,在今天要添加新功能时可能就不再合理。好在,当我需要改变这些权衡以反映现实情况的变化时,整洁的代码重构起来会更容易。

    长久以来,人们认为编写软件是一个累加的过程:要添加新功能,我们就应该增加新代码。但优秀的程序员知道,添加新功能最快的方法往往是先修改现有的代码,使新功能容易被加入。所以,软件永远不应该被视为“完成”。每当需要新能力时,软件就应该做出相应的改变。越是在已有代码中,这样的改变就越显重要。

    不过,说了这么多,并不表示有计划的重构总是错的。如果团队过去忽视了重构,那么常常会需要专门花一些时间来优化代码库,以便更容易添加新功能。在重构上花一个星期的时间,会在未来几个月里发挥价值。有时,即便团队做了日常的重构,还是会有问题在某个区域逐渐累积长大,最终需要专门花些时间来解决。但这种有计划的重构应该很少,大部分重构应该是不起眼的、见机行事的。

    长期重构

    大多数重构可以在几分钟—最多几小时—内完成。但有一些大型的重构可能要花上几个星期,例如要替换一个正在使用的库,或者将整块代码抽取到一个组件中并共享给另一支团队使用,再或者要处理一大堆混乱的依赖关系,等等。

    即便在这样的情况下,我仍然不愿让一支团队专门做重构。可以让整个团队达成共识,在未来几周时间里逐步解决这个问题,这经常是一个有效的策略。每当有人靠近“重构区”的代码,就把它朝想要改进的方向推动一点。这个策略的好处在于,重构不会破坏代码—每次小改动之后,整个系统仍然照常工作。例如,如果想替换掉一个正在使用的库,可以先引入一层新的抽象,使其兼容新旧两个库的接口。一旦调用方已经完全改为使用这层抽象,替换下面的库就会容易得多。(这个策略叫作Branch By Abstraction[mf-bba]。)

    复审代码时重构

    至于如何在代码复审的过程中加入重构,这要取决于复审的形式。在常见的pull request模式下,复审者独自浏览代码,代码的作者不在旁边,此时进行重构效果并不好。如果代码的原作者在旁边会好很多,因为作者能提供关于代码的上下文信息,并且充分认同复审者进行修改的意图。对我个人而言,与原作者肩并肩坐在一起,一边浏览代码一边重构,体验是最佳的。这种工作方式很自然地导向结对编程:在编程的过程中持续不断地进行代码复审。

    三、坏代码长什么样?


    1240

    这让我想起之前在捧读《阿里巴巴 Java 开发手册》时学习的代码规范的问题(传送门
    ,只不过当时学习的是好的代码应该长什么样,而现在讨论的事情是:坏的代码长什么样?

    其实大部分的情况应该作为程序员的我们都有一定的共识,所以我觉得简单列一下书中提到的情况就足以说明:

    • 神秘命名

    • 重复代码

    • 过长函数

    • 过长参数列表

    • 全局数据: 全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。一次又一次,全局数据造成了一些诡异的 BUG,而问题的根源却在遥远的别处。

    • 可变数据: 对数据的修改经常导致出乎意料的结果和难以发现的 BUG。我在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据。

    • 发散式变化: 模块经常因为不同的原因在不同的方向上发生变化。

    • 散弹式修改: 每遇到某种变化,你都必须在许多不同的类内做出许多小修改。

    • 依恋情结: 所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。但有时你会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流。

    • 数据泥团: 你经常在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。

    • 基本类型偏执: 很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。

    • 重复的 switch: 在不同的地方反复使用相同的 switch 逻辑。问题在于:每当你想增加一个选择分支时,必须找到所有的 switch,并逐一更新。

    • 循环语句: 我们发现,管道操作(如 filter 和 map)可以帮助我们更快地看清被处理的元素一级处理它们的动作。

    • 冗余的元素

    • 夸夸其谈通用性: 函数或类的唯一用户是测试用例。

    • 临时字段: 有时你会看到这样的类:其内部某个字段仅为某种特定情况而定。这样的代码让人不理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。

    • 过长的消息链

    • 中间人: 过度运用委托。

    • 内幕交易: 软件开发者喜欢在模块之间筑起高墙,极其反感在模块之间大量交换数据,因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放到明面上来。

    • 过大的类

    • 异曲同工的类

    • 纯数据类: 所谓纯数据类是指:他们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。

    • 被拒绝的遗赠: 拒绝继承超类的实现,我们不介意:但如果拒绝支持超类的接口,这就难以接受了。

    • 注释: 当你感觉需要纂写注释时,请先尝试重构,试着让所有注释都变得多余。

    四、重构的一些方法


    书中花了大量的章节介绍我们应该如何重构我们的程序,有几个关键的点是我自己能够提炼出来的:找出代码中不合理的地方、结构化、容易理解、测试确保正确。总之围绕这几个点,书中介绍了大量的方法,下面结合自己的一些理解来简单概述一下吧。

    结构化代码

    结构化的代码更加便于我们阅读和理解,例如最常使用的重构方法:提炼函数

    • 动机:把意图和实现分开
     void printOwing(double amount) {
         printBanner();
         //print details
         System.out.println ("name:" + _name);
         System.out.println ("amount" + amount);
     }

    =>

     void printOwing(double amount) {
         printBanner();
         printDetails(amount);
     }
     void printDetails (double amount) {
         System.out.println ("name:" + _name);
         System.out.println ("amount" + amount);
     }

    更清楚的表达用意

    要保持软件的 KISS 原则是不容易的,但是也有一些方法可以借鉴,例如:引入解释性变量

    动机:用一个良好命名的临时变量来解释对应条件子句的意义,使语义更加清晰。

     if ( (platform.toUpperCase().indexOf("MAC") > -1) &&
         (browser.toUpperCase().indexOf("IE") > -1) &&
          wasInitialized() && resize > 0 )
    {
         // do something
    }

    =>

       final boolean isMacOs     = platform.toUpperCase().indexOf("MAC") > -1;
       final boolean isIEBrowser = browser.toUpperCase().indexOf("IE")  > -1;
       final boolean wasResized  = resize > 0;
       if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
           // do something
       }

    另外由于 lambda 表达式的盛行,我们现在有一些更加优雅易读的方法使我们的代码保持可读:以管道取代循环就是这样一种方法。

       const names = [];
       for (const i of input) {
          if (i.job === "programer")
             names.push(i.name);
       }

    =>

       const names = input
          .filter(i => i.job === "programer")
          .map(i => i.name)
       ;

    合理的组织结构

    例如上面介绍的提炼函数的方法,固然是一种很好的方式,但也应该避免过度的封装,如果别人使用了太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托(delegation),造成我在这些委托动作之间晕头转向,并且内部代码和函数名称同样清晰易读,那么就应该考虑内联函数。

    动机:①去处不必要的间接性;②可以找出有用的间接层。

     int getRating() {
         return (moreThanFiveLateDeliveries()) ? 2 : 1;
     }
     boolean moreThanFiveLateDeliveries() {
         return _numberOfLateDeliveries > 5;
     }

    =>

     int getRating() {
         return (_numberOfLateDeliveries > 5) ? 2 : 1;
     }

    合理的封装

    封装能够帮助我们隐藏细节并且,能够更好的应对变化,当我们发现我们的类太大而不容易理解的时候,可以考虑使用提炼类的方法。

    动机:类太大而不容易理解。

     class Person {
         get officeAreaCode() { return this._officeAreaCode; }
         get officeNumber() { return this._officeNumber; }
     }

    =>

     class Person {
         get officeAreaCode() { return this._telephoneNumber.areaCode; }
         get officeNumber() { return this._telephoneNumber.number; }
     }
     class TelephoneNumber {
         get areaCode() { return this._areaCode; }
         get number() { return this._number; }
     }

    反过来,如果我们发现一个类不再承担足够责任,不再有单独存在的理由的时候,我们会进行反向重构:内敛类

     class Person {
         get officeAreaCode() { return this._telephoneNumber.areaCode; }
         get officeNumber() { return this._telephoneNumber.number; }
     }
     class TelephoneNumber {
         get areaCode() { return this._areaCode; }
         get number() { return this._number; }
     }

    =>

     class Person {
         get officeAreaCode() { return this._officeAreaCode; }
         get officeNumber() { return this._officeNumber; }
     }

    简化条件表达式

    分解条件式: 我们能通过提炼代码,把一段 「复杂的条件逻辑」 分解成多个独立的函数,这样就能更加清楚地表达自己的意图。

         if (date.before (SUMMER_START) || date.after(SUMMER_END))
             charge = quantity * _winterRate + _winterServiceCharge;
         else charge = quantity * _summerRate;

    =>

         if (notSummer(date))
             charge = winterCharge(quantity);
         else charge = summerCharge (quantity);

    另外一个比较受用的一条建议就是:以卫语句取代嵌套条件式。根据经验,条件式通常有两种呈现形式。第一种形式是:所有分支都属于正常行为。第二种形式则是:条件式提供的答案中只有一种是正常行为,其他都是不常见的情况。

    精髓是:给某一条分支以特别的重视。如果使用 if-then-else 结构,你对 if 分支和 else 分支的重视是同等的。 这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句(guard clauses)就不同了,它告诉阅读者:「这种情况很罕见,如果它真的发生了,请做 一些必要的整理工作,然后退出。」

    「每个函数只能有一个入口和一个出口」的观念,根深蒂固于某些程序员的脑海里。 我发现,当我处理他们编写的代码时,我经常需要使用 Replace Nested Conditional with Guard Clauses。现今的编程语言都会强制保证每个函数只有一个入口, 至于「单一出口」规则,其实不是那么有用。在我看来,保持代码清晰才是最关键的:如果「单一出口」能使这个函数更清楚易读,那么就使用单一出口;否则就不必这么做。

     double getPayAmount() {
       double result;
       if (_isDead) result = deadAmount();
       else {
           if (_isSeparated) result = separatedAmount();
           else {
               if (_isRetired) result = retiredAmount();
               else result = normalPayAmount();
           };
       }
     return result;
     };

    =>

     double getPayAmount() {
       if (_isDead) return deadAmount();
       if (_isSeparated) return separatedAmount();
       if (_isRetired) return retiredAmount();
       return normalPayAmount();
     };

    自我测试代码

    如果认真观察程序员把最多时间耗在哪里,你就会发现,编写代码其实只占非常小的一部分。有些时间用来决定下一步干什么,另一些时间花在设计上面,最多的时间则是用来调试(debug)。每个程序员都能讲出「花一整天(甚至更多)时间只找出一只小小臭虫」的故事。修复错误通常是比较快的,但找出错误却是噩梦一场。当你修好一个错误,总是会有另一个错误出现,而且肯定要很久以后才会注意到它。 彼时你又要花上大把时间去寻找它。

    「频繁进行测试」是极限编程( extreme programming XP)[Beck, XP]的重要一 环。「极限编程」一词容易让人联想起那些编码飞快、自由而散漫的黑客(hackers), 实际上极限编程者都是十分专注的测试者。他们希望尽可能快速开发软件,而他们也知道「测试」可协助他们尽可能快速地前进。

    在重构之前,先保证一组可靠的测试用例(有自我检验的能力),这不仅有助于我们检测 BUG,其中也有一种以终为始的思想在里面,实际上,我们可以通过编写测试用例,更加清楚我们最终的函数应该长什么样子,提供什么样的服务。

    结束语


    感谢您的耐心阅读,以上就是整个学习的笔记了。

    重构不是一个一蹴而就的事,需要长期的实践和经验才能够完成得很好。重构强调的是 Be Better,那在此之前我们首先需要先动起手来搭建我们的系统,而不要一味地“完美主义”,近些时间接触的敏捷式开发也正是这样的一种思想。

    如果有兴趣阅读,这里只找到一份第一版可以在线阅读的地方,请自行食用吧:https://www.kancloud.cn/sstd521/refactor/194190


    按照惯例黏一个尾巴:

    欢迎转载,转载请注明出处!
    简书ID:@我没有三颗心脏
    github:wmyskxz
    欢迎关注公众微信号:wmyskxz
    分享自己的学习 & 学习资料 & 生活
    想要交流的朋友也可以加qq群:3382693

    转载于:https://www.cnblogs.com/wmyskxz/p/10990059.html

    展开全文
  • 读书笔记《重构 改善既有代码设计》本文github地址: https://github.com/YoungBear/MyBlog/blob/master/refactor.md重构:在不改变软件可观察行为的前提下改善其内部结构。refactoringtips: 如果你发现自己...

    读书笔记

    《重构 改善既有代码的设计》

    本文github地址:
    https://github.com/YoungBear/MyBlog/blob/master/refactor.md

    重构:在不改变软件可观察行为的前提下改善其内部结构。

    refactoring

    tips:

      如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。

      重构前,先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验能力。

      重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。

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

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

      重构(动词):使用一系列重构方法,在不改变软件可观察可观察行为的前提下,调整其结构。
      

    为何重构

    1. 重构改进软件设计
    2. 重构使软件更容易理解
    3. 重构帮助找到Bug
    4. 重构提高编程速度

    何时重构

    三次法则

    第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。

    tips:

    事不过三,三则重构。

    1. 添加新功能时重构
    2. 修补错误时重构
    3. 复审代码时重构

    第3章 代码的坏味道

    1. Duplicated Code 重复代码
    2. Long Method 过长函数
    3. Large Class 过大的类
    4. Long Parameter List 过长参数列
    5. Divergent Change 发散式变化 – 软件能够更容易被修改
    6. Shotgun Surgery 霰弹式修改 – 在很多类中做出许多小的修改
    7. Feature Envy 依恋情结 – 函数对某个类的兴趣高过对自己所处类的兴趣
    8. Data Clumps 数据泥团 – 很多相同的参数(可以新建一个类来保存)
    9. Primitive Obsession 基本类型偏执
    10. Switch Statements switch 惊悚现身
    11. Parallel Inheritance Hierarchies 平行继承体系
    12. Lazy Class 冗赘类
    13. Speculative Generality 夸夸其谈未来性
    14. Temporary Field 令人迷惑的暂时字段
    15. Message Chains 过度耦合的消息链
    16. Middle Man 中间人
    17. Inappropriate Intimacy 狎昵关系
    18. Alternative Classes with Different Interfaces 异曲同工的类
    19. Incomplete Library Class 不完美的类库
    20. Data Class 纯稚的数据类
    21. Refused Bequest 被拒绝的遗赠
    22. Comments 过多的注释

    第6章 重新组织函数

    6.1 Extract Method 提炼函数

    6.2 Inline Method 内联函数

    6.3 Inline Temp 内联临时变量

    6.4 Replace Temp with Query 以查询取代临时变量

    6.5 Introduce Explaining Variable 引入解释性变量

    6.6 Split Temporary Variable 分解临时变量

      如果某一个临时变量被赋值超过一次,并且它既不是循环变量也不是用于搜集计算结果则针对每次赋值,创造一个独立、对应的临时变量

    做法:

    1. 将新的临时变量声明为final
    2. 以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量
    3. 在第二次赋值处,重新声明原先那个临时变量
    4. 编译,测试
    5. 逐次重复上述过程。每次都在声明处对临时变量改名,并修改下次赋值之前的引用点

    6.7 Remove Assignments to Parameters 移除对参数的赋值

    做法:

    1. 建立一个临时变量,把待处理的参数值赋予它。
    2. 以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。
    3. 修改赋值语句,使其改为对新建之临时变量赋值。
    4. 编译,测试。

    6.8 Replace Method with Method Object 以函数对象取代函数

      如果一个函数中局部变量泛滥成灾,那么想分解这个函数是非常困难的。以查询取代临时变量可以助你减轻这个负担,但有时候你会发现根本无法拆解一个需要拆解的函数。这种情况下,你应该把手伸进工具箱的深处,祭出函数对象这件法宝。

    做法:

    1. 建立一个新类,根据待处理函数的用途,为这个类命名。
    2. 在新类中建立一个final字段,用以保存原先大型函数所在的对象。我们将这个字段成为“源对象”。同时,针对原函数的每个临时变量每个参数,在新类中建立一个对应的字段保存之。
    3. 在新类中建立一个构造函数,接收源对象及原函数的所有参数作为参数。
    4. 在新类中建立一个compute()函数。
    5. 将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
    6. 编译。
    7. 将旧函数的函数本体替换为这样一条语句:“创建上述新类的一个新对象,而后调用其中的compute()函数”。

       这项重构的好处是:我们可以轻松地对compute()函数采取Extract Method(提炼函数),不必担心参数传递的问题。
       

    6.9 Substitute Algorithm 替换算法

    把某一个算法替换为另一个更清晰的算法。

    第7章 在对象之间搬移特性

    7.1 Move Method (搬移函数)

    你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或者被后者调用。

    思路:
    在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。

    7.2 Move Field (搬移字段)

    你的程序中,某个字段被其所驻类之外的另一个类更多地用到。

    思路:在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。

    7.3 Extract Class (提炼类)

    某各类做了应该由两个类做的事。

    思路:建立一个新类,将相关的字段和函数从旧类搬移到新类。

    7.4 Inline Class (将类内联化)

    某个类没有做太多事情。

    思路:将这个类的所有特性搬移到另一个类中,然后移除原类。

    7.5 Hide Delegate (隐藏“委托关系”)

    客户通过一个委托类来调用另一个对象。

    思路:在服务类上建立客户所需求的所有函数,用以隐藏委托关系。

    7.6 Remove Middle Man (移除中间人)

    某个类做了过多的简单委托动作。

    思路:让客户直接调用受托类。(和7.5刚好相反)

    7.7 Introduce Foreign Method (引入外加函数)

    你需要为提供服务的类增加一个函数,但你无法修改这个类。

    思路:在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。

    7.8 Introduce Local Extension (引入本地扩展)

    你需要为服务类提供一些额外函数,但你无法修改这个类。

    思路:建立一个新类,使它包含这些额外函数。让这个扩展品成为源类的子类或包装类。

    第8章 重新组织数据

    8.1 Self Encapsulate Filed (自封装字段)

    将属性声明为private,使用get/set函数来访问。

    8.2 Replace Data Value with Object (以对象取代数据值)

    你有一个数据项,需要与其他数据和行为一起使用才有意义。

    思路:将数据项变成对象。

    8.3 Change Value to Reference (将值对象改为引用对象)

    你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象。

    思路:将这个值对象变成引用对象。

    8.4 Change Reference to Value (将引用对象改为值对象)

    你有一个引用对象,很小且不可变,而且不易管理。

    思路:将它变成值对象。

    8.5 Replace Array with Object (以对象取代数组)

    你有一个数组,其中的元素各自代表不同的东西。

    思路:以对象替换数组,其中的数组中的每个元素,以一个字段来表示。

    8.6 Duplicate Observed Data (复制“被监视数据”)

    你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。

    思路:将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重要数据。

    8.7 Change Unidirectional Association to Bidirectional (将单向关联改为双向关联)

    两个类都需要使用双方特性,但其间只有一条单向连接。

    思路:添加一个反向指针,并使修改函数能够同时更新两条连接。

    8.8 Change Bidirectional Association to Unidirectional (将双向关联改为单向关联)

    两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。

    思路:去除不必要的关联。

    8.9 Replace Magic Number with Symbolic Constant (以字面常量取代魔法数)

    你有一个字面数值,带有特别含义。

    思路:创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。

    eg. 使用PI来代替3.14

    8.10 Encapsulate Field (封装字段)

    你的类中存在一个 public 字段。

    思路:将它声明为 private, 并提供相应的访问函数。

    8.11 Encapsulate Collection (封装集合)

    有一个函数返回一个集合。

    思路:让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。

    动机:

    我们常常会在一个类中使用集合(collection,可能是array,list,set或vector)来保存一组实例。这样的类通常也会提供指针对该集合的取值/设值函数。

    但是,集合的处理方式应该和其他种类的数据略有不同。取值函数不该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者却一无所悉。这也会对用户暴露过多对象内部数据结构的信息。如果一个取值函数确实需要返回多个值,它应该避免用户直接操作对象内所保存的集合,并隐藏对象内与用户无关的数据结构。至于如何做到这一点,视你使用的 Java 版本不同而有所不同。

    另外,不应该为这整个集合提供一个设值函数,但应该提供用以为集合添加/移除元素的函数。这样,集合拥有者(对象)就可以控制元素的添加和移除。

    如果你做到以上几点,集合就可以很好地封装起来了,这便可以降低集合拥有者和用户之间的耦合度。

    8.12 Replace Record with Data Class (以数据类取代记录)

    你需要面对传统编程环境中的记录结构。

    思路:为该记录创建一个“哑”数据对象

    8.13 Replace Type Code with Class (以类取代类型码)

    类之中有一个数值类型码,但它并不影响类的行为。

    思路:以一个新的类替换该数值类型码

    8.14 Replace Type Code with Subclasses (以子类取代类型码)

    你有一个不可变的类型码,它会影响类的行为。

    思路:以子类取代这个类型码

    8.15 Replace Type Code with State/Strategy (以 State/Strategy 取代类型码)

    你有一个类型码,它会影响类的行为,但你无法通过集成手法消除它。

    思路:以状态对象取代类型码

    8.16 Replace Subclass with Fields (以字段取代子类)

    你的各个子类的唯一差别只在“返回常量数据”的函数身上。

    思路:修改这些函数,使它们返回超类中的某个(新增)字段,然后销毁子类。

    动机:

    建立子类的目的,是为了增加新特性或变化其行为。有一种变化行为被称为“常量函数(constant method)”,它们会返回一个硬编码的值。这东西有其用途:你可以让不同的子类中的同一个访问函数返回不同的值。你可以在超类中将反问函数声明为抽象函数,并在不同的子类中让它返回不同的值。

    尽管常量函数有其用途,但若子类只有常量函数,实在没有足够的存在价值。你可以在超类中设计一个与常量函数返回值相应的字段,从而完全去除这样的子类。如此一来就可以避免因继承而带来的额外复杂性。

    第9章 简化条件表达式

    9.1 Decompose Conditional (分解条件表达式)

    你有一个复杂的条件 (if-then-else) 语句。

    思路:从 if,then,else 三个段落中分别提炼出独立函数。

    动机

    程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。你必须编写代码来检查不同的条件分支、根据不同的分支做不同的事,然后,你很快就会得到一个相当长二代函数。大型函数自身就会使代码的可读性下降,而条件逻辑则会使代码更难阅读。在带有复杂条件逻辑的函数中,代码(包括检查条件分支的代码和真正实现功能的代码)会告诉你发生的事,但常常让你弄不清楚为什么会发生这样的事,这就说明代码的可读性的确大大降低了。

    和任何大块头代码一样,你可以将它分解为多个独立函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新建函数,从而更清楚地表达自己的意图。对于条件逻辑,将每个分支条件分解成新函数还可以给你带来更多好处:可以突出条件逻辑,更清楚地表达每个分支的作用,并且突出每个分支的原因。

    做法

    • 将 if 段落提炼出来,构成一个独立函数。
    • 将 then 段落和 else 段落都提炼出来,各自构成一个独立函数。

    9.2 Consolidate Conditional Expression (合并条件表达式)

    你有一系列条件测试,都得到相同结果。

    思路:将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。

    动机

    有时你会发现这样一串条件检查:检查条件各不相同,最终行为却一致。如果发现这种情况,就应该使用“逻辑或”和“逻辑与”将它们合并为一个条件表达式。

    之所以要合并条件代码,有两个重要原因。首先,合并后的条件代码会告诉你“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这一次检查的用意更清晰。当然, 合并前和合并后的代码有着相同的效果,但原先代码传达出的信息却是“这里有一些各自独立的条件测试,它们只是恰好同时发生”。其次,这项重构往往可以为你使用 Extract Method 做好准备。将检查条件提炼成一个独立函数对于理清代码意义非常有用,因为它把描述“做什么”的语句换成了“为什么这样做”。

    条件语句的合并理由也同时指出了不要合并的理由:如果你认为这些检查的确彼此独立,的确不应该被视为同一次检查,那么就不要使用本项重构。因为在这种情况下,你的代码已经很清楚表达出自己的意义。

    9.3 Consolidate Duplicate Conditional Fragments (合并重复的条件片段)

    在条件表达式的每个分支上有着相同的一段代码。

    思路:将这段重复代码搬移到条件表达式之外

    动机:

    有时你会发现,一组条件表达式的所有分支都执行了相同的某段代码。如果是这样,你就应该将这段代码搬移到条件表达式外面。这样,代码才能更清楚地表明哪些东西随条件的变化而变化、哪些东西保持不变。

    9.4 Remove Control Flag (移除控制标记)

    在一系列布尔表达式中,某个变量带有“控制标记”(control flag) 的作用。

    思路:以 break 语句或 return 语句取代控制标记。

    9.5 Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)

    函数中的条件逻辑使人难以看清正常的执行路径。

    思路:使用卫语句表现所有特殊情况。

    eg.

    double getPayAmount() {
        double result;
        if (_isDead) result = deadAmount();
        else {
            if (_isSeparated) result = separatedAmount();
            else {
                if (_isReetired) result = retiredAmount();
                else result = normalPayAmount();
            }
        }
        return result;
    }

    重构之后:

    double getPayAmount() {
        if (_isDead) return deadAmount();
        if (_isSeparated) return separatedAmount();
        if (_isRetired) return retiredAmount();
        return normalPayAmount();
    }

    动机:

    根据我的经验,条件表达式通常有两种表现形式。第一种形式是:所有分支都属于正常行为。第二种形式是:条件表达式提供的答案中只有一种是正常行为,其他都不是不常见的情况。

    这两类条件表达式有不同的用途,这一点应该通过代码表现出来。如果两条分支都是正常行为,就应该使用形如 if…else… 的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句” (guard clauses)。

    Replace Nested Conditional with Guard Clauses 的精髓就是:给某一条分支以特别的重视。如果使用 if-then-else 结构,你对 if 分支
    和 else 分支的重视是同等的。这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它告诉阅读者:“这种情况很罕见,如果它真的发生了,请做一些必要的整理工作,然后退出。”

    “每个函数只能有一个入口和一个出口”的观念,根深蒂固于某些程序员的脑海里。我发现,当我处理他们编写的代码时,经常需要使用这项重构。现今的编程语言都会强制保证每个函数只有一个入口,至于“单一出口”规则,其实不是那么有用。在我看来,保持代码清晰才是最关键的:如果单一出口能使这个函数更清楚易读,那么就使用单一出口;否则就不必这么做。

    嵌套条件代码往往由那些深信“每个函数只能有一个出口”的程序员写出。我发现那条规则实在有点太简单粗暴了。如果对函数剩余部分不再有兴趣,当然应该立刻退出。引导阅读者去看一个没有用的else区段,只会妨碍他们的理解。

    范例:将条件反转

    我们常常可以将条件表达式反转,从而实现该项重构。

    初始代码:

    public double getAdjustedCapital() {
        double result = 0.0;
        if (_capital > 0.0) {
            if (_intRate > 0.0 && _duration > 0.0) {
                result = (_income / _duration) * ADJ_FACTOR;
            }
        }
        return result;
    }

    我们将逐一进行替换。不过这次在插入卫语句时,我们需要将相应的条件反转过来:

    public double getAdjustedCapital() {
        double result = 0.0;
        if (_capital <= 0.0) return result;//将这个条件反转,并使用卫语句(Guard Glauses)
        if (_intRate > 0.0 && _duration > 0.0) {
                result = (_income / _duration) * ADJ_FACTOR;
        }
        return result;
    }

    下一个条件稍微复杂一点,所以我们分两步进行逆反。首先加入一个逻辑非操作:

    public double getAdjustedCapital() {
        double result = 0.0;
        if (_capital <= 0.0) return result;
        if (!(_intRate > 0.0 && _duration > 0.0)) return result;//加入逻辑非操作,并使用卫语句
        result = (_income / _duration) * ADJ_FACTOR;
        return result;
    }

    将逻辑非简化:

    public double getAdjustedCapital() {
        double result = 0.0;
        if (_capital <= 0.0) return result;
        if (_intRate <= 0.0 || _duration <= 0.0)) return result;//简化逻辑非操作
        result = (_income / _duration) * ADJ_FACTOR;
        return result;
    }

    这时候,我比较喜欢在卫语句内返回一个明确值,因为这样我们可以一目了然地看到卫语句返回的失败结果。此外,这种时候我们也会考虑使用 Replace Magic Number with System Constant。

    public double getAdjustedCapital() {
        double result = 0.0;
        if (_capital <= 0.0) return 0.0;//在卫语句中返回明确值
        if (_intRate <= 0.0 || _duration <= 0.0)) return 0.0;//在卫语句中返回明确值
        result = (_income / _duration) * ADJ_FACTOR;
        return result;
    }

    完成替换之后,我们同样可以将临时变量移除:

    public double getAdjustedCapital() {
        if (_capital <= 0.0) return 0.0;
        if (_intRate <= 0.0 || _duration <= 0.0)) return 0.0;
        return (_income / _duration) * ADJ_FACTOR;
    }

    9.6 Replace Conditional with Polymorphism (以多态取代条件表达式)

    你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。

    思路:将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。

    动机

    在面向对象术语中,听上去最高贵的词非“多态”莫属。多态最根本的好处就是:如果你需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式。

    正因为有了多态,所以你会发现:“类型码的 switch 语句”以及“基于类型名称的 if-then-else 语句” 在面向对象程序中很少出现。

    多态能够给你带来很多好处。如果同一组条件表达式在程序许多地点出现,那么使用多态的收益是最大的。使用条件表达式时,如果你想添加一种新类型,就必须查找并更新所有条件表达式。但如果改用多态,只需建立一个新的子类,并在其中提供适当的函数就行了。类的用户不需要了解这个子类,这就大大降低了系统各部分之间的依赖,使系统升级更加容易。

    9.7 Introduce Null Object (引入 Null 对象)

    你需要再三检查某对象是否为null。

    思路:将null值替换为null对象

    9.8 Introduce Assertion (引入断言)

    某一段代码需要对程序状态做出某种假设。

    思路:以断言明确表现这种假设。

    double getExpenseLimit() {
        //shoule have either expense limit or a primary project
        return (_expenseLimit != NULL_EXPENSE) ?
                _expenseLimit : _primaryProject.getMemberExpenseLimit();
    }

    这项重构后:

    double getExpenseLimit() {
        Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null);
        return (_expenseLimit != NULL_EXPENSE) ?
                _expenseLimit : _primaryProject.getMemberExpenseLimit();
    }

    动机

    常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行。例如平方根计算只对正值才能进行,又例如某个对象可能假设其字段至少有一个不等于null。

    这样的假设通常并没有在代码中明确表现出来,你必须阅读整个算法才能看出。有时程序员会以注释写出这样的假设。而我要介绍的是一种更好的技术:使用断言明确标明这些假设。

    断言是一个条件表达式,应该总是为真。如果它失败,表示程序员犯了错误。因此断言的失败应该导致一个非受控异常 (unchecked exception)。断言绝对不能被系统的其他部分使用。实际上,程序最后的成品往往将断言统统删除。因此,标记“某些东西是个断言”是很重要的。

    断言可以作为交流与调试的辅助。在交流的角度上,断言可以帮助程序阅读者理解代码所做的假设;在调试的角度上,断言可以在距离bug最近的地方抓住它们。当我编写自我测试代码的时候发现,断言在调试方面的帮助变得不那么重要了,但我仍然非常看重它们在交流方面的价值。

    第10章 简化函数调用

    10.1 Rename Method (函数改名)

    函数的名称未能揭示函数的用途。

    思路:修改函数名称。

    10.2 Add Parameter (添加参数)

    某个函数需要从调用端得到更多信息。

    思路:为此函数添加一个对象参数,让该对象带进函数所需信息。

    10.3 Remove Parameter (移除参数)

    函数本体不再需要某个参数。

    思路:将该参数去除。

    10.4 Separate Query from Modifier (将查询函数和修改函数分离)

    某个函数既返回对象状态值,又修改对象状态。

    思路:建立两个不同的函数,其中一个负责查询,另一个负责修改。

    10.5 Parameterize Method (令函数携带参数)

    若干函数做了类似的工作,但在函数本体中却包含了不同的值。

    思路:建立单一函数,以参数表达那些不同的值。

    动机:

    你可能会发现这样的两个函数:它们做着类似的工作,但因少数几个值致使行为略有不同。这种情况下,你可以将这些各自分离的函数统一起来,并通过参数来处理那些变化情况,用以简化问题。这样的修改可以去除重复的代码,并提高灵活性,因为你可以用这个参数处理更多的变化情况。

    范例

    一个最简单的例子:

    class Employee {
        void tenPercentRaise() {
            salary *= 1.1;
        }
    
        void fivePercentRaise() {
            salary *= 1.05;
        }
    }

    这段代码可以替换如下:

    void raise(double factor) {
        salary *= (1 + factor);
    }

    本项重构的要点在于:以“可将少量数值视为参数”为依据,找出带有重复性的代码。

    10.6 Replace Rarameter with Explicit Methods (以明确函数取代参数)

    你有一个函数,其中完全取决于参数值而采取不同行为

    思路:针对该参数的每一个可能值,建立一个独立函数。

    eg.

    void setValue(String name, int value) {
        if (name.equals("height")) {
            _height = value;
            return;
        }
        if (name.equals("width")) {
            _width = value;
            return;
        }
        Assert.shouldNeverReachHere();
    }

    经过这项重构后:

    void setHeight(int arg) {
        _height = arg;
    }
    
    void setWidth(int arg) {
        _width = arg;
    }

    动机:

    Replace Parameter with Explicit Methods 恰恰相反于 Parameterize Method 。如果某个参数有多种可能的值,而函数内又以条件表达式检查这些参数值,并根据不同参数值做出不同的行为,那么就应该使用本项重构。调用者原本必须赋予参数适当的值,以决定该函数做出何种响应。现在,既然你提供了不同的函数给调用者使用,就可以避免出现条件表达式。此外你还可以获得编译期检查的好处,而且接口也更清楚。如果以参数值决定函数行为,那么函数用户不但需要观察该函数,而且还要判断参数值是否合法,而“合法的参数值”往往很少在文档中被清楚地提出。

    就算不考虑编译期检查的好处,只是为了获得一个清晰的接口,也值得你执行本项重构。哪怕只是给一个内部的布尔变量赋值,相比较之下,Switch.beOn() 也比 Switch.setState(true) 要清楚得多。

    但是,如果参数值不会对函数行为有太多影响,你就不应该使用 Replace Parameter with Explicit Methods。如果情况这是这样,而你也只需要通过参数为一个字段赋值,那么直接使用设置函数就行了。如果的确需要条件判断的行为,可考虑使用Replace Conditional with Polymorphism。

    做法:

    • 针对参数的每一种可能值,新建一个明确函数。
    • 修改条件表达式的每个分支,使其调用合适的新函数。
    • 修改每个分支后,编译并测试。
    • 修改原函数的每一个被调用点,改而调用上述的某个合适的新函数。
    • 编译,测试。
    • 所有调用端都修改完毕后,删除原函数。

    10.7 Preserve Whole Object (保持对象完整)

    你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。

    思路:改为传递整个对象。

    eg.

    int low = daysTempRange().getLow();
    int high = daysTempRange().getHigh();
    withinPlan = plan.withinRange(low, high);

    经过这项重构后:

    withinPlan = play.withinRange(daysTempRange());

    动机:

    有时候,你会将来自同一对象的若干项数据作为参数,传递给某个函数。这样做的问题在于:万一将来被调用函数需要新的数据项,你就必须查找并修改对此函数的所有调用。如果你把这些数据所属的整个对象传给函数,可以避免这种尴尬的处境,因为被调用函数可以向那个参数对象请求任何它想要的信息。

    除了可以使参数列更稳固之外,Preserve Whole Object 往往还能提高代码的可读性。过长的参数列很难使用,因为调用者和被调用者都必须记住这些参数的用途。此外,不使用完整对象也会造成重复代码,因为被调用函数无法利用完整对象中的函数来计算某些中间值。

    不过事情总有两面。如果你传的是数值,被调用函数就只依赖于这些数值,而不依赖它们所数的对象。但如果你传递的是整个对象,被调用函数所在的对象就需要依赖参数对象。如果这会使你的依赖结构恶化,那么就不该使用Preserve Whole Object。

    还有一种不使用 Preserve Whole Object 的理由:如果被调用函数只需要参数对象的其中一项数值,那么只传递那个数值会更好。我并不认同这种观点,因为传递一项数值和传递一个对象,至少在代码清晰度上是等价的(当然对于按值传递的参数来说,性能上可能有所差异)。更重要的考量应该放在对象之间的依赖关系上。

    如果被调用函数使用了来自另一个对象的很多数据,这可能意味着该函数实际上应该本定义在那些数据所属的对象中。所以,考虑 Preserce Whole Object 的同时,你也应该考虑Move Method。

    运用本项重构之前,你可能还没定义一个完整对象。那么就应该先用 Introduce Parameter Object 。

    还有一种常见情况:调用者将自己的若干数据作为参数,传递给被调用函数。这种情况下,如果该对象有合适的取值函数,你可以使用this取代这些参数值,并且无需担心对象依赖问题。

    10.8 Replace Parameter with Methods (以函数取代参数)

    对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。

    思路:让参数接受者取出该项参数,并直接调用前一个函数。

    eg.

    int basePrice = _quantity * _itemPrice;
    discountLevel = getDiscountLevel();
    double finalPrice = discountedPrice (basePrice, discountLevel);

    通过本项重构后:

    int basePrice = _quantity * _itemPrice;
    double finalPrice = discountedPrice (basePrice);//在discountedPrice内部调用getDiscountLevel()

    动机

    如果函数可以通过其他途径获得参数值,那么它就不应该通过参数取得该值。过长的参数列会增加程序阅读者的理解难度,因此我们应该尽可能缩短参数列的长度。

    10.9 Introduce Parameter Object(引入参数对象)

    某些参数总是很自然地同时出现。

    思路:以一个对象取代这些参数。

    动机

    你经常会看到特定的一组参数总是一起被传递。可能有好几个函数都使用这一组参数,这些函数可能隶属同一个类,也可能隶属于不同的类。这样一组参数就是所谓的 Data Clumps (数据抱团),我们可以运用一个对象包装所有这些数据,再以该对象取代它们。哪怕只是为了把这些数据组织在一起,这样做也是值得的。本项重构的价值在于缩短参数列,而你知道,过长的参数列总是难以理解的。此外,新对象所定义的访问函数还可以使代码更具一致性,这又进一步降低了理解和修改代码的难度。

    本项重构还可以带给你更多好处。当你把这些参数组织到一起之后,往往很快可以发现一些可被移至新建类的行为。通常,原本使用那些参数的函数对这一组参数会有一些共通的处理,如果将这些共通行为移到新对象中,你可以减少很多重复代码。

    10.10 Remove Setting Method (移除设值函数)

    类中的某个字段应该在对象创建时被设值,然后就不再改变。

    思路:去掉该字段的所有设值函数。

    动机

    如果你为某个字段提供了设值函数,这就按时这个字段值可以被改变。如果你不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数 (同时将该字段设为final)。这样你的意图会更加清晰,并且可以排除其值被修改的可能性————这种可能性往往是非常大的。

    如果你保留了间接访问变量的方法,就可能疆场有程序员盲目使用它们。这些人甚至会在构造函数中使用设值函数!我猜想他们或许是为了代码的一致性,但却忽略了设值函数往后可能带来的混淆。

    10.11 Hide Method (隐藏函数)

    有一个函数,从来没有被其他任何类用到。

    思路:将这个函数修改为private。

    动机

    重构往往促使你修改函数的可见度。提高函数可见度的情况很容易想象:另一个类需要用到某个函数,因此你必须提高该函数的可见度。但是要指出一个函数的可见度是否过高,就稍微困难一些。理想状况下,你可以使用工具检查所有函数,指出可被隐藏起来的函数。即使没有这样的工具,你也应该时常进行这样的检查。

    一种特别常见的情况是:当你面对一个过于丰富、提供了过多行为的接口时,就值得将非必要的取值函数和设值函数隐藏起来。尤其当你面对的是一个只有简单封装的数据容器时,情况更是如此。随着越来越多行为被放入这个类,你会发现许多取值/设值函数不再需要公开,因此可以把它们隐藏起来。如果你把取值/设值函数设为private,然后在所有地方都直接访问变量,那就可一个放心移除取值/设值函数了。

    10.12 Replace Constructor with Factory Method (以工厂函数取代构造函数)

    你希望在创建对象时不仅仅是做简单的建构动作。

    思路:将构造函数替换为工厂函数。

    动机

    使用 Replace Constructor with Factory Method 的最显而易见的动机,就是在派生子类的过程中以工厂函数取代类型码。你可能常常需要根据类型码创建相应的对象,现在,创建名单中还得加上子类,那些子类也是根据类型码来创建。然而由于构造函数只能返回单一类型的对象,因此你需要将构造函数替换为工厂函数。

    此外,如果构造函数的功能不能满足你的需要,也可以使用工厂函数来代替它。工厂函数也是 Change Value to Reference 的基础。你也可以令你的工厂函数根据参数的个数和类型,选择不同的创建行为。

    10.13 Encapsulate Downcast (封装向下转型)

    某个函数返回的对象,需要由函数调用者执行向下转型(downcast)。

    思路:将向下转型动作移到函数中。

    eg.

    Object lastReading() {
        return readings.lastElement();
    }

    通过这项重构后:

    Reading lastReading() {
        return (Reading) readings.lastElement();
    }

    10.14 Replace Error Code with Exception (以异常取代错误码)

    某个函数返回一个特定的代码,用以表示某种错误情况。

    思路:改用异常。

    eg.

    int withdraw(int amount) {
        if (amount > _balance) {
            return -1;
        } else {
            _balance -= amount;
            return 0;
        }
    }

    通过这项重构后:

    void withdraw(int amount) throws BalanceExceprion {
        if (amount > _balance) {
            throw new BalanceException();
        }
        _balance -= amount;
    }

    10.15 Replace Exception with Test (以测试取代异常)

    面对一个调用者可以预先检查的条件,你抛出了一个异常。

    思路:修改调用者,使它在调用函数之前先做检查。

    eg.

    double getValueForPeriod(int periodNumber) {
        try {
            return _values[periodNumber];
        } catch (ArrayIndexOutOfBoundsException e) {
            return 0;
        }
    }

    通过这项重构后:

    double getValueForPeriod(int periodNumber) {
        if (periodNumber >= _values.length) {
            return 0;
        }
        return _values[periodNumber];
    }

    第11章 处理概括关系

    11.1 Pull Up Field (字段上移)

    两个子类拥有相同的字段。

    思路:将该字段移至超类。

    11.2 Pull Up Method (函数上移)

    有些函数,在各个子类中产生完全相同的结果。

    思路:将该函数移至超类。

    11.3 Pull Up Constructor Body (构造函数本体上移)

    你在各个子类中拥有一些构造函数,它们的本体几乎完全一致。

    思路:在超类中新建一个构造函数,并在子类构造函数中调用它。

    eg.

    class Manager extends Employee...
        public Manager(String name, String id, int grade) {
            _name = name;
            _id = id;
            _grade = grade;
        }

    通过这项重构后:

    public Manager(String name, String id, int grade) {
        super(name, id);
        _grade = grade;
    }

    11.4 Push Down Method (函数下移)

    超类中的某个函数只与部分(而非全部)子类有关。

    思路:将这个函数移到相关的那些子类去。

    11.5 Push Down Field (字段下移)

    超类中的某个字段只被部分(而非全部)子类用到。

    思路:将这个字段移到需要它的那些子类去。

    11.6 Extract Subclass (提炼子类)

    类中的某些特性只被某些 (而非全部)实例用到。

    思路:新建一个子类,将上面所说的那一部分特性移到子类中。

    11.7 Extract Superclass (提炼超类)

    两个类有相似特性。

    思路:为这两个类建立一个超类,将相同特性移至超类。

    11.8 Extract Interface (提炼接口)

    若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。

    思路:将相同的子集提炼到一个独立接口中。

    11.9 Collapse Hierarchy (折叠继承体系)

    超类和子类之间无太大区别。

    思路:将它们合为一体。

    11.10 Form Template Method (塑造模板函数)

    你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。

    思路:将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将原函数上移至超类。

    11.11 Replace Inheritance with Delegation (以委托取代继承)

    某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。

    思路:在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉两者之间的继承关系。

    11.12 Replace Delegation with Inheritance (以继承取代委托)

    你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。

    思路:让委托类继承受托类。

    展开全文
  • 第0篇,引言为什么写这个系列 想写这个重构系列的文章已经一段时间了,至于写作的动机应该三个。  首先,是带领的两个团队的所有成员都是刚毕业不久的半新人,都充满了积极的干劲和责任心。只是在一些基础...
  • * 这段代码根本不可能在输出xml的代码中复用report()的任何行为,唯一 * 可以做的就是重写一个xmlReport(),大量重复report()中的行为,当然, * 现在这个修改还不费劲,拷贝一份report()直接修改就是了。 * ...
  • 距离《重学Java设计模式》这本PDF书籍???? 7月12日出炉以后,因为工作内容时间加长,已经两周没有推文了,对我自己来说还蛮不习惯的。也同时让我感悟到,除了上学阶段以后,想给自己投入时间真的不容易。 就像说...
  • 代码重构,可以改善既有代码设计,增强既有工程的可扩充、可维护性。随着项目需求的不断迭代,需求的不断更新,我们在项目中所写的代码也在时时刻刻的在变化之中。在一次新的需求中,你添加了某些功能模块,但这些...
  • 开头先抛出几个问题吧,这几个问题也是《重构改善既有代码设计》这本书第2章的问题。 什么是重构? 为什么要重构? 什么时候要重构? 接下来就从这几个问题出发,通过这几个问题来系统的了解重构的意义。 什么是...
  • 使用interface重构代码,面向接口,减少重复代码项目背景需要提供节目,节目集数据的增删改查,数据库使用ES(elasticsearch)重构前 →_→本文着重强调用接口重构的思路,所以只选取读取功能作为例子数据结构type ...
  • 代码重构(一):函数重构规则

    千次阅读 2018-01-06 15:43:18
    代码重构,可以改善既有代码设计,增强既有工程的可扩充、可维护性。随着项目需求的不断迭代,需求的不断更新,我们在项目中 所写的代码也在时时刻刻的在变化之中。在一次新的需求中,你添加了某些功能模块,但...
  • Github: https://github.com/iccb1013/Jade.Net我们只消耗了8/人天的时间,完成了全部工作,基于我们 Jade.Net 的开源后台代码,任何小规模的后台管理系统,都可以在极短的时间内完成。这是我们在 2017 年早些...
  • 安卓各组件介绍一、ListView二、ActionBar三、Menu四、ViewPager 、Gallery五、GridView六、ImageView七、ProgressBar八、其他2.GitHub上优秀Android开源项目3. Android开发神器1.Xabber客户端2.oschina客户端3.手机...
  • 深受国内程序员喜爱,已经超过3万多star了。 1. 算法 (1) 剑指 Offer 题解:目录根据原书第二版进行编排,代码和原书有所不同,尽量比原书更简洁。 这里面包含了非常多的程序员找工作面试和笔试需要准备的知识点...
  • 一直以来,想分享MVP的实战,因为很多项目开始并不是就是mvp架构的,可能是从传统的mvc结构变迁过来的。今天呈详给大家分享的这篇从mvc重构到mvp,让大家能看到前后的对比,又能突出...
  • 优质文章,及时送达原文地址:...他们无不希望自己的代码作品简洁清晰,又可读性强,而且还具有一定的容错能力。本文将为您带来八点建议和技巧,以帮助您编写出简洁、干练的 Java 代码...
  • 作者:小傅哥 ... 沉淀、分享、成长,让自己和他人都能有所收获!...在本次编写的重学 Java 设计模式的编写中尽可能多的用各种场景案例还介绍设计的使用,包括我们已经使用过的场景;各种类型奖品发放、多套
  • Java的前景如何,好不好自学?

    千次阅读 多人点赞 2020-06-15 20:09:37
    作为一名拥有十年编程经验的老鸟,我觉得还是很资格来回答这个问题的,毕竟刚下飞机。 首先来回答,Java 的前景如何? 在我眼里,Java 绝对是世界第一编程语言啊,PHP 表示不服,那也没办法啊,来看 TIOBE 这个...
  • 代码重构

    2018-07-11 17:07:50
    代码重构,可以改善既有代码设计,增强既有工程的可扩充、可维护性。随着项目需求的不断迭代,需求的不断更新,我们在项目中所写的代码也在时时刻刻的在变化之中。在一次新的需求中,你添加了某些功能模块,但这些...
  • 上一篇:3600万中国人在抖音“上清华”来源:https://www.zhihu.com/question/434704940/answer/1630300574 对互联网来说,重构是政治...
  • 一年前,在公司大佬的指点之下,我开始写系统级重构工具 Coca (https://github.com/phodal/coca) 。哦,不,不对,是刚开始学习 Golang,因为我的第一...
  • JEECG 3.7.7 闪电版本发布,提供5套主流UI代码生成器模板!此版本为性能和表单UI深化加强版本,简称闪电版本 (闪电般的速度,主流的Bootstrap表单风格)。平台性能访问速度提升至少3倍,表单提供Bootstrap风格,新...
  • 前言: 感谢大家观看本文,希望大家都能有所收获 ...从出来本人一直在做 Java 相关的工作,现在终于时间坐下来,写一篇关于 Java 写法的一篇文章,来探讨一下如果你真的是一个 Java 程序员...
  • Github: https://github.com/iccb1013/Jade.Net 我们只消耗了8/人天的时间,完成了全部工作,基于我们 Jade.Net 的开源后台代码,任何小规模的后台管理系统,都可以在极短的时间内完成。 这是我们在 2017 年早...
  • JEECG 3.7.8版本发布,多样化主题UI满足你不同的需求 导读 ⊙平台性能优化,速度闪电般提升 ⊙提供11套新的主流UI代码生成器模板(Bootstrap表单+BootstrapTable列表\ElementUI列表表单) ⊙表单U...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 5,453
精华内容 2,181
关键字:

重构:改善既有代码的设计javagithub

java 订阅
友情链接: CH375DRVEXE.rar