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

简化条件表达式

Decompose Conditional(分解条件表达式)

从if、then、else三个段落中分别提炼出独立函数

    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段落都提炼出来,各自构成一个独立函数

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

如果有一系列条件测试,都得到相同结果,将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数

    double disabilityAmount(){
        if(_seniority < 2) return 0;
        if(_monthsDisabled > 12) return 0;
        if(_isPartTime) return 0;
    }

    =>

    double disabilityAmount(){
        if(isNotEligableForDisability()) return 0;
    }

动机

如果一串条件检查:检查条件各不相同,最终行为却一致,就应该将它们合并为一个条件表达式,之所以要合并条件代码,有两个重要原因,首先,合并后的条件代码用意更清晰,其次,这项重构往往可以为使用Extract Method做好准备

做法

  • 确定这些条件语句都没有副作用
  • 使用适当的逻辑操作符,将一系列相关条件表达式合并为一个
  • 编译,测试
  • 对合并后的条件表达式实施Extract Method

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

在条件表达式的每个分支上有着相同的一段代码,将这段重复代码搬移到条件表达式之外

    if(isSpecialDeal()){
        total = price * 0.95;
        send();
    }
    else{
        total = price * 0.98;
        send();
    }

    =>

    if(isSpecialDeal())
        total = price * 0.95;
    else
        total = price * 0.98;
    send();

动机

有助于清楚地表明哪些东西随条件的变化而变化、哪些东西保持不变

做法

  • 鉴别出”执行方式不随条件变化而变化”的代码
  • 如果这些共通代码位于条件表达式起始处,就将它移到条件表达式之前
  • 如果这些共通代码位于条件表达式尾端,就将它移到条件表达式之后
  • 如果这些共通代码位于条件表达式中段,就需要观察共通代码之前或之后的代码是否改变了什么东西,如果的确有所改变,应该首先将共通代码向前或向后移动,移至条件表达式的起始处或尾端,再以前面所受的办法来处理
  • 如果共通代码不止一条语句,应该先使用Extract Method将共通代码提炼到一个独立函数中,再以前面所说的办法来处理

Remove Control Flag(移除控制标记)

在一系列布尔表达式中,某个变量带有”控制标记”的作用,以break语句或return语句取代控制标记

动机

用break语句和continue语句跳出复杂的条件语句

做法

  • 找出让你跳出这段逻辑的控制标记值
  • 找出对标记变量赋值的语句,代以恰当的break语句或continue语句
  • 每次替换后,编译并测试

范例:以break取代简单的控制标记

    void checkSecurity(String[] people){
        boolean found = false;
        for(int i = 0; i < people.length; i++){
            if(!found){
                if(people[i].equals("Don")){
                    sendAlert();
                    found = true;
                }
                if(people[i].equals("John")){
                    sendAlert();
                    found = true;
                }
            }
        }
    }

    =>

    void checkSecurity(String[] people){
        for(int i = 0; i < people.length; i++){
            if(people[i].equals("Don")){
                sendAlert();
                break;
            }
            if(people[i].equals("John")){
                    sendAlert();
                    break;
            }
        }
    }

范例:以return返回控制标记

    void checkSecurity(String[] people){
        String found = "";
        for(int i = 0; i < people.length; i++){
            if(found.equals("")){
                if(people[i].equals("Don")){
                    sendAlert();
                    found = "Don";
                }
                if(people[i].equals("John")){
                    sendAlert();
                    found = "John";
                }
            }
        }
        someLaterCode(found);
    }

    =>

    void checkSecurity(String[] people){
        String found = foundMiscreant(people);
        someLaterCode(found);
    }

    String foundMiscreant(String[] people){
        String found = "";
        for(int i = 0; i < people.length; i++){
            if(found.equals("")){
                if(people[i].equals("Don")){
                    sendAlert();
                    return "Don";
                }
                if(people[i].equals("John")){
                    sendAlert();
                    return "John";
                }
            }
        }
        return "";
    }

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

动机

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

如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回,这样的单独检查常常被称为”卫语句”

做法

  • 对于每个检查,放进一个卫语句,卫语句要么从函数中返回,要么就抛出一个异常
  • 每次将条件检查替换成卫语句后,编译并测试

范例:将条件反转

    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 0.0;
        if(_intRate <= 0.0 || _duration <= 0.0) return 0.0;
        return (_income / _duration) * ADJ_FACTOR;
    }

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

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

    double getSpeed(){
        switch(_type){
            case EUROPEAN:
                return getBaseSpeed();
            case AFRICAN:
                return getBaseSpeed() - getLoadFactory() * _numberOfCoconuts;
            case NORWEGIAN_BLUE:
                return (_isNailed) ? 0 : getBaseSpeed(_voltage);
        }
        throw new RuntimeException("Should be unreachable");
    }

    =>

《重构 改善既有代码的设计》——学习笔记(三)_第1张图片

动机

多态最根本的好处就是:如果需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式

