《重构 改善既有代码的设计》——学习笔记(一)

来源:互联网 发布:远控软件下载 编辑:程序博客网 时间:2024/06/07 05:52

代码的坏味道

Duplicated Code(重复代码)

  • 同一个类的两个函数含有相同的表达式,需要将重复的这段代码提出来,让这两个函数都调用这段代码
  • 两个互为兄弟的子类内含相同表达式,需要将代码提炼出来放入父类中
  • 如果两个毫不相关的类出现Duplicated Code,需要将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类,抑或这个函数可能属于第三个类,而另两个类应该引用这第三个类

Long Method(过长函数)

拥有短函数的对象会活得比较好、比较长。解释能力、共享能力、选择能力——都是由小型函数支持的。每当感觉需要以注释来说明点什么的时候,就把需要说明的东西写进一个独立函数中,并以其用途命名

Large Class(过大的类)

一个类如果拥有太多代码,就需要将其拆分,可以先确定客户端如何使用它们,然后为每一种使用方式提炼出一个接口

Long Parameter List(过长参数列)

太长的参数列难以理解,太多参数会造成前后不一致、不易使用,而且一旦需要更多数据,就不得不修改它,可以把函数所需要的东西通过对象传入

Divergent Change(发散式变化)

如果某个类经常因为不同的原因在不同的方向上发生变化,就会出现Divergent Change,如增加一个功能需要修改多处,这时应该把针对某一外界变化的所有相应修改都放在一个类中

Shotgun Surgery(霰弹式修改)

如果每遇到某种变化,就必须在许多不同的类内做出许多小修改,这就是Shotgun Surgery,应该把需要修改的代码放进同一个类,如果没有合适的类就创建一个

Feature Envy(依恋情结)

如果一个函数为了计算某个值,需要用到几个类的数据,就把函数移到最多被此函数使用的数据的类中

Data Clumps(数据泥团)

总是绑在一起出现的数据应该拥有属于它们自己的对象,评判方法是:删掉众多数据中的一项,如果其他数据没有意义了,那就应该为它们产生一个新对象

Primitive Obsession(基本类型偏执)

大多数编程环境都有两种数据:结构类型允许你将数据组织成有意义的形式;基本类型则是构成结构类型的积木块。结构总是会带来一定的额外开销,它们可能代表着数据库中的表,如果只为做一两件事而创建结构类型也可能显得太麻烦

对象技术的新手通常不愿意在小任务上运用小对象,但是对象的一个极大的价值在于:它们模糊(甚至打破)了横亘于基本数据和体积较大的类之间的界限

Switch Statements(switch惊悚现身)

面向对象程序的一个最明显特征就是:少用switch(或case)语句,从本质上来说,switch语句的问题在于重复,如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们,面向对象中的多态概念可为此带来优雅的解决方法

Parallel Inheritance Hierarchies(平行继承体系)

每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类,这就是平行继承体系,消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例

Lazy Class(冗赘类)

如果一个类的所得不值其身价,它就应该消失

Speculative Generality(夸夸其谈未来性)

当有人说”我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了,这么做的结果往往造成系统更难理解和维护,如果所有装置都会被用到,那就值得这么做;如果用不到,就应该移除

Temporary Field(令人迷惑的暂时字段)

其内某个实例变量仅为某种特定情况而设,这样的代码让人不易理解,因为通常会被认为对象在所有时候都需要它的所有变量,应该把所有和这个实例变量相关的代码都放进一个地方(或一个独立的类中)

Message Chains(过度耦合的消息链)

如果一个用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象…这就是消息链。采取这种方式,意味客户代码将与查找过程中的导航结构紧密耦合,一旦对象间的关系发生任何变化,客户端就不得不做出相应修改

可以先观察消息链最终得到的对象是用来干什么的,看看能否把使用该对象的代码提炼到一个独立的函数中,再把这个函数推入消息链。如果这条链上的某个对象有多位客户打算航行此航线的剩余部分,就加一个函数来做这件事

Middle Man(中间人)

对象的基本特性之一就是封装——对外部世界隐藏其内部细节,封装往往伴随委托

如果某个类接口有一半的函数都委托给其他类,就是过度运用委托,这时应该移除Middle Man,直接和真正负责的对象打交道。如果这样”不干实事”的函数只有少数几个,可以把它们放进调用端。如果这些Middle Man还有其他行为,可以把它变成负责对象的子类,这样既可以扩展原对象的行为,又不必负担那么多的委托动作

Inappropriate Intimacy(狎昵关系)

如果两个类过于亲密,需要花费太多时间去探究彼此的private成分,就是过分狎昵,可以把两个类的共同点提炼到一个新类中

继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望

Incomplete Library Class(不完美的库类)

如果只想修改库类的一两个函数,可以运用Introduce Foreign Method;如果想要添加一大堆额外行为,就得运用Introduce Local Extension

