美文网首页重构
简化函数调用

简化函数调用

作者: 塞外的风 | 来源:发表于2018-06-19 23:29 被阅读0次

    0. 本章内容导图

    在对象技术中,最重要的概念莫过于“接口”,容易被理解和被使用的接口是开发良好面向对象软件的关键。本章介绍的重构手法是用来使接口变得更简洁易用的。

    简化函数调用

    1. 重构手法

    1.1 函数改名

    概要:
    函数的名称未能揭示函数的用途。
    修改函数名称。
    动机:
    a. 让函数名称准确表达它的用途
    示例:
    重构前:

    public String getTelephoneNumber() {
        return mOfficeAreaCode + "-" +mOfficeNumber;
    }
    

    重构后:

    // 此方法是否要删除,需要根据它是否被客户代码所使用
    public String getTelephoneNumber() {
        return getOfficeTelephoneNumber();
    }
    
    public String getOfficeTelephoneNumber() {
        return mOfficeAreaCode + "-" +mOfficeNumber;
    }
    

    总结:
        将复杂的处理过程分解成小函数是一种良好的编程风格,如果函数命名的好,理解整个处理过程就像阅读一行行的注释一样,理想情况是函数的名称能像自然语言一样表达出自己的功能。如果看到一个函数名称不能很好地表达它的用途,应该立马加以修改。起个好名字是成为编程高手的必备技能。
        给函数命名有一个好办法:首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释变成函数名称。
        如果旧函数已被客户调用,可以新增一个命名良好的函数实现相同功能,让旧函数转调新函数。

    1.2 添加参数

    概要:
    某个函数需要从调用端得到更多信息。
    为此函数添加一个对象参数,让该对象带进函数所需信息。
    动机:
    a. 函数需要一些过去没有的信息,通过参数将所需信息传递进来
    示例:
    重构前:

    getContact() {
        // do something
    }
    

    重构后:

    //需求变更或做其他重构,必须修改此函数,让它从对象参数获得某些信息
    getContact(Date date) {
        // do something
    }
    

    总结:
        使用本重构手法时要仔细考虑是否一定需要添加参数,是否有其他的选择。

    1.3 移除参数

    概要:
    函数本体不再需要某个参数。
    将该参数去除。
    动机:
    a. 去除冗余,简化函数调用
    示例:

    getContact(Date date) {
        // do something
    }
    

    重构后:

    //需求变更或做了其他重构,已不再需要参数对象提供信息
    getContact() {
        // do something
    }
    

    总结:
        参数代表着函数所需的信息,不同的参数有不同的意义,函数调用者需要考虑每一个调用所需要的参数,因此,如果已经不再需要某个参数了,要及时去除。

    1.4 将查询函数和修改函数分离

    概要:
    某个函数既返回对象状态值,又修改对象状态。
    建立两个不同的函数,其中一个负责查询,另一个负责修改。
    动机:
    a. 保持函数职责单一,避免函数调用的副作用
    示例:
    重构前:

    Object getTotalOutstandingAndSetReadyForSummaries() {
    }
    

    重构后:

    Object getTotalOutstanding() {
    }
    
    setReadyForSummaries() {
    }
    

    总结:
        承担多个责任的函数一般较难命名,往往需要在命名时引入And/Or等,当你遇到这种命名难题时,考察一下函数是否既做了查询又做了修改。
        每次调用查询函数同时又会修改对象某个状态值的话,很容易引起难以排查的bug。

    1.5 令函数携带参数

    概要:
    若干函数做了类似的工作,但在函数本体中却包含了不同的值。
    建立单一函数,以参数表达那些不同的值。
    动机:
    a. 通过参数处理变化的情况,简化问题,去除重复代码
    示例:
    重构前:

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

    重构后:

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

    总结:
        有时候这种方法并不能处理整个函数,但可以处理函数中的一部分代码,即便如此,也应将这部分代码提炼到一个独立函数中,用函数调用去除重复的代码。

    1.6 以明确函数取代参数

    概要:
    你有一个函数,其中完全取决于参数值而采取不同行为。
    针对该参数的每一个可能值,建立一个独立函数。
    动机:
    a. 获得更清晰的接口
    示例:
    重构前:

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

    重构后:

    void setHeight(int height) {
        mHeight = height;
    }
    
    void setWidth(int width) {
        mWidth = width;
    }
    

    总结:
        如果某个参数有多种可能的值,而函数内又以条件表达式检查这些参数值,并根据不同参数做出不同的行为,就应该使用本项重构。重构后不仅可以使接口更清晰,还避免了对参数值进行合法性检测的步骤。

    1.7 保持对象完整

    概要:
    你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。
    改为传递整个对象。
    动机:
    a. 预防被调用函数将来需要新的数据项
    b. 避免过长的参数列
    示例:
    重构前:

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

    重构后:

    withinPlan = plan.withinRange(daysTempRange());
    

    总结:
        当把对象当作参数传递给函数时,被调用函数所在的对象就需要依赖此参数对象,要谨防依赖结构恶化。
        如果被调用函数使用来自另一个对象的很多项数据,要考虑这个函数是否应该定义在数据所属的对象中。

    1.8 以函数取代参数

    概要:
    对象调用某个函数,并将所得结果作为参数,传递给另一个函数。
    而接受该参数的函数本身也能够调用前一个函数。
    让参数接受者去除该项参数,并直接调用前一个函数。
    动机:
    a. 函数可以通过其他途径获得参数值,就不应该通过参数取得该值
    示例:
    重构前:

    int basePrice = mQuantity * mItemPrice;
    discountLevel = getDiscountLevel();
    double finalPrice = discountedPrice(basePrice, discountLevel);
    

    重构后:

    int basePrice = mQuantity * mItemPrice;
    double finalPrice = discountedPrice(basePrice);
    

    总结:
        过长的参数列会增加程序阅读者的理解难度,应该尽可能地缩短参数列长度。如果所传参数本身可以被函数直接调用到,就没有必要再通过参数来传递了。

    1.9 引入参数对象

    概要:
    某些参数总是很自然地同时出现。
    以一个对象取代这些参数。
    动机:
    a. 缩短参数列
    b. 通过参数对象使代码对数据的访问更具一致性
    示例:
    重构前:

    Bill getBill(Date start, Date end) {
        //统计参数日期范围内的账单并返回
    }
    

    重构后:

    class DateRange {
        private final Date mStart;
        private final Date mEnd;
    
        public DateRange(Date start, Date end) {
            mStart = start;
            mEnd = end;
        }
    
        public Date getStart() {
            return mStart;
        }
    
        public Date getEnd() {
            return mEnd;
        }
    }
    
    Bill getBill(DateRange dateRange) {
        //统计参数日期范围内的账单并返回
    }
    

    总结:
        要留意这组参数是否总是在多个地方被一起传递。另外,把这些参数组织到一起之后,就会发现可以将一些行为移至新建的类中,还可减少很多重复代码。

    1.10 移除设值函数

    概要:
    类中的某个字段应该在对象创建时被设值,然后就不再改变。
    去掉该字段的所有设值函数。
    动机:
    a. 使字段不可被修改的意图更清晰
    b. 排除字段被修改的可能性
    示例:
    重构前:

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

    重构后:

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

    总结:
        如果不想用户修改,就不应该提供可以修改的函数,否则,是无法知晓用户会怎样使用的。另外,为了清晰地表达这层意图,应该通过一些语法修饰(如用final修饰)来明确这种意图。

    1.11 隐藏函数

    概要:
    有一个函数,从来没有被其他任何类用到。
    将这个函数修改为private。
    动机:
    a. 降低函数可见度
    示例:
    重构前:

    class Employee {
        public void unusedMethodByOtherClass() {
        }
    }
    

    重构后:

    class Employee {
        private void unusedMethodByOtherClass() {
        }
    }
    

    总结:
        将未被其他类用到的方法封装起来,还可以明确地告诉代码阅读者此方法是在类内部使用的,不属于类对外提供的服务接口。

    1.12 以工厂函数取代构造函数

    概要:
    你希望在创建对象时不仅仅是做简单的建构动作。
    将构造函数替换为工厂函数。
    动机:
    a. 在派生子类的过程中以工厂函数取代类型码
    示例:
    重构前:

    class Person {}
    class Male extends Person {}
    class Female extends Person {}
    
    Person jack = new Male();
    
    

    重构后:

    class Person {
        static Person createMale() {
            return new Male();
        }
    
        static Person createFemale() {
            return new Female();
        }
    }
    class Male extends Person {}
    class Female extends Person {}
    
    Person jack = Person.createMale();
    

    总结:
        根据使用的场景,你还可以使用简单工厂模式或工厂方法模式来解决这类问题。

    《Effective Java》一书中,Joshua Bloch介绍的第一条经验法则就是:考虑用静态工厂方法代替构造器,并介绍了这种方法的优点和缺点,以及在Java Collections Framework中的运用。

    1.13 封装向下转型

    概要:
    某个函数返回的对象,需要由函数调用者执行向下转型。
    将向下转型动作移到函数中。
    动机:
    a. 尽量给用户提供准确的类型,减少用户非必要的工作
    示例:
    重构前:

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

    重构后:

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

    总结:
        这种情况常出现返回迭代器或集合的函数身上,Java5引入泛型之后,实际上做了很多类似的工作,可以参看Java集合源码。

    1.14 以异常取代错误码

    概要:
    某个函数返回一个特定的代码,用以表示某种错误情况。
    改用异常。
    动机:
    a. 将“普通程序”和“错误处理”区分开,使程序更容易理解
    示例:
    重构前:

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

    重构后:

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

    总结:
        程序中发现错误的地方并不一定知道如何处理错误,当一段子程序发现错误时,它需要让它的调用者知道这个错误,而调用者也可能将这个错误继续沿着调用链传递上去。Unix系统和基于C语言的软件是以返回值来表示子程序的成功或失败。Java语言引入了异常这种错误处理机制,可以使得你写出更健壮、清晰的代码。

    1.15 以测试取代异常

    概要:
    面对一个调用者可以预先检查的条件,你抛出了一个异常。
    修改调用者,使它在调用函数之前先做检查。
    动机:
    a. 避免滥用异常
    示例:
    重构前:

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

    重构后:

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

    总结:
        异常的出现是程序语言的一大进步,但异常也不应该被滥用,它只应该被用于异常的、罕见的行为,指的是那些会产生意料之外的错误的行为,异常不应该成为条件检查的替代品,当可以对输入做预先检查时,就先对其进行取值检查,而不是任其产生异常,将错误抛出。

    相关文章

      网友评论

        本文标题:简化函数调用

        本文链接:https://www.haomeiwen.com/subject/teqaeftx.html