做法

  • 如果要处理的条件表达式是一个更大函数中的一部分,首先对条件表达式进行分析,然后使用Extract Method将它提炼到一个独立函数中
  • 如果有必要,使用Move Method将条件表达式放置到继承结构的顶端
  • 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数,将与该子类相关的条件表达式分支复制到新建函数中,并对它进行适当调整
  • 编译,测试
  • 在超类中删除条件表达式内被复制了的分支
  • 编译,测试
  • 针对条件表达式的每个分支,重复上述过程,直到所有分支都被移动子类内函数为止
  • 将超类之中容乃条件表达式的函数声明为抽象函数

范例

    class Employee...
        int payAmount(int type){
            switch(type){
                case Employee.ENGINEER:
                    return _monthlySalary;
                case Employee.SALESMAN:
                    return _monthlySalary + _commission;
                case Employee.MANAGER:
                    return _monthlySalary + _bonus;
                default:
                    throw new IllegalArgumentException("Incorrect type code value");
            }
        }
        private Employee _type;
        int getType(){
            return _type.getTypeCode()
        }

    abstract class EmployeeType...
        abstract int getTypeCode();

    class Engineer extends EmployeeType...
        int getTypeCode(){
            return Employee.ENGINEER:
        }

    ...and other subclasses

    =>

    class Employee...
        int payAmount(){
            return _type.payAmount(this);
        }

    class EmployeeType...
        int payAmount(Employee emp){
            switch(getTypeCode()){
                case Employee.ENGINEER:
                    return emp.getMonthlySalary();
                case Employee.SALESMAN:
                    return emp.getMonthlySalary() + emp.getCommission();
                case Employee.MANAGER:
                    return emp.MonthlySalary() + emp.getBonus();
                default:
                    throw new IllegalArgumentException("Incorrect type code value");
            }
        }

    => 把条件分支语句放入子类函数中

    class EmployeeType...
        abstract int payAmount(Employee emp);

    class Engineer...
        int payAmount(Employee emp){
            return emp.getMonthlySalary();
        }
    ...

Introduce Null Object(引入Null对象)

将null值替换为null对象

动机

当实例变量的某个字段内容允许为null时,在进行操作时往往要进行非空判断,这个工作是非常繁杂的,所以不让实例变量被设为null,而是插入各式各样的空对象——它们都知道如何正确地显示自己,这样就可以摆脱大量过程化的代码

空对象一定是常量,它们的任何成分都不会发生变化,因此可以使用Singleton模式来实现它们

做法

  • 为源类建立一个子类,使其行为就像是源类的null版本,在源类和null子类中都加上isNull()函数,前者的应该返回false,后者的应该返回true,或者建立一个nullable接口,将isNull()函数放入其中,让源类实现这个接口
  • 编译
  • 找出所有”索求源对象却获得一个null”的地方,修改这些地方,使它们改而获得一个空对象
  • 找出所有”将源对象与null做比较”的地方,修改这些地方,使它们调用isNull()函数
  • 编译,测试
  • 找出这样的程序点:如果对象不是null,做A动作,否则做B动作
  • 对于每一个上述地点,在null类中覆写A动作,使其行为和B动作相同
  • 使用上述被覆写的动作,然后删除”对象是否等于null”的条件测试,编译并测试

范例

    class Site...
        Customer getCustomer(){
            return _customer;
        }
        Customer _customer;

    class Customer...
        public String getName(){...}
        public BillingPlan getPlan(){...}
        public PaymentHistory getHistory(){...}

    public class PaymentHistory...
        int getWeesDelingquentInLastYear()

    Customer customer = site.getCustomer();
    BillingPlan plan;
    if(customer == null) plan = BillingPlan.basic();
    else plan = customer.getPlan();
    ...

    => 

    class NullCustomer extens Customer{
        public boolean isNull(){
            return true;
        }
    }

    class Customer...
        public boolean isNull(){
            return false;
        }
        static Customer new Null(){
            return new NullCustomer();
        }

    class Site...
        Customer getCustomer(){
            return (_customer == null) ? Customer.newNull() : _customer;
        }

    Customer customer = site.getCustomer();
    BillingPlan plan;
    if(customer.isNull()) plan = BillingPlan.basic();
    else plan = customer.getPlan();

Introduce Assertion(引入断言)

如果某一段代码需要对程序状态做出某种假设,以断言明确表现这种假设

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

    =>

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

动机

常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行,这时应该使用断言,把不符合条件的假设标明出来

断言可以作为交流与调试的辅助,在交流的角度上,断言可以帮助程序阅读者理解代码所做的假设;在调试的角度上,断言可以在距离bug最近的地方抓住它们

在一段逻辑中加入断言是有好处的,因为它迫使你重新考虑这段代码的约束条件,如果不满足这些约束条件,程序也可以正常运行,断言就不会带给你任何帮助,只会把代码变得混乱,并且有可能妨碍以后的修改

做法

  • 如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况

简化函数调用

Rename Method(函数改名)

如果函数的名称未能揭示函数的用途,就应该修改函数名称

动机

将复杂的处理过程分解成小函数,并给这些函数起一个好名称

