本章主要针对的是对函数的重构,包括函数改名,参数增删等。良好的函数调用可以增加代码的可读性和可维护性。
Rename Method(函数改名)
我们提倡的一种编程风格是:将复杂的处理过程分解成小函数。但是,如果做得不好,这会使你费尽周折却弄不清这些小函数的用途。要避免这种麻烦,关键就在于给函数起一个好的名字。给函数命名有个好的方法:首先考虑给函数写一个注释,然后把注释变成函数的名称。
重构为
这种重构常用的做法为:
- 检查函数签名是否被超类或子类实现过。如果是,则需要针对每份实现分别进行下列步骤。
- 声明一个新函数,将它命名为你想要的新名称。将旧函数的代码复制到新函数中,并进行适当调整。
- 编译。
- 修改旧函数,令它将调用转发给新函数。
- 编译,测试。
- 找出旧函数的所有被引用点,修改它们,令它们改而引用新函数。每次修改后,编译并测试。
- 删除旧函数。
- 编译,测试。
Add Parameter(添加参数)
如果修改一个函数,而修改后的函数需要一些过去没有的信息,因此你必须给函数增加一个参数。如果有其他选择,尽量不要使用本重构手段。因为过长的参数也是代码坏味道。
重构为
这种重构的一般手段是:
- 检查函数签名是否被超类或子类实现过。如果是,则需要针对每份实现分别进行下列步骤。
- 声明一个新函数,名称与原函数同,只是加上新添参数。将旧函数的代码复制到新函数中。
- 编译。
- 修改旧函数,令它调用新函数。
- 编译,测试。
- 找出旧函数的所有被引用点,将它们全部修改为对新函数的引用。每次修改后,编译并测试。
- 删除旧函数。
- 编译,测试。
Remove Parameter(移除参数)
移除参数与增加参数相反,如果一个参数可以从另一个参数得到,或者可以从其他渠道获取。就要对入参进行删除。如果不去掉多余参数,就会在每次调用时多费一份心。
重构为
通常的做法为:
- 检查函数签名是否被超类或子类实现过。如果是,则需要针对每份实现分别进行下列步骤。
- 声明一个新函数,名称与原函数同,只是去除不必要的参数。将旧函数的代码复制到新函数中。
- 编译。
- 修改旧函数,令它调用新函数。
- 编译,测试。
- 找出旧函数的所有被引用点,将它们全部修改为对新函数的引用。每次修改后,编译并测试。
- 删除旧函数。
- 编译,测试。
Separate Query from Modifier(将查询函数与修改函数分离)
某个函数既返回对象状态值,又修改对象状态。建立两个不同的函数,其中一个负责查询,另一个负责修改。
重构为
这种重构一般的做法为:
- 新建一个查询函数,令它返回的值与原函数相同。
- 修改原函数,令它调用查询函数,并返回获得的结果。
- 编译。
- 编译,测试。
- 将调用原函数的代码改为调用查询函数。然后,在调用查询函数的那一行之前,加上对原函数的调用。每次修改后,编译并测试。
- 将原函数的返回值改为void,并删掉其中的所有return语句。
Parameterize Method(令函数携带参数)
若干函数做了类似的工作,但在函数本体中却包含了不同的值。那么建立单一函数,以参数表达那些不同的值。
重构为
这种重构一般的做法为:
- 新建一个带有参数的函数,使它可以替换先前所有的重复性函数。
- 编译。
- 将调用旧函数的代码改为调用新函数。
- 编译,测试。
- 多所有旧函数重复上述步骤,每次替换后,修改并测试。
Replace Parameter with Explicit Methods(以明确函数取代参数)
如果你有个函数,其中完全取决于参数值而采取不同行为。针对该参数的每一个可能值,建立一个独立函数。这样,可以避免出现条件表达式,而且接口更加清楚。
public void setValue(String name, int value) {
if (name.equals("height")) {
height = value;
} else if (name.equals("width")) {
width = value;
}
}
可以重构为:
public void setHeight(int height) {
height = this.height;
}
public void setWidth(int width) {
width = this.width;
}
这种重构的做法:
- 针对参数的每一种可能值,新建一个明确函数。
- 修改条件表达式的每一个分支,使其调用合适的新函数。
- 修改每个分支后,编译并测试。
- 修改原函数的每一个被调用点,该而调用上述的某个合适的新函数。
- 编译,测试。
- 所有调用端都修改完毕后,删除原函数。
Preserve Whole Object(保持对象完整)
你从某个对象中取出若干值,将它们作为某一次函数调用时的参数,这种调用方法的问题在于:如果将来调用函数需要新的数据项,你就必须查找并修改对此函数的所有调用。因此,建议改为传递整个对象 。
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
withinPlan = plan.withinRange(low, high);
重构为:
withinPlan = plan.withinRange(daysTempRange());
这种重构的做法:
- 对你的目标函数新添加一个参数项,用以代表原数据所在的完整对象。
- 编译,测试。
- 判断哪些参数可被包含在新添的完整对象中。
- 选择上述参数之一,将被调用函数中原来引用该参数的地方,改为调用新添参数对象的相应取值函数。
- 删除该项参数。
- 编译,测试。
- 针对所有可从完整对象中获得的参数,重复上述过程。
- 删除调用端中那些带有被删除参数的代码。
- 编译,测试。
Replace Parameter with Methods(以函数取代参数)
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。让参数接受者去除该项参数,并直接调用前一个函数。
做法
- 如果有必要,将参数的计算过程提炼到一个独立的函数中。
- 将函数本体内引用该参数的地方改为调用新建的函数。
- 每次替换后,修改并测试。
- 全部替换完成后,使用Remove Parameter将该参数去掉。
Introduce Parameter Object(引入参数对象)
如果一组特定的参数总是一起传递,可能有好几个函数都使用这一组参数,这一组参数就是所谓的Data Clumps(数据泥团)。这时候我们可以使用一个对象包装所有的这些数据。
amountInvoicedln(start:Date,end:Date)
amountReceivedln(start:Date,end:Date)
可以重构为:
amountInvoicedln(DateRange)
amountReceivedln(DateRange)
这种重构可以减少代码量,并增加代码的可移植性。这种重构的做法通常为:
- 新建一个类,用以表现你想替换的一组参数。将这个类设为不可变的。
- 编译。
- 针对使用该组参数的所有函数,实施Add Parameter,传入上述新建类的实例对象,并将此参数设为null。
- 对于Data Clumps中的每一项,从函数签名中移除之,并修改调用端和函数本体,令它们都改而通过新的参数对象取得该值。
- 每去除一个参数,编译并测试。
- 将原先的参数全部去除之后,观察有无适当函数可以运用Move Method搬移到参数对象之中。
Remove Setting Method(移除设值函数)
如果某个字段在对象创建的时候就被创建,然后就不再改变。那么就去除该字段的所有设值函数。
如果为某个字段提供了设值函数,那么就暗示这个字段值可以改变。如果你不希望该字段在创建后还有机会改变,那么就不要为它提供设值函数,同时将该字段设置为final。通常的做法为:
- 检查设值函数(setter)被使用的情况,看它是否只被构造函数调用,或者被构造函数所调用的另一个函数调用。
- 修改构造函数,使其直接访问设值函数所针对的那个变量。
如果某个subclass 通过设值函数给superclass 的某个private 值域设了值,那么你就不能这样修改。这种情况下你应该试着在superclass 中提供一个protected 函数(最好是构造函数)来给这些值域设值。不论你怎么做,都不要给superclass 中的函数起一个与设值函数混淆的名字。 - 编译,测试。
- 移除这个设值函数,将它所计对的值域设为final 。
- 编译,测试。
Hide Method(隐藏函数)
如果有一个函数,从来没有被其他任何类用到,那么将这个函数设为private。
做法:
- 经常检查有没有可能降低某个函数的可见度(使它更私有化)。
- 特别对设值函数(setter)进行上述的检查。
- 尽可能降低所有函数的可见度。
- 每完成一组函数的隐藏之后,编译并测试。
- 如果有不适当的隐藏,编译器很自然会检验出来,因此不必每次修改 后都进行编译。如有任何错误出现,很容易被发现。
Replace Constructor with Factory Method(以工厂函数取代构造函数)
这种重构方法就是构建工厂模式的最基本的方法。
Employee (int type){
_type=type;
}
重构为
static Employee create(int type){
return new Employee(type);
}
工厂模式的好处会在将来的设计模式文章中详细介绍。使用Replace Constructor with Factory Method 的最显而易见的动机就是在subclassing(子类化) 过程中以factory method 以取代type code(类型代码)。
你可能常常需要根据type code 创建相应的对象,现在,创建名单中还得加上subclasses,那些subclasses 也是根据type code 来创建。然而由于构造函数只能返回「被索求之对象」,因此你需要将构造函数替换为Factory Method [Gang of Four]。
做法:
- 新建一个factory method ,让它调用现有的构造函数。
- 将「对构造函数的调用」替换为「对factory method 的调用」。
- 每次替换后,编译并测试。
- 将构造函数声明为private。
- 编译。
Encapsulate Downcast(封装向下转型)
某个函数返回的对象,需要由函数调用者执行「向下转型」(downcast)动作。那么将向下转型(downcast)动作移到函数中。
Object lastReading() { return readings.lastElement();}
重构为:
Reading lastReading() { return (Reading) readings.lastElement();}
在强型面向对象语言中,向下转型是最烦人的事情之一。之所以很烦人,是因为从感觉上来说它完全没有必要:你竟然越俎代庖地告诉编译器某些应该由编译器自己计算出来的东西。但是,由于「计算对象型别」的动作往往比较麻烦,你还是常常需要亲自告诉编译器「对象的确切型别」。向下转型在Java 特别盛行,因为Java 没有template(模板)机制,因此如果你想从群集(collection)之中取出一个对象,就必须进行向下转型。向下转型也许是一种无法避免的罪恶,但你仍然应该尽可能少做。如果你的某个函数返回一个值,并且你知道「你所返回的对象」其型别比函数签名式(signature) 所昭告的更特化(specialized;译注:意指返回的是原本声明之return type 的subtype),你便是在函数用户身上强加了非必要的工作。这种情况下你不应该要求用户承担向 下转型的责任,应该尽量为他们提供准确的型别。
做法:
- 找出「必须对函数调用结果进行向下转型」的地方。
- 这种情况通常出现在「返回一个群集(collection)或迭代器(iterator)」 的函数中。
- 将向下转型动作搬移到该函数中。
- 针对返回群集(collection)的函数,使用Encapsulate Collection 。
Replace Error Code with Exception(用异常取代错误码)
问题:某个函数返回一个特定的代码(special code),用以表示某种错误情况。
解决:改用异常(exception)
int withdraw(int amount) {
if (amount > _balance)//数量大于平衡值
return -1;
else {
_balance -= amount;
return 0;
}
}
重构为
void withdraw(int amount) throws BalanceException {
if (amount > _balance)
throw new BalanceException();
_balance -= amount;
}
和生活一样,计算器偶尔也会出错。一旦事情出错,你就需要有些对策。
最简单的情况下,你可以停止程序运行,返回一个错误码。
如果程序崩溃代价很小,用户又足够宽容,那么就放心终止程序的运行好了。
Java 有一种更好的错误处理方式:异常(exceptions)。这种方式之所以更好,因 为它清楚地将「普通程序」和「错误处理」分开了,这使得程序更容易理解——我希望你如今已经坚信:代码的可理解性应该是我们虔诚追求的目标。
做法
- 决定待抛异常应该是checked 还是unchecked。
- 如果调用者有责任在调用前检查必要状态,就抛出unchecked异常。
- 如果想抛出checked 异常,你可以新建一个exception class,也可以使用现有的exception classes。
- 找到该函数的所有调用者,对它们进行相应调整,让它们使用异常。
- 如果函数抛出unchecked 异常,那么就调整调用者,使其在调用函数 前做适当检查。每次修改后,编译并测试。
- 如果函数抛出checked 异常,那么就调整调用者,使其在try 区段中调用该函数。
- 修改该函数的签名式(sigature),令它反映出新用法。