[代码重构]简化函数调用

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

 
简化函数调用

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.cnblogs.com/justuntil/p/12116118.html