做法

  • 检查函数签名是否被超类或子类实现过,如果是,则需要针对每份实现分别进行下列步骤
  • 声明一个新函数,将它命名为你想要的新名称,将旧函数的代码复制到新函数中,并进行适当调整
  • 编译
  • 修改旧函数,令它将调用转发给新函数
  • 编译,测试
  • 找出旧函数的所有被引用点,修改它们,令它们改而引用新函数,每次修改后,编译并测试
  • 删除旧函数
  • 编译,测试

Add Parameter(添加参数)

某个函数需要从调用端得到更多信息,为此函数添加一个对象参数,让该对象带进函数所需信息

做法

  • 检查函数签名是否被超类或子类实现过,如果是,则需要针对每份实现分别进行下列步骤
  • 声明一个新函数,名称与原函数同,只是加上新添参数,将旧函数的代码复制到新函数中
  • 编译
  • 修改旧函数,令它调用新函数
  • 编译,测试
  • 找出旧函数的所有被引用点,将它们全部修改为对新函数的引用,每次修改后,编译并测试
  • 删除旧函数
  • 编译,测试

Remove Parameter(移除参数)

当函数本体不再需要某个参数时,将该参数去除

做法

  • 检查函数签名是否被超类或子类实现过,如果是,则需要针对每份实现分别进行下列步骤
  • 声明一个新函数,名称与原函数同,只是去除不必要的参数,将旧函数的代码复制到新函数中
  • 编译
  • 修改旧函数,令它调用新函数
  • 编译,测试
  • 找出旧函数的所有被引用点,将它们全部修改为对新函数的引用,每次修改后,编译并测试
  • 删除旧函数
  • 编译,测试

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

如果某个函数既返回对象状态值,又修改对象状态,就应该建立两个不同的函数,其中一个负责查询,另一个负责修改

动机

任何有返回值的函数,都不应该有看得到的副作用,如果遇到一个”既有返回值又有副作用”的函数,就应该试着将查询动作从修改动作中分割出来

做法

  • 新建一个查询函数,令它返回的值与原函数相同
  • 修改原函数,令它调用查询函数,并返回获得的结果
  • 编译,测试
  • 将调用原函数的代码改为调用查询函数,然后,在调用查询函数的那一行之前,加上对原函数的调用,每次修改后,编译并测试
  • 将原函数的返回值改为void,并删除其中所有的return语句

范例

    String foundMiscreant(String[] people){
        for(int i = 0; i < people.length; i++){
            if(people[i].equals("Don")){
                sendAlert();
                return "Don";
            }
            if(people[i].equals("John")){
                sendAlert();
                return "John";
            }
        }
        return "";
    }

    void checkSecurity(String[] people){
        String found = foundMiscreant(people);
        someLaterCode(found);
    }

    =>

    void sendAlert(String[] people){
        if(!foundPerson(people).equals(""))
            sendAlert();
    }

    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";
            }
        }
        return "";
    }   

Parameterize Method(令函数携带参数)

若干函数做了类似的工作,但在函数本体中却包含了不同的值,应该建立单一函数,以参数表达那些不同的值

动机

如果两个函数做着类似的工作,但因少数几个值致使行为略有不同,这种情况下, 应该将这些各自分离的函数统一起来,并通过参数来处理那些变化情况,用以简化问题,这样可以去除重复代码,并提高灵活性

做法

  • 新建一个带有参数的函数,使它可以替换先前所有的重复性函数
  • 编译
  • 将调用旧函数的代码改为调用新函数
  • 编译,测试
  • 对所有旧函数重复上述步骤,每次替换后,修改并测试

范例

    protected Dollars baseCharge(){
        double result = Math.min(lastUsage(), 100) * 0.03;
        if (lastUsage() > 100){
            result += (Math.min(lastUsage(), 200) - 100) * 0.05;
        } 
        if (lastUsage() > 200){
            result += (lastUsage() - 200) * 0.07;
        }
        return new Dollars(result);
    }

    =>

    protected Dollars baseCharge(){
        double result = usageInRange(0, 100) * 0.03;
        result += usageInRange(100, 200) * 0.05;
        result += usageInRange(200, Integer.MAX_VALUE) * 0.07;
        return new Dollars(result);
    }

    protected int usageInRange(int start, int end){
        if (lastUsage() > start) return Math.min(lastUsage(), end) - start;
        else return 0;
    }

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

如果一个函数,其中完全取决于参数值而采取不同行为,应该针对该参数的每一个可能值建立一个独立函数

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

    =>

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

动机

如果某个参数有多种可能的值,而函数内又以条件表达式检查这些参数值,并根据不同参数值做出不同的行为,那么就应该使用本项重构

如果参数值不会对函数行为有太多影响,就不应该使用Replace Parameter with Explicit Methods,如果的确需要条件判断的行为,可考虑使用Replace Conditional with Polymorphism

做法

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

Preserve Whole Object(保持对象完整)

如果从某个对象中取出若干值,将它们作为某一次函数调用时的参数,应该改为传递整个对象

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

    =>

    withinPlan = plan.withinRange(daysTempRange());

动机

本项重构不仅能够使参数列更稳固外,还能提高代码的可读性,但是如果传递的对象会使依赖结构恶化,就不该使用本项重构