Data Class(纯稚的数据类)

所谓Data Class是指:它们拥有一些字段,以及用于访问(读写)这些字段函数,除此之外一无长物,这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过分细琐地操控着。需要找出这些取值/设值函数被其他类运用的地点,尝试把那些调用行为搬移到Data Class类,如果无法搬移整个函数,就产生一个可被搬移的函数

构筑测试体系

  • 确保所有测试都完全自动化,让它们检查自己的测试结果
  • 一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需要的时间
  • 撰写测试代码的最有用时机是在开始编程之前,当你需要添加特性的时候,先写相应测试代码
  • 频繁地运行测试,每次编译请把测试也考虑进去——每天至少执行每个测试一次
  • 每当你收到bug报告,请先写一个单元测试来暴露bug
  • 编写未臻完善的测试并实际运行,好过对完美测试的无尽等待
  • 考虑可能出错的边界条件,把测试火力集中在那儿
  • 测试时,别忘了检查预期的错误是否如期出现
  • 当测试数量达到一定程度后,继续增加测试带来的效益就会呈现递减态势,而非持续递增,所以应该把测试集中在可能出错的地方
  • 不要因为无法捕捉所有bug就不写测试,因为测试的确可以捕捉到大多数bug

重构列表

重构的记录格式

  • 名称
  • 简短概要:简单介绍此一重构手法的适用情景,以及它所做的事情
  • 动机:”为什么需要这个重构”和”什么情况下不该使用这个重构”
  • 做法:简明扼要地一步一步介绍如何进行此一重构
  • 范例:以一个十分简单的例子说明此重构手法如何运作

Extract Method(提炼函数)

将一段代码放进一个独立函数中,并让函数名称解释该函数的用途

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

动机

简短而命名良好的函数有以下几个优点:
- 如果每个函数的粒度都很小,那么函数被复用的机会就更大
- 会使高层函数读起来就像一系列注释
- 如果函数都是细粒度,那么函数的覆写也会更容易些

做法

  • 创建一个新函数,根据这个函数的意图来对它命名(“做什么”)
  • 将提炼出的代码从源函数复制到新建的目标函数中
  • 仔细检查提炼出的代码,看看其中是否引用了”作用域限于源函数”的变量(包括局部变量和源函数参数)
  • 检查是否有”仅用于被提炼代码段”的临时变量,如果有,在目标函数中将它们声明为临时变量
  • 检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量,如果很难这样做,或如果被修改的变量不止一个,就不能仅仅将这段代码原封不动地提炼出来
  • 将被提炼代码段中需要读取的局部变量,当作参数传给目标函数
  • 处理完所有局部变量之后,进行编译
  • 在源函数中, 将被提炼代码段替换为对目标函数的调用