做法

  • 对你的目标函数新添一个参数项,用以代表原数据所在的完整对象
  • 编译,测试
  • 判断哪些参数可被包含在新添的完整对象中
  • 选择上述参数之一,将被调用函数中原来引用该参数的地方,改为调用新添参数对象的相应取值函数
  • 删除该项参数
  • 编译,测试
  • 针对所有可从完整对象中获得的参数,重复上述过程
  • 删除调用端中那些带有被删参数的代码
  • 编译,测试

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

对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数,就应该让参数接收者去除该项参数,并直接调用前一个函数

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

    =>

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

动机

如果函数可以通过其他途径获得参数值,那么它就不应该通过参数取得该值

做法

  • 如果有必要,将参数的计算过程提炼到一个独立函数中
  • 将函数本体内引用该参数的地方改为调用新建的函数
  • 每次替换后,修改并测试
  • 全部替换完成后,使用Remove Parameter将该参数去掉

范例

    public double getPrice(){
        int basePrice = _quantity * _itemPrice;
        int discountLevel;
        if(_quantity > 100) discountLevel = 2;
        else discountLevel = 1;
        double finalPrice = discountedPrice(basePrice, discountLevel);
        return finalPrice;
    }

    private double discountedPrice(int basePrice, int discountLevel){
        if(discountLevel == 2) return basePrice * 0.1;
        else return basePrice * 0.05;
    }

    => 

    public double getPrice(){
        if(getDiscountLevel() == 2) return getBasePrice() * 0.1;
        else return getBasePrice() * 0.05;
    }

    private double getBasePrice(){
        return _quantity * _itemPrice;
    }

    private int getDiscountLevel(){
        if(_quantity > 100) return 2;
        else return 1;
    }

Introduce Parameter Object(引入参数对象)

某些参数总是很自然地同时出现,应该以一个对象取代这些参数

动机

缩短参数列,减少重复代码

做法

  • 新建一个类,用以表现你想替换的一组参数,将这个类设为不可变的
  • 编译
  • 针对使用该组参数的所有函数,实施Add Parameter,传入上述新建类的实例对象,并将此参数值设为null
  • 对于Data Clumps中的每一项,从函数签名中移除之,并修改调用端和函数本体,令它们都改而通过新的参数对象取得该值
  • 每去除一个参数,编译并测试
  • 将原先的参数全部去除之后,观察有无适当函数可以运用Move Method搬移到参数对象中

范例

    class Entry...
        Entry(double value, Date chargeDate){
            _value = value;
            _chargeDate = chargeDate;
        }
        Date getDate(){
            return _chargeDate;
        }
        double getValue(){
            return _value;
        }
        private Date _chargeDate;
        private double _value;

    class Account...
        double getFlowBetween(Date start, Date end){
            double result = 0;
            Enumeration e = _entries.elements();
            while(e.hasMoreElements()){
                Entry each = (Entry)e.nextElement();
                if(each.getDate().equals(start) || each.getDate().equals(end) || (each.getDate().after(start) && each.getDate().before(end))){
                    result += each.getValue();
                }
            }
            return result;
        }

        private Vector _entries = new Vector();

    client code...
        double flow = anAccount.getFlowBetween(startDate, endDate);

    =>

    class DateRange{
        DateRange(Date start, Date end){
            _start = start;
            _end = end;
        }
        Date getStart(){
            return _start;
        }
        Date getEnd(){
            return _end;
        }
        private final Date _start;
        private final Date _end;
    }

    class Account...
        double getFlowBetween( DateRange range){
            double result = 0;
            Enumeration e = _entries.elements();
            while(e.hasMoreElements()){
                Entry each = (Entry) e.nextElement();
                if(each.getDate().equals(range.getStart()) || each.getDate().equals(range.getEnd()) || 
                (each.getDate().after(range.getStart()) && each.getDate().before(range.getEnd()))){
                    result += each.getValue();
                }
            }
            return result;
        }

    client code...
        double flow = anAccount.getFlowBetween(new DateRange(startDate, endDate));

    =>

    class Account...
        double getFlowBetween(DateRange range){
            double result = 0;
            Enumeration e = _entries.elements();
            while(e.hasMoreElements()){
                Entry each = (Entry) e.nextElement();
                if(range.includes(each.getDate())){
                    result += each.getValue();
                }
            }
            return result;
        }

    class DateRange...
        boolean includes(Date arg){
            return (arg.equals(_start) || arg.equals(_end) || (arg.after(_start) && arg.before(_end)));
        }

Remove Setting Method(移除设值函数)

类中的某个字段应该在对象创建时被设值,然后就不再改变,应该去掉该字段的所有设值函数

动机

如果为某个字段提供设值函数,就暗示这个字段可以被改变

做法

  • 检查设值函数被使用的情况,看它是否只被构造函数调用,或者被构造函数所调用的另一个函数调用
  • 修改构造函数,使其直接访问设值函数所针对的那个变量
  • 编译,测试
  • 移除这个设值函数,将它所针对的字段设为final
  • 编译,测试

范例

    class Account{
        private String _id;
        Account(String id){
            setId(id);
        }
        void setId(String arg){
            _id = arg;
        }
    }

    =>

    class Account{
        private final String _id;
        Account(String id){
            _id = id;
        }
    }

Hide Method(隐藏函数)

有一个函数,从来没有被其他任何类用到,就将这个函数修改为private

做法

  • 经常检查有没有可能降低某个函数的可见度
  • 尽可能降低所有函数的可见度
  • 每完成一组函数的隐藏之后,编译并测试

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

如果你希望在创建对象时不仅仅是做简单的建构动作,就应该将构造函数替换为工厂函数

    Employee (int type){
        _type = type;
    }

    =>

    static Employee create(int type){
        return new Employee(type);
    }

动机

使用本项重构的最显而易见的动机,就是在派生子类的过程中以工厂函数取代类型码,还可以令工厂函数根据参数的个数和类型,选择不同的创建行为

做法

  • 新建一个工厂函数,让它调用现有的构造函数
  • 将调用构造函数的代码改为调用工厂函数
  • 每次替换后,编译并测试
  • 将构造函数声明为private
  • 编译

范例:根据整数(实际是类型码)创建对象

    class Employee{
        private int _type;
        static final int ENGINEER = 0;
        static final int SALESMAN = 1;
        static final int MANAGER = 2;

        Employee(int type){
            _type = type;
        }
    }

    =>

    static Employee create(int type){
        return new Employee(type);
    }

    client code...
        Employee eng = Employee.create(Employee.ENGINEER);

范例:根据字符串创建子类对象

    static Employee create(int type){
        switch(type){
            case ENGINEER:
                return new Engineer();
            case SALESMAN:
                return new Salesman();
            case MANAGER:
                return new Manager();
            default:
                throw new IllegalArgumentException("Incorrect type code value");
        }
    }

    =>

    static Employee create(String name){
        try{
            return (Employee) Class.forName(name).newInstance();
        }catch(Exception e){
            throw new IllegalArgumentException("Incorrect type code value");
        }
    }

    Employee.create("Engineer")

范例:以明确函数创建子类

    // 只有少数几个子类,而且它们都不再变化
    class Person...
        static Person createMale(){
            return new Male();
        }
        static Person createFemale(){
            return new Female();
        }

    Person kent = Person.createMale();

Encapsulate Downcast(封装向下转型)

某个函数返回的对象,需要由函数调用者执行向下转型,应该将向下转型动作移到函数中

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

    =>

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

做法

  • 找出必须对函数调用结果进行向下转型的地方
  • 将向下转型动作搬移到该函数中

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

将某个函数返回一个特定的代码,用以表示某种错误情况,改为使用异常

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

    =>

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

做法

  • 决定应该抛出受控异常还是非受控异常,如果调用者有责任在调用前检查必要状态,就抛出非受控异常
  • 找到该函数的所有调用者,对它们进行相应调整,让它们使用异常
  • 修改该函数的签名,令它反映出新用法

范例

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

        private int _balance;

    // 非受控异常
    class Account...
        void withdraw(int amount){
            Assert.isTrue("sufficient funds", amount <= _balance);
            _balance -= amount;
        }

    class Assert...
        static void isTrue(String comment, boolean test){
            if(!test){
                throw new RuntimeException("Assertion failed:" + comment);
            }
        }

    // 受控异常
    class BalanceException extends Exception{}

    try{
        account.newWithdraw(amount);
        doTheUsualThing();
    }catch(BalanceException e){
        handleOverdrawn();
    }

    void withdraw(int amount) throws BalanceException{
        try{
            newWithdraw(amount);
            return 0;
        }catch(BalanceException e){
            return -1;
        }
    }

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

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

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

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

动机

异常可协助我们避免很多复杂的错误处理逻辑,但是异常应该用于那些产生意料之外的错误的行为,而不应该成为条件检查的替代品

做法

  • 在函数调用点之前,放置一个测试语句,将函数内catch区段中的代码复制到测试句的适当if分支中
  • 在catch区段起始处加入一个断言,确保catch区段绝对不会被执行
  • 编译,测试
  • 移除所有catch区段,然后将try区段内的代码复制到try之外,然后移除try区段
  • 编译,测试

处理概括关系(继承关系)

Pull Up Field(字段上移)

如果两个子类拥有相同的字段,应该将该字段移至超类

动机

去除子类重复的数据声明和重复的行为

做法

  • 针对待提升字段,检查它们的所有被使用点,确认它们以同样的方式被使用
  • 如果这些字段的名称不同,先将它们改名,使每一个名称都和你想为超类字段取的名称相同
  • 编译,测试
  • 在超类中新建一个字段
  • 移除子类中的字段
  • 编译,测试
  • 考虑对超类的新建字段使用Self Encapsulate Field

Pull Up Method(函数上移)

有些函数,在各个子类中产生完全相同的结果,就应该将该函数移至超类

动机

避免重复的行为