范例:无局部变量

    void printOwing(){        Enumeration e = _orders.elements();        double outstanding = 0.0;        // print banner        System.out.println("***********************");        System.out.println("** Customer Owes**");        System.out.println("***********************");        // calculate outstanding        while(e.hasMoreElements()){            Order each = (Order)e.nextElement();            outstanding += each.getAmount();        }        // print details        System.out.println("name:" + _name);        System.out.println("amount" + outstanding);    }    =>    void printOwing(){        Enumeration e = _orders.elements();        double outstanding = 0.0;        printBanner();        // calculate outstanding        while(e.hasMoreElements()){            Order each = (Order)e.nextElement();            outstanding += each.getAmount();        }        // print details        System.out.println("name:" + _name);        System.out.println("amount" + outstanding);    }    void printBanner(){        // print banner        System.out.println("***********************");        System.out.println("** Customer Owes**");        System.out.println("***********************");    }

范例:有局部变量

    void printOwing(){        Enumeration e = _orders.elements();        double outstanding = 0.0;        printBanner();        // calculate outstanding        while(e.hasMoreElements()){            Order each = (Order)e.nextElement();            outstanding += each.getAmount();        }        // print details        System.out.println("name:" + _name);        System.out.println("amount" + outstanding);    }    =>    void printOwing(){        Enumeration e = _orders.elements();        double outstanding = 0.0;        printBanner();        // calculate outstanding        while(e.hasMoreElements()){            Order each = (Order)e.nextElement();            outstanding += each.getAmount();        }        printDetails(outstanding);    }    void printDetails(double outstanding){        System.out.println("name:" + _name);        System.out.println("amount" + outstanding);    }

范例:对局部变量再赋值

    void printOwing(){        Enumeration e = _orders.elements();        double outstanding = 0.0;        printBanner();        // calculate outstanding        while(e.hasMoreElements()){            Order each = (Order)e.nextElement();            outstanding += each.getAmount();        }        printDetails(outstanding);    }    =>    void printOwing(){        printBanner();        double outstanding = getOutstanding();        printDetails(outstanding);    }    double getOutstanding(){        Enumeration e = _orders.elements();        double result = 0.0;        while(e.hasMoreElements()){            Order each = (Order)e.nextElement();            result += each.getAmount();        }        return result;    }

如果需要返回的变量不止一个,最好的选择通常是:挑选另一块代码来提炼

Inline Method(内联函数)

在函数调用点插入函数本体,然后移除该函数

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

动机

如果使用了太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,那就应该去除无用的间接层

做法

  • 检查函数,确定它不具多态性,如果子类继承了这个函数,就不要将此函数内联,因为子类无法覆写一个根本不存在的函数
  • 找出这个函数的所有被调用点
  • 将这个函数的所有被调用点都替换为函数本体
  • 编译、测试
  • 删除该函数的定义

Inline Temp(内联临时变量)

将所有对该变量的引用动作,替换为对它赋值的那个表达式自身

    double basePrice = anOrder.basePrice();    return (basePrice > 100)    =>    return (anOrder.basePrice() > 100)

动机

Inline Temp多半是作为Replace Temp with Query的一部分使用,唯一单独使用的情况是:发现某个临时变量被赋予某个函数调用的返回值

做法

  • 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用
  • 如果这个临时变量并未被声明为final,那就将它声明为final,然后编译,这可以检查该临时变量是否真的只被赋值一次
  • 找到该临时变量的所有引用点,将它们替换为”为临时变量赋值”的表达式
  • 每次修改后,编译并测试
  • 修改完所有引用点后,删除该变量的声明和赋值语句
  • 编译,测试

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

将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用,此后,新函数就可被其他函数使用

    double basePrice = _quantity * _itemPrice;    if (basePrice > 1000)        return basePrice * 0.95;    else        return basePrice * 0.98;    =>    if (basePrice() > 1000)        return basePrice() * 0.95;    else        return basePrice() * 0.98;    ...    double basePrice(){        return _quantity * _itemPrice;    }

动机

临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用,如果把临时变量替换为一个查询,那么同一个类中的所有函数都将可以获得这份信息

Replace Temp with Query往往是运用Extract Method之前必不可少的一个步骤,局部变量会使代码难以被提炼,所以应该尽可能把它们替换为查询式

做法

  • 找出只被赋值一次的临时变量,超过一次考虑使用Split Temporary Variable将它分割成多个变量
  • 将该临时变量声明为final
  • 编译
  • 将”对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中
  • 编译,测试
  • 在该临时变量身上实施Inline Temp

范例

    double getPrice(){        int basePrice = _quantity * _itemPrice;        double discountFactor;        if (basePrice > 1000) discountFactor = 0.95;        else discountFactor = 0.98;        return basePrice * discountFactor;    }    =>    double getPrice(){        return basePrice() * discountFactor();    }    private double discountFactor(){        if (basePrice() > 1000) return 0.95;        else return 0.98;    }    private int basePrice(){        return _quantity * _itemPrice;    }

Introduce Explaining Variable(引入解释性变量)

将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途

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

动机

表达式有可能非常复杂而难以阅读,这种情况下,临时变量可以帮助将表达式分解为比较容易管理的形式。使用这项重构的另一种情况是,在较长算法中,可以运用临时变量来解释每一步运算的意义

做法

  • 声明一个final临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它
  • 将表达式中的”运算结果”这一部分,替换为上述临时变量
  • 编译,测试
  • 重复上述过程,处理表达式的其他部分

范例

    double price(){        // price is base price - quantity discount + shipping        return _quantity * _itemPrice - Math.max(0, _quantity - 500) * _itemPrice * 0.05 + Math.min(_quantity * _itemPrice * 0.1, 100.0);    }    =>    double price(){        return basePrice() - quantityDiscount() + shipping();    }    private double quantityDiscount(){        return Math.max(0, _quantity - 500) * _itemPrice * 0.05;    }    private double shipping(){        return Math.min(basePrice() * 0.1, 100.0);    }    private double basePrice(){        return _quantity * _itemPrice;    }

Split Temporary Variable(分解临时变量)

针对每次赋值,创造一个独立、对应的临时变量

    double temp = 2 * (_height + _width);    System.out.println(temp);    temp = _height * _width;    System.out.println(temp);    =>    final double perimeter = 2 * (_height + _width);    System.out.println(temp);    final double area = _height * _width;    System.out.println(area);

动机

很多临时变量用于保存一段冗长代码的运算结果,以便稍后使用,这种临时变量应该只被赋值一次,如果它们被赋值超过一次,就意味着它们在函数中承担了一个以上的责任,如果临时变量承担多个责任,就应该被替换(分解)为多个临时变量,每个变量只承担一个责任

做法

  • 在待分解临时变量的声明及其第一次被赋值处,修改其名称
  • 将新的临时变量声明为final
  • 以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量
  • 在第二次赋值处,重新声明原先那个临时变量
  • 编译,测试
  • 逐次重复上述过程,每次都在声明处对临时变量改名,并修改下次赋值之前的引用点

范例

    double getDistanceTravellled(int time){        double result;        double acc = _primaryForce / _mass;        int primaryTime = Math.min(time,_delay);        result = 0.5 * acc * primaryTime * primaryTime;        int secondaryTime = time - _delay;        if (secondaryTime > 0){            double primaryVel = acc * _delay;            acc = (_primaryForce + _secondaryForce) / _mass;            result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;        }        return result;    }    =>    double getDistanceTravellled(int time){        double result;        final double primaryAcc = _primaryForce / _mass;        int primaryTime = Math.min(time,_delay);        result = 0.5 * acc * primaryTime * primaryTime;        int secondaryTime = time - _delay;        if (secondaryTime > 0){            double primaryVel = primaryAcc * _delay;            final double secondaryAcc = (_primaryForce + _secondaryForce) / _mass;            result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime;        }        return result;    }

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

以一个临时变量取代该参数的位置

    int discount(int inputVal, int quantity, int yearToDate){        if (inputVal > 50) inputVal -= 2;    =>    int discount(int inputVal, int quantity, int yearToDate){        int result = inputVal;        if (inputVal > 50) result -= 2;

动机

如果把一个名为foo的对象作为参数传给某个函数,那么”对参数赋值”意味改变foo,使它引用另一个对象,这会降低代码的清晰度,而且混用了按值传递和按引用传递两种参数传递方式。如果在”被传入对象”身上进行操作就没有问题

做法

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

范例

    int discount(int inputVal, int quantity, int yearToDate){        if (inputVal > 50) inputVal -= 2;        if (quantity > 100) inputVal -= 1;        if (yearToDate > 10000) inputVal -= 4;        return inputVal;    }    =>    int discount(final int inputVal, final int quantity, final int yearToDate){        int result = inputVal;        if (inputVal > 50) result -= 2;        if (quantity > 100) result -= 1;        if (yearToDate > 10000) result -= 4;        return result;    }

Replace Method with Method Object(以函数对象取代函数)

如果有一个大型函数,可以将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段,然后就可以在同一个对象中将这个大型函数分解为多个小型函数

动机

局部变量的存在会增加函数分解难度,Replace Method with Method Object会将所有局部变量都变成函数对象的字段,然后就可以将原本的大型函数拆解变短

做法

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

范例

    Class Account        int gamma(int inputVal, int quantity, int yearToDate){            int importantValue1 = (inputVal * quantity) + delta();            int importantValue2 = (inputVal * yearToDate) + 100;            if ((yearToDate - importantValue1) > 100)                importantValue2 -= 20;            int importantValue3 = importantValue2 * 7;            // and so on.            return importantValue3 - 2 * importantValue1;        }        =>        class Gamma...            private final Account _account;            private int inputVal;            private int quantity;            private int yearToDate;            private int importantValue1;            private int importantValue2;            private int importantValue3;            Gamma(Account source, int inputValArg, int quantityArg, int yearToDateArg){                _account = source;                inputVal = inputValArg;                quantity = quantityArg;                yearToDate = yearToDateArg;            }            int compute(){                importantValue1 = (inputVal * quantity) + _account.delta();                importantValue2 = (inputVal * yearToDate) + 100;                importantThing();                int importantValue3 = importantValue2 * 7;                // and so on.                return importantValue3 - 2 * importantValue1;            }            void importantThing(){                if ((yearToDate - importantValue1) > 100)                    importantValue2 -= 20;            }            class Account                int gamma(int inputVal, int quantity, int yearToDate){                    return new Gamma(this, inputVal, quantity, yearToDate).compute();                }

Substitute Algorithm(替换算法)

将函数本体替换为另一个算法

    String foundPerson(String[] people){        for(int i = 0;i < people.length;i++){            if(people[i].equals("Don")){                return "Don";            }            if(people[i].equals("John")){                return "John";            }            if(people[i].equals("Kent")){                return "Kent";            }        }        return "";    }    =>    String foundPerson(String[] people){        List candidates = Arrays.asList(new String[]{"Don", "John", "Kent"});        for(int i = 0; i < people.length; i++){            if(candidates.contants(people[i])){                return people[i];            }            return "";        }    }

动机

如果发现做一件事可以有更清晰的方式,就应该以较清晰的方式取代复杂的方式

做法

  • 准备好另一个(替换用)算法,让它通过编译
  • 针对现有测试,执行上述的新算法,如果结果与原本结果相同,重构结束
  • 如果测试结果不同于原先,在测试和调试过程中,以旧算法为比较参照标准
0 0
原创粉丝点击