做法

  • 检查待提升函数,确定它们是完全一致的
  • 如果待提升函数的签名不同,将那些签名都修改为你想要在超类中使用的签名
  • 在超类中新建一个函数,将某一个待提升函数的代码复制到其中,做适当调整,然后编译
  • 移除一个待提升的子类函数
  • 编译,测试
  • 逐一移除待提升的子类函数,直到只剩下超类中的函数为止,每次移除之后都需要测试
  • 观察该函数的调用者,看看是否可以改为使用超类类型的对象

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

如果在各个子类中拥有一些构造函数,它们的本体几乎完全一致,应该在超类中新建一个构造函数,并在子类构造函数中调用它

    class Manager extens 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;
    }

做法

  • 在超类中定义一个构造函数
  • 将子类构造函数中的共同代码搬移到超类构造函数中
  • 将子类构造函数中的共同代码删掉,改而调用新建的超类构造函数
  • 编译,测试

Push Down Method(函数下移)

超类中的某个函数只与部分(而非全部)子类有关,将这个函数移到相关的那些子类去

做法

  • 在所有子类中声明该函数,将超类中的函数本体复制到每一个子类函数中
  • 删除超类中的函数
  • 编译,测试
  • 将该函数从所有不需要它的那些子类中删掉
  • 编译,测试

Push Down Field(字段下移)

超类中的某个字段只被部分(而非全部)子类用到,将这个字段移到需要它的那些子类去

做法

  • 在所有子类中声明该字段
  • 将该字段从超类中移除
  • 编译,测试
  • 将该字段从所有不需要它的那些子类中删掉
  • 编译,测试

Extract Subclass(提炼子类)

类中的某些特性只被某些(而非全部)实例用到,新建一个子类,将上面所说的那一部分特性移到子类中

动机

当类中的某些行为只被一部分实例用到,其他实例不需要它们

做法

  • 为源类定义一个新的子类
  • 为这个新的子类提供构造函数
  • 找出调用超类构造函数的所有地点,如果它们需要的是新建的子类,令它们改而调用新构造函数
  • 逐一使用Push Down Method和Push Down Field将源类的特性移到子类去
  • 找到所有这样的字段:它们所传达的信息如今可由继承体系自身传达,以Self Encapsulate Field避免直接使用这些字段,然后将它们的取值函数替换为多态常量函数,所有使用这些字段的地方都应该以Replace Conditional with Polymorphism重构
  • 每次下移后,编译并测试

范例

    class JobItem...
        public JobItem(int unitPrice, int quantity, boolean isLabor, Employee employee){
            _unitPrice = unitPrice;
            _quantity = quantity;
            _isLabor = isLabor;
            _employee = employee;
        }
        public int getTotalPrice(){
            return getUnitPrice() * _quantity;
        }
        public int getUnitPrice(){
            return (_isLabor) ? _employee.getRate():_unitPrice;
        }
        public int getQuantity(){
            return _quantity;
        }
        public Employee getEmployee(){
            return _employee;
        }
        private int _unitPrice;
        private int _quantity;
        private Employee _employee;
        private boolean _isLabor;

    class Employee...
        public Employee(int rate){
            _rate = rate;
        }
        public int getRate(){
            return _rate;
        }
        private int _rate;

    => 上述某些行为和数据只在按工时(labor)收费的情况下才需要

    class LaborItem extends JobItem{
        public LaborItem(int unitPrice, int quantity){
            super(0, quantity, true);
            _employee = employee;
        }
        public int getUnitPrice(){
            return _employee.getRate();
        }
        protected boolean isLabor(){
            return true;
        }
        public Employee getEmployee(){
            return _employee;
        }
    }

    class JobItem...
        protected JobItem(int unitPrice, int quantity, boolean isLabor){
            _unitPrice = unitPrice;
            _quantity = quantity;
            _isLabor = isLabor;
        }
        public JobItem(int unitPrice, int quantity){
            this(unitPrice, quantity, false, null);
        }
        protected boolean isLabor(){
            return true;
        }
        public int getUnitPrice(){
            return _unitPrice;
        }
        protected Employee _employee;

    JobItem j1 = new LaborItem(0, 5);

Extract Superclass(提炼超类)

两个类有相似特性,为这两个类建立一个超类,将相同特性移至超类

做法

  • 为原本的类新建一个空白的抽象超类
  • 运用Pull Up Field、Pull Up Method和Pull Up Constructor Body逐一将子类的共同元素上移到超类
  • 每次上移后,编译并测试
  • 检查留在子类中的函数,看它们是否还有共通成分,如果有,可以使用Extract Method将共通部分再提炼出来,然后使用Pull Up Method将提炼出的函数上移到超类,如果各个子类中某个函数的整体流程很相似,也许可以使用Form Template Method
  • 将所有共通元素都上移到超类后,检查子类的所有用户,如果它们只使用共同接口,就可以把它们请求的对象类型改为超类

Extract Interface(提炼接口)

若干客户使用类接口中的同一子集,或者两个类的接口有部分相同,应该将相同的子集提炼到一个独立接口中

做法

  • 新建一个空接口
  • 在接口中声明待提炼类的共通操作
  • 让相关的类实现上述接口
  • 调整客户端的类型声明,令其使用该接口

Collapse Hierarchy(折叠继承体系)

超类和子类之间无太大区别,就将它们合为一体

动机

继承体系很容易变得过分复杂,如果某个子类并未带来该有的价值,就把超类与子类合并起来

做法

  • 选择你想移除的类:是超类还是子类
  • 使用Pull up Field和Pull up Method,或者Push Down Method和Push Down Field,把想要移除的类的所有行为和数据搬移到另一个类
  • 每次移动后,编译并测试
  • 调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类,这个动作将会影响变量的声明、参数的类型以及构造函数
  • 移除我们的目标;此时的它应该已经成为一个空类
  • 编译,测试

Form TemPlate Method(塑造模板函数)

有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同,可以将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了,然后将原函数上移至超类

动机

继承是避免重复行为的一个强大工具,只要两个子类中有类似的函数,就可以把它们提升到超类

若两个函数以相同顺序执行大致相近的操作,但是各操作不完全相同,这种情况下可以将执行操作的序列移至超类,并借助多态保证各操作仍得以保持差异性,这样的函数被称为Template Method(模板函数)

做法

  • 在各个子类中分解目标函数,使分解后的各个函数要不完全相同,要不完全不同
  • 运用Pull Up Method将各子类内完全相同的函数上移至超类
  • 对于那些完全不同的函数,实施Rename Method,使所有这些函数的签名完全相同
  • 修改上述所有签名后,编译并测试
  • 运用Pull Up Method将所有原函数逐一上移至超类,在超类中将那些代表各种不同操作的函数定义为抽象函数
  • 编译,测试
  • 移除其他子类中的原函数,每删除一个,编译并测试

范例

    class Statement...
        public String statement(){
            Enumeration rentals = _rentals.elements();
            String result = "Rental Record for " + getName() + "\n";
            while(rentals.hasMoreELements()){
                Rental each = (Rental)rentals.nextElement();
                // show figures for this rental
                result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
            }
            // add footer lines
            result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
            result += "You earched " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points";
            return result;
        }

        public String htmlStatement(){
            Enumeration rentals = _rentals.elements();
            String result = "

Rentals for" + getName() + "

\n"; while(rentals.hasMoreElements()){ Rental each = (Rental) rentals.nextElement(); // show figures for each rental result += each.getMovie().getTitle() + ": " + String.valueOf(each.getCharge()) + "
\n"; result += "On this rental you earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points

"; return result; } } => class Statement... public String value(Customer aCustomer){ Enumeration rentals = aCustomer.getRentals(); String result = headerString(aCustomer); while(rentals.hasMoreElements()){ Rental each = (Reantal) rentals.nextElement(); result += eachRentalString(each); } result += footerString(aCustomer); return result; } abstract String headerString(Customer aCustomer); abstract String eachRentalString(Rental aRental); abstract String footerString(Customer aCustomer); class TextStatement... String eachRentalString(Rental aRental){ return "\t" + aRental.getMovie().getTitle() + "\t" + String.valueOf(aRental.getCharge()) + "\n"; } String footerString(Customer aCustomer){ return "Amount owed is " + String.valueOf(aCustomer.getTotalCharge()) + "\n" + "Yout earned " + String.valueOf(aCustomer.getTotalFrequentRenterPoints()) + " frequent renter points"; }

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

某个子类只使用超类接口中的一部分,或者根本不需要继承而来的数据,应该在子类中新建一个字段用以保存超类,调整子类函数,令它改而为委托超类,然后去掉两者之间的继承关系

动机

如果以委托取代继承,可以更清楚的表明:你只需要受托类的一部分功能,接口中的哪一部分应该被使用,哪一部分应该被忽略,完全由你主导控制

做法

  • 在子类中新建一个字段,使其引用超类的一个实例,并将它初始化为this
  • 修改子类内的所有函数,让它们不再使用超类,转而使用上述那个受托字段,每次修改后,编译并测试
  • 去除两个类之间的继承关系,新建一个受托类的对象赋给受托字段
  • 针对客户端所用的每一个超类函数,为它添加一个简单的委托函数
  • 编译,测试

范例

    class MyStack extends Vector{
        public void push(Object element){
            insertElementAt(element, 0);
        }

        public Object pop(){
            Object result = firestElement();
            removeElementAt(0);
            return result;
        }
    }

    =>

    class MyStack{
        private Vector _vector = new Vector();
        public void push(Object element){
            _vector.insertElementAt(element, 0);
        }

        public Object pop(){
            Object result = _vector.firstElement();
            _vector.removeElementAt(0);
            return result;
        }
    }

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

如果在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数,可以让委托类继承受委托类

动机

如果委托类没有使用受托类的所有函数,就不应该使用本重构

如果受托对象被不止一个其他对象共享,而且受托对象是可变的,在这种情况下就不能将委托关系替换成继承关系,因为这样就无法再共享数据了

做法

  • 让委托类称为受托类的一个子类
  • 编译
  • 将受托类字段设为该字段所处对象本身
  • 去掉简单的委托函数
  • 编译并测试
  • 将所有其他涉及委托关系的代码,改为调用对象自身
  • 移除受托字段

大型重构

四个大型重构

  • Tease Apart Inheritance用于处理混乱的继承体系——这种继承体系往往以一种令人迷惑的方式组合了多个不同方面的变化
  • Convert Procedural Design to Objects可以帮助解决一个经典问题:如何处理过程式代码?
  • 如果你看到以传统的两层结构(two-tier,用户界面和数据库)方式编写代码,你可能需要使用Separate Domain from Presentation将业务逻辑与用户界面隔离开来
  • Extract Hierarchy则可以将过于复杂的类转变为一群子类,从而简化系统

Tease Apart Inheritance(梳理并分解继承体系)

如果某个继承体系同时承担两项责任,就应该建立两个继承体系,并通过委托关系让其中一个可以调用另一个

动机

混乱的继承体系是一个严重的问题,因为它会导致重复代码,使修改变得困难,因为特定问题的解决策略被分散到了整个继承体系

要指出继承体系是否承担了两项不同的责任并不困难:如果继承体系中的某一特定层级上的所有类,其子类名称都以相同的形容词开始,那么这个体系很有可能就是承担着两项不同的责任

做法

  • 首先识别出继承体系所承担的不同责任,然后建立一个二维表格,并以坐标轴标示出不同的任务
  • 判断哪一项责任更重要些,并准备将它留在当前的继承体系中,准备将另一项责任移到另一个继承体系中
  • 使用Extract Class从当前的超类提炼出一个新类,用以表示重要性稍低的责任,并在原超类中添加一个实例变量,用以保存新类的实例
  • 对应于原继承体系中的每个子类,创建上述新类的一个子类,在原继承体系的子类中,将前一步骤所添加的实例变量初始化为新建子类的实例
  • 针对原继承体系中的每个子类,使用Move Method将其中的行为搬移到与之对应的新建子类中
  • 当原继承体系中的某个子类不再有任何代码时,就将它去除
  • 重复以上步骤,直到原继承体系中的所有子类都被处理过为止,观察新继承体系,看看是否有可能对它实施其他重构手法,例如Pull Up Method或Pull Up Field

范例

Convert Procedural Design to Objects(将过程化设计转化为对象设计)

如果有一些传统过程化风格的代码,可以将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中

做法

  • 针对每一个记录类型,将其转变为只含有访问函数的哑数据对象
  • 针对每一处过程化风格,将该处的代码提炼到一个独立类中
  • 针对每一段长长的程序,实施Extract Method及其他相关重构将它分解,再以Move Method将分解后的函数分别移到它所相关的哑数据类中
  • 重复上述步骤,直到原始类中的所有函数都被移除,如果原始类是一个完全过程化的类,将它拿掉

Separate Domain from Presentation(将领域和表述/显示分离)

如果某些GUI类之中包含了领域逻辑,应该将领域逻辑分离出来,为它们建立独立的领域类

动机

MVC(模型-视图-控制器)模式的最核心价值在于:它将用户界面代码(即视图)和领域逻辑(即模型)分离了,这样会使程序的修改变得更加容易,同时也使同一业务逻辑的多展现方式成为可能

做法

  • 为每个窗口建立一个领域类
  • 如果窗口内有一张表格,新建一个类来表示其中的行,再以窗口所对应之领域类中的一个集合来容纳所有的行领域对象
  • 检查窗口中的数据,如果数据只被用于UI,就把它留着;如果数据被领域逻辑使用,而且不显示于窗口上,就用Move Field将它搬移到领域类中,如果数据同时被UI和领域逻辑使用,就对它实施Duplicate Observerd Data,使它同时存在于两处,并保持两处之间的同步
  • 检查展现类中的逻辑,实施Extract Method将展现逻辑从领域逻辑中分开,一旦隔离了领域逻辑,再运用Move Method将它移到领域类
  • 以上步骤完成后,就拥有了两组彼此分离的类:展现类用以处理GUI,领域类包含所有业务逻辑

Extract Hierarchy(提炼继承体系)

如果某个类做了太多工作,其中一部分工作是以大量条件表达式完成的,就建立继承体系,以一个子类表示一种特殊情况

做法

  • 鉴别出一种变化情况
  • 针对这种变化情况,新建一个子类,并对原始类实施Replace Constructor with Factory Class将它提炼为一个独立的类
  • 将含有条件逻辑的函数,一次一个,逐一复制到子类,然后在明确情况下(对子类明确,对超类不明确),简化这些函数
  • 重复上述过程,将所有变化情况都分离出来,直到可以将超类声明为抽象类为止
  • 删除超类中那些被所有子类覆写的函数本体,并将它们声明为抽象函数

如果非常清楚原始类会有哪些变化情况,可以使用另一种做法

  • 针对原始类的每一种变化情况,建立一个子类
  • 使用Replace Constructor with Factory Method将原始类的构造函数转变成工厂函数,并令它针对每一种变化情况返回适当的子类实例
  • 针对带有条件逻辑的函数,实施Replace Conditional with Polymorphism,如果并非整个函数的行为有所变化,而只是函数一部分有所变化,先运用Extract Method将变化部分和不变部分隔开来

你可能感兴趣的:(重构-改善既有代码的设计)