添加参数
某个函数需要从调用端得到更多信息。 为此函数添加一个对象参数,让该对象带进函数所需信息。
动机(Motivation)
添加参数是一个很常用的重构手法,我几乎可以肯定已经用过它了。使用这项重构的动机很简单:你必须修改一个函数,而修改后的函数需要一些过去没有的信息,因此你需要给该函数添加一个参数。
实际上我比较需要说明的是:不使用本重构的时机。除了添加参数外,你常常还有其他选择。只要可能,其他选择都比本项「添加参数」要好,因为它们不会增加参数列的长度。过长的参数列是不好的味道,因为程序员很难记住那么多参数,而且长参数列往往伴随着坏味道Date Clumps。
请看看现有的参数,然后问自己:你能从这些参数得到所需的信息吗?如果回答是否定的,有可能通过某个函数提供所需信息吗?你究竟把这些信息用于何处?这个函数是否应该属于拥有该信息的那个对象所有?看看现有参数,考虑一下,加入新参数是否合适?也许你应该考虑使用引入参数对象。
我并非要你绝对不要添加参数。事实上我自己经常添加参数,但是在添加参数之前你有必要了解其他选择。
作法(Mechanics)
- 检查函数签名式(signature)是否被superclass 或subclass 实现过。如果是,则需要针对每份实现分别进行下列步骤。
- 声明一个新函数,名称与原函数同,只是加上新添参数。将旧函数的代码拷贝到新函数中。
- 如果需要添加的参数不止一个,将它们一次性添加进去比较容易。
- 编译。
- 修改旧函数,令它调用新函数。
- 如果只有少数几个地方引用旧函数,你大可放心地跳过这一步骤。
- 此时,你可以给参数提供任意值。但一般来说,我们会给对象参数提供null ,给内置型参数提供一个明显非正常值。对于数值型参数,我建议使用0 以外的值,这样你比较容易将来认出它。
- 编译,测试。
- 找出旧函数的所有被引用点,将它们全部修改为对新函数的引用。每次修改后,编译并测试。
- 删除旧函数。
- 如果旧函数是class public 接口的一部分,你可能无法安全地删除它。这种情况下,请将它保留在原地,并将它标示为"deprecated"(不再 被赞同)。
- 编译,测试。
封装[向下转型]动作
某个函数返回的对象,需要由函数调用者执行「向下转型」(downcast)动作。 将向下转型(downcast)动作移到函数中。
Object lastReading() {
return readings.lastElement();
}
Reading lastReading() {
return (Reading) readings.lastElement();
}
动机(Motivation)
在强型别(strongly typed)OO语言中,向下转型是最烦人的事情之一。之所以很烦人,是因为从感觉上来说它完全没有必要:你竟然越俎代庖地告诉编译器某些应该由编译器自己计算出来的东西。但是,由于「计算对象型别」的动作往往比较麻烦,你还是常常需要亲自告诉编译器「对象的确切型别」。向下转型在Java 特别盛行,因为Java 没有template(模板)机制,因此如果你想从群集(collection)之中取出一个对象,就必须进行向下转型。
向下转型也许是一种无法避免的罪恶,但你仍然应该尽可能少做。如果你的某个函数返回一个值,并且你知道「你所返回的对象」其型别比函数签名式(signature) 所昭告的更特化(specialized;译注:意指返回的是原本声明之return type 的subtype),你便是在函数用户身上强加了非必要的工作。这种情况下你不应该要求用户承担向 下转型的责任,应该尽量为他们提供准确的型别。
以上所说的情况,常会在「返回迭代器(iterator)或群集(collection)」的函数身上发生。此时你就应该观察人们拿这个迭代器干什么用,然后针对性地提供专用函数。
作法(Mechanics)
- 找出「必须对函数调用结果进行向下转型」的地方。
- 这种情况通常出现在「返回一个群集(collection)或迭代器(iterator)」 的函数中。
- 将向下转型动作搬移到该函数中。
- 针对返回群集(collection)的函数,使用封装群集。
范例:(Example)
下面的例子中,我以Reading 表示「书籍」。我还拥有一个名为lastReading() 的函数,它从一个用以「保存Reading 对象」的Vector 中返回其最后一个元素:
Object lastReading() {
return readings.lastElement();
}
我应该将这个函数变成:
Reading lastReading() {
return (Reading) readings.lastElement();
}
当我拥有一个群集时,上述那么做就很有意义。如果「保存Reading 对象」的群集被放在Site class 中,并且我看到了如下的代码(客户端):
Reading lastReading = (Reading) theSite.readings().lastElement()
我就可以不再把「向下转型」工作推给用户,并得以向用户隐藏群集:
Reading lastReading = theSite.lastReading();
class Site...
Reading lastReading() {
return (Reading) readings().lastElement();
}
如果你修改函数,将其「返回型别」(return type )改为原返回型别的subclass,那就是改变了函数签名式(signature),但并不会破坏客户端代码,因为编译器知道 它总是可以将一个subclass 自动向上转型为superclass 。当然啦你必须确保这个subclass 不会破坏superclass 带来的任何契约(contract)。(译注:在OO设计中,继承关系代表is-a 关系,因此subclass is-a superclass ,因此正确设计之subclass 决不会破坏superclass 带来的任何契约。)
隐藏某个函数
有一个函数,从来没有被其他任何class 用到。 将这个函数修改为private 。
动机(Motivation)
重构往往促使你修改「函数的可见度」( visibility of methods)。提高函数可见度的情况很容易想像:另一个class 需要用到某个函数,因此你必须提高该函数的可见度。但是要指出一个函数的可见度是否过高,就稍微困难一些。理想状况下你可以使用工具检查所有函数,指出可被隐藏起来的函数。即使没有这样的工具,你也应该时常进行这样的检查。
一种特别常见的情况是:当你而对一个过于丰富、提供了过多行为的接口时,就值得将非必要的取值函数(getter)和设值函数(setter)隐藏起来。尤其当你面对的是一个「只不过做了点简单封装」的数据容器(data holder)时,情况更是如此。 随着愈来愈多行为被放入这个class 之中,你会发现许多取值/设值函数不再需要为public ,因此可以把它们隐藏起来。如果你把取值/设值函数设为private ,并在他处直接访问变量,那就可以放心移除取值/设值函数了。
作法(Mechanics)
- 经常检查有没有可能降低某个函数的可见度(使它更私有化)。
- 使用lint-style 工具,尽可能频繁地检查。当你在另一个class 中移除对某个函数的调用时,也应该进行检查。
- 特别对设值函数(setter)进行上述的检查。
- 尽可能降低所有函数的可见度。
- 每完成一组函数的隐藏之后,编译并测试。
- 如果有不适当的隐藏,编译器很自然会检验出来,因此不必每次修改 后都进行编译。如有任何错误出现,很容易被发现。
范例:(Example)
译注:本书英文版网站上的勘误网页(www.refactoring.com/errata.html)显示,本页程序有些问题,惟因前后颇有牵连,故勘误表上并未明确条列代码之修改。请读者自行上网査阅理解。
下面是一个简单例子:
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;
}
问题可能以数种不同的形式出现。首先,你可能会在设值函数中对引数做运算:
class Account {
private String _id;
Account (String id) {
setId(id);
}
void setId (String arg) {
_id = "ZZ" + arg;
}
如果对引数的修改很简单(就像上面这样)而且又只有一个构造函数,我可以直接在构造函数中做相同的修改。如果修改很复杂,或者有一个以上的函数调用它,我就需要提供一个独立函数。我需要为新函数起个好名字,清楚表达该函数的用途:
class Account {
private final String _id;
Account (String id) {
initializeId(id);
}
void initializeId (String arg) {
_id = "ZZ" + arg;
}
如果subclass 需要对superclass 的private 变量赋初值,情况就比较麻烦一些:
class InterestAccount extends Account...
private double _interestRate;
InterestAccount (String id, double rate) {
setId(id);
_interestRate = rate;
}
问题是我无法在InterestAccount() 中直接访问id 变量。最好的解决方法是使用superclass 构造函数:
class InterestAccount...
InterestAccount (String id, double rate) {
super(id);
_interestRate = rate;
}
如果不能那样做,那么使用一个命名良好的函数就是最好的选择:
class InterestAccount...
InterestAccount (String id, double rate) {
initializeId(id);
_interestRate = rate;
}
另一种需要考虑的情况就是对一个群集(collections)设值:
class Person {
Vector getCourses() {
return _courses;
}
void setCourses(Vector arg) {
_courses = arg;
}
private Vector _courses;
在这里,我希望将设值函数替换为"add"操作加上"remove"操作。我己经在 封装群集 中谈到了这一点。
引入参数对象
某些参数总是很自然地同时出现。 以一个对象取代这些参数。
动机(Motivation)
你常会看到特定的一组参数总是一起被传递。可能有好几个函数都使用这一组参数,这些函数可能隶属同一个class,也可能隶属不同的classes 。这样一组参数就是所谓的Date Clump (数据泥团)」,我们可以运用一个对象包装所有这些数据,再以该对象取代它们。哪怕只是为了把这些数据组织在一起,这样做也是值得的。本项重构的价值在于「缩短了参数列的长度」,而你知道,过长的参数列总是难以理解的。此外,新对象所定义的访问函数(accessors)还可以使代码更具一致性,这又进一步降低了代码的理解难度和修改难度。
本项重构还可以带给你更多好处。当你把这些参数组织到一起之后,往往很快可以发现一些「可被移至新建class」的行为。通常,原本使用那些参数的函数对那些参数会有一些共通措施,如果将这些共通行为移到新对象中,你可以减少很多重复代码。
作法(Mechanics)
- 新建一个class,用以表现你想替换的一组参数。将这个设为不可变的(不可被修改的,immutable)。
- 编译。
- 针对使用该组参数的所有函数,实施添加参数,以上述新建class 之实体对象作为新添参数,并将此一参数值设为null 。
- 如果你所修改的函数被其他很多函数调用,那么你可以保留修改前的旧函数,并令它调用修改后的新函数。你可以先对旧函数进行重构, 然后逐一令调用端转而调用新函数,最后再将旧函数删除。
- 对于Data Clump(数据泥团)中的每一项(在此均为参数),从函数签名式(signature)中移除之,并修改调用端和函数本体,令它们都改而通过「新建 的参数对象」取得该值。
- 每去除一个参数,编译并测试。
- 将原先的参数全部去除之后,观察有无适当函数可以运用搬移函数 搬移到参数对象之中。
- 被搬移的可能是整个函数,也可能是函数中的一个段落。如果是后者, 首先使用提炼函数 将该段落提炼为一个独立函数,再搬移这一新建函数。
范例:(Example)
下面是一个「帐目和帐项」(account and entries)范例。表示「帐项」的Entry 实际上只是个简单的数据容器:
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;
我关注的焦点是用以表示「帐目」的Account,它保存了一组Entry 对象,并有一个函数用来计算两日期间的帐项总量:
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);
我已经记不清有多少次看见代码以「一对值」表示「一个范围」,例如表示日期范围的start 和end、表示数值范围的upper 和lower 等等。我知道为什么会发生这种情况,毕竟我自己也经常这样做。不过,自从我得知Range 模式[Fowler,AP]之后,我就尽量以「范围对象」取而代之。我的第一个步骤是声明一个简单的数据容器,用以表示范围:
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;
}
我把DateRange class 设为不可变,也就是说,其中所有值域都是final ,只能由构造函数来赋值,因此没有任何函数可以修改其中任何值域值。这是一个明智的决定, 因为这样可以避免别名(aliasing)带来的困扰。Java 的函数参数都是pass by value(传值),不可变类(immutable class)正是能够模仿Java 参数的工作方式,因此这种作法对于本项重构是最合适的。
接下来我把DateRange 对象加到getFlowBetween() 函数的参数列中:
class Account...
double getFlowBetween (Date start, Date end, DateRange range) {
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;
}
client code...
double flow = anAccount.getFlowBetween(startDate, endDate, null);
至此,我只需编译一下就行了,因为我尚未修改程序的任何行为。
下一个步骤是去除旧参数之一,以新建对象取而代之。首先我删除start 参数,并修改getFlowBetween() 函数及其调用者,让它们转而使用新对象:
class Account...
double getFlowBetween (Date end, 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(end) ||
(each.getDate().after(range.getStart()) && each.getDate().before(end)))
{
result += each.getValue();
}
}
return result;
}
client code...
double flow = anAccount.getFlowBetween(endDate, new DateRange (startDate, null));
然后我将参数也移除:
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)));
}
如此单纯的提炼和搬移动作,我通常一步完成。如果在这个过程中出错,我可以回到重构前的状态,然后分成两个较小步骤重新进行。
令函数携带参数
若干函数做了类似的工作,但在函数本体中却包含了不同的值。 建立单一函数,以参数表达那些不同的值。
动机(Motivation)
你可能会发现这样的两个函数:它们做着类似的工作,但因少数几个值致使动作略有不同。这种情况下,你可以将这些各自分离的函数替换为一个统一函数,并通过参数来处理那些变化情况,用以简化问题。这样的修改可以去除重复的代码,并提高灵活性,因为你可以用这个参数处理其他(更多种)变化情况。
作法(Mechanics)
- 新建一个带有参数的函数,使它可以替换先前所有的重复性函数(repetitive methods)。
- 编译。
- 将「对旧函数的调用动作」替换为「对新函数的调用动作」。
- 编译,测试。
- 对所有旧函数重复上述步骤,每次替换后,修改并测试。
也许你会发现,你无法用这种办法处理整个函数,但可以处理函数中的一部分代码。 这种情况下,你应该首先将这部分代码提炼到一个独立函数中,然后再对那个提炼所得的函数使用令函数携带参数。
范例:(Example)
下面是一个最简单的例子:
class Employee {
void tenPercentRaise () {
salary *= 1.1;
}
void fivePercentRaise () {
salary *= 1.05;
}
这段代码可以替换如下:
void raise (double factor) {
salary *= (1 + factor);
}
当然,这个例子实在太简单了,所有人都能做到。
下面是一个稍微复杂的例子:
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;
}
本项重构的伎俩在于:以「可将少量数值视为参数」为依据,找出带有重复性的代码。
保持对象完整
你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。
改使用(传递)整个对象。
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
withinPlan = plan.withinRange(low, high);
withinPlan = plan.withinRange(daysTempRange());
动机(Motivation)
有时候,你会将来自同一对象的若干项数据作为参数,传递给某个函数。这样做的问题在于:万一将来被调用函数需要新的数据项,你就必须查找并修改对此函数的所有调用。如果你把这些数据所属的整个对象传给函数,可以避免这种尴尬的处境, 因为被调用函数可以向那个参数对象请求任何它想要的信息。
除了可以使参数列更稳固(不变动)之外,保持对象完整往往还能提高代码的可读性。过长的参数列很难使用,因为调用者和被调用者都必须记住这些参数的用途。此外,不使用完整对象也会造成重复代码,因为被调用函数无法利用完整对象中的函数来计算某些中间值。
「甘蔗不曾两头甜」!如果你传的是数值,被调用函数就只与这些数值有依存关系(dependency),与这些数值所属对象没有任何依存关系。但如果你传递的是整个对象,「参数对象」和「被调用函数所在对象」之间,就有了依存关系。如果这会使你的依存结构恶化,那么你就不该使用保持对象完整。
我还听过另一种不使用保持对象完整 的理由:如果被调用函数只需要「参数对象」的其中一项数值,那么只传递那个数值会更好。我并不认同这种观点,因为传递一项数值和传递一个对象,至少在代码清晰度上是等价的〔当然对于pass by value(传值)参数来说,性能上可能有所差异)。更重要的考量应该放在「对象之间的依存关系」上。
如果被调用函数使用了 [来自另一个对象的很多项数据」,这可能意味该函数实际上应该被定义在「那些数据所属的对象」中。所以,考虑保持对象完整 的同时,你也应该考虑搬移函数。
运用本项重构之前,你可能还没有定义一个完整对象。那么你就应该先使用引入参数对象。
还有一种常见情况:调用者将自己的若干数据作为参数,传递给被调用函数。这种情况下,如果该对象有合适的取值函数(getter),你可以使用取代这些参数值,并且无须操心对象依存问题。
作法(Mechanics)
- 对你的目标函数新添一个参数项,用以代表原数据所在的完整对象。
- 编译,测试。
- 判断哪些参数可被包含在新添的完整对象中。
- 选择上述参数之一,将「被调用函数」内对该参数的各个引用,替换为「对新添之参数对象的相应取值函数(getter)」的调用。
- 删除该项参数。
- 编译,测试。
- 针对所有「可从完整对象中获得」的参数,重复上述过程。
- 删除调用端中那些带有「被删除之参数」的所有代码。
- 当然,如果调用端还在其他地方使用了这些参数,就不要删除它们。
- 编译,测试。
范例:(Example)
以下范例,我以一个Room 对象表示「房间」,它负责记录房间一天中的最高温度和最低温度。然后这个对象需要将「实际温度范围」与预先规定的「温度控制计划」 相比较,告诉客户当天温度是否符合计划要求:
class Room...
boolean withinPlan(HeatingPlan plan) {
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
return plan.withinRange(low, high);
}
class HeatingPlan...
boolean withinRange (int low, int high) {
return (low >= _range.getLow() && high <= _range.getHigh());
}
private TempRange _range;
必将TempRange 对象的信息拆开来单独传递,只需将整个对象传递给withinPlan() 函数即可。在这个简单的例子中,我可以一次性完成修改。如果相关的参数更多些,我也可以进行小步重构。首先,我为参数列添加新的参数项,用 以传递完整的TempRange 对象:
class HeatingPlan...
boolean withinRange (TempRange roomRange, int low, int high) {
return (low >= _range.getLow() && high <= _range.getHigh());
}
class Room...
boolean withinPlan(HeatingPlan plan) {
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
return plan.withinRange(daysTempRange(), low, high);
}
然后,我以TempRange 对象提供的函数来替换low 参数:
class HeatingPlan...
boolean withinRange (TempRange roomRange, int high) {
return (roomRange.getLow() >= _range.getLow() && high <= _range.getHigh());
}
class Room...
boolean withinPlan(HeatingPlan plan) {
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
return plan.withinRange(daysTempRange(), high);
}
重复上述步骤,直到把所有待处理参数项都去除为止:
class HeatingPlan...
boolean withinRange (TempRange roomRange) {
return (roomRange.getLow() >= _range.getLow() && roomRange.getHigh() <= _range.getHigh());
}
class Room...
boolean withinPlan(HeatingPlan plan) {
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
return plan.withinRange(daysTempRange());
}
现在,我不再需要low 和high 这两个临时变量了:
class Room...
boolean withinPlan(HeatingPlan plan) {
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
return plan.withinRange(daysTempRange());
}
使用完整对象后不久,你就会发现,可以将某些函数移到TempRange 对象中,使它更容易被使用,例如:
class HeatingPlan...
boolean withinRange (TempRange roomRange) {
return (_range.includes(roomRange));
}
class TempRange...
boolean includes (TempRange arg) {
return arg.getLow() >= this.getLow() && arg.getHigh() <= this.getHigh();
}
移除参数
函数本体(method body)不再需要某个参数。 将该参数去除。
动机(Motivation)
程序员可能经常添加参数,却往往不愿意去掉它们。他们打的如意算盘是,无论如 何,多余的参数不会引起任何问题,而且以后还可能用上它。
这也是恶魔的诱惑,一定要把它从脑子里赶出去!参数指出函数所需信息,不同的参数值代表不同的意义。函数调用者必须为每一个参数操心该传什么东西进去。如果你不去掉多余参数,你就是让你的每一位用户多费一份心。这是很不划算的,尤其「去除参数」是非常简单的一项重构。
但是,对于多态函数(polymorphic method),情况有所不同。这种情况下,可能多态函数的另一份(或多份)实现码会使用这个参数,此时你就不能去除它。你可以添加一个独立函数,在这些情况下使用,不过你应该先检查调用者如何使用这个函数,以决定是否值得这么做。如果某些调用者已经知道他们正在处理的是 一个特定的subclass ,并且已经做了额外工作找出自己需要的参数,或已经利用对classes 体系的了解来避免取到null ,那么就值得你建立一个新函数,去除那多余参数。如果调用者不需要了解该函数所属的class ,你也可以保持调用者无知(而幸福)的状态。
作法(Mechanics)
- 检查函数签名式(signature)是否被superclass 或如subclass 实现过。如果是,则需要针对每份实现品分别进行下列步骤。
- 声明一个新函数,名称与原函数同,只是去除不必要的参数。将旧函数的代码拷贝到新函数中。
- 如果需要去除的参数不止一个,将它们一次性去除比较容易。
- 编译。
- 修改旧函数,令它调用新函数。
- 如果只有少数几个地方引用旧函数,你大可放心地跳过这一步骤。
- 编译,测试。
- 找出旧函数的所有被引用点,将它们全部修改为对新函数的引用。每次修改后,编译并测试。
- 删除旧函数。
- 如果旧函数是class public 接口的一部分,你可能无法安全地删除它。 这种情况下,将它保留在原处,并将它标记为"deprecated"(不再被赞同)。
- 编译,测试。
由于我可以轻松地添加、去除参数,所以我经常一次性地添加或去除必要的参数。
移除设置函数
你的class 中的某个值域,应该在对象初创时被设值,然后就不再改变。 去掉该值域的所有设值函数(setter)。
动机(Motivation)
如果你为某个值域提供了设值函数(setter),这就暗示这个值域值可以被改变。如果你不希望在对象初创之后此值域还有机会被改变,那就不要为它提供设值函数 (同时并将该值域设为final )。这样你的意图会更加清晰,并且往往可以排除其值被修改的可能性——这种可能性往往是非常大的。
如果你保留了间接访问变量的方法,就可能经常有程序员盲目使用它们[Beck]。这些人甚至会在构造函数中使用设值函数!我猜想他们或许是为了代码的一致性,但却忽视了设值函数往后可能带来的混淆。
作法(Mechanics)
- 检查设值函数(setter)被使用的情况,看它是否只被构造函数调用,或者被构造函数所调用的另一个函数调用。
- 修改构造函数,使其直接访问设值函数所针对的那个变量。
- 如果某个subclass 通过设值函数给superclass 的某个private 值域设了值,那么你就不能这样修改。这种情况下你应该试着在superclass 中提供一个protected 函数(最好是构造函数)来给这些值域设值。不论你怎么做,都不要给superclass 中的函数起一个与设值函数混淆的名字。
- 编译,测试。
- 移除这个设值函数,将它所计对的值域设为final 。
- 编译,测试。
范例:(Example)
译注:本书英文版网站上的勘误网页(www.refactoring.com/errata.html)显示,本页程序有些问题,惟因前后颇有牵连,故勘误表上并未明确条列代码之修改。请读者自行上网査阅理解。
下面是一个简单例子:
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;
}
问题可能以数种不同的形式出现。首先,你可能会在设值函数中对引数做运算:
class Account {
private String _id;
Account (String id) {
setId(id);
}
void setId (String arg) {
_id = "ZZ" + arg;
}
如果对引数的修改很简单(就像上面这样)而且又只有一个构造函数,我可以直接在构造函数中做相同的修改。如果修改很复杂,或者有一个以上的函数调用它,我就需要提供一个独立函数。我需要为新函数起个好名字,清楚表达该函数的用途:
class Account {
private final String _id;
Account (String id) {
initializeId(id);
}
void initializeId (String arg) {
_id = "ZZ" + arg;
}
如果subclass 需要对superclass 的private 变量赋初值,情况就比较麻烦一些:
class InterestAccount extends Account...
private double _interestRate;
InterestAccount (String id, double rate) {
setId(id);
_interestRate = rate;
}
问题是我无法在InterestAccount() 中直接访问id 变量。最好的解决方法是使用superclass 构造函数:
class InterestAccount...
InterestAccount (String id, double rate) {
super(id);
_interestRate = rate;
}
如果不能那样做,那么使用一个命名良好的函数就是最好的选择:
class InterestAccount...
InterestAccount (String id, double rate) {
initializeId(id);
_interestRate = rate;
}
另一种需要考虑的情况就是对一个群集(collections)设值:
class Person {
Vector getCourses() {
return _courses;
}
void setCourses(Vector arg) {
_courses = arg;
}
private Vector _courses;
在这里,我希望将设值函数替换为"add"操作加上"remove"操作。我己经在 封装群集 中谈到了这一点。
重新命名函数
函数的名称未能揭示函数的用途。 修改函数名称。
动机(Motivation)
我极力提倡的一种编程风格就是:将复杂的处理过程分解成小函数。但是,如果做得不好,这会使你费尽周折却弄不清楚这些小函数各自的用途。要避免这种麻烦,关键就在于给函数起一个好名称。函数的名称应该准确表达它的用途。给函数命名有一个好办法:首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释变成函数名称。
人生不如意,十之八九。你常常无法第一次就给函数起一个好名称。这时候你可能会想:就这样将就着吧——毕竟只是一个名称而已。当心!这是恶魔的召唤,是通 向混乱之路,千万不要被它诱惑!如果你看到一个函数名称不能很好地表达它的用 途,应该马上加以修改。记住,你的代码首先是为人写的,其次才是为计算器写的。 而人需要良好名称的函数。想想过去曾经浪费的无数时间吧。如果给每个函数都起一个良好的名称,也许你可以节约好多时间。起一个好名称并不容易,需要经验; 要想成为一个真正的编程高手,「起名称」的水平是至关重要的。当然,函数签名式(signature)中的其他部分也一样重要;如果重新安排参数顺序,能够帮助提高代码的清晰度,那就大胆地去做吧,你有 添加参数 和移除参数 这两项武器。
作法(Mechanics)
- 检查函数签名式(signature)是否被superclass 或subclass 实现过。如果是,则需要针对每份实现品分别进行下列步骤。
- 声明一个新函数,将它命名为你想要的新名称。将旧函数的代码拷贝到新函数中,并进行适当调整。
- 编译。
- 修改旧函数,令它将调用转发给新函数。
- 如果只有少数几个地方引用旧函数,你可以大胆地跳过这一步骤。
- 编译,测试。
- 找出旧函数的所有被引用点,修改它们,令它们改而引用新函数。每次修改后,编译并测试。
- 删除旧函数。
- 如果旧函数是class public 接口的一部分,你可能无法安全地删除它。这种情况下,将它保留在原处,并将它标记为"deprecated"(不再被赞同)。
- 编译,测试。
范例(Example)
我以getTelephoneNumber() 函数来取得某人的电话号码:
public String getTelephoneNumber() {
return ("(" + _officeAreaCode + ") " + _officeNumber);
}
现在,我想把这个函数改名为getOfficeTelephoneNumber()。首先建立一个新函 数,命名为getOfficeTelephoneNumber() ,并将原函数getTelephoneNumber() 的代码拷贝过来。然后,让旧函数直接调用新函数:
class Person...
public String getTelephoneNumber(){
return getOfficeTelephoneNumber();
}
public String getOfficeTelephoneNumber() {
return ("(" + _officeAreaCode + ") " + _officeNumber);
}
现在,我需要找到旧函数的所有调用者,将它们全部改为调用新函数。全部修改完 后,就可以将旧函数删掉了。
如果需要添加或去除某个参数,过程也大致相同。
如果旧函数的调用者并不多,我可以直接修改这些调用者,令它们调用新函数,不必让旧函数充当中介。如果测试出错,我可以回到起始处,并放慢前进速度。
以[工厂函数]取代[构造函数]
你希望在创建对象时不仅仅是对它做简单的建构动作(simple construction )。
将constructor (构造函数)替换为factory method(工厂函数)。
Employee (int type) {
_type = type;
}
static Employee create(int type) {
return new Employee(type);
}
动机(Motivation)
使用以工厂函数取代构造函数 的最显而易见的动机就是在subclassing 过程中以factory method 以取代type code。你可能常常需要根据type code 创建相应的对象,现在,创建名单中还得加上subclasses,那些subclasses 也是根据type code 来创建。然而由于构造函数只能返回「被索求之对象」,因此你需要将构造函数替换为Factory Method [Gang of Four]。
此外,如果构造函数的功能不能满足你的需要,也可以使用factory method 来代替它。Factory method 也是将实值对象改为引用对象 的基础。你也可以令你的factory method 根据参数的个数和型别,选择不同的创建行为。
作法(Mechanics)
- 新建一个factory method ,让它调用现有的构造函数。
- 将「对构造函数的调用」替换为「对factory method 的调用」。
- 每次替换后,编译并测试。
- 将构造函数声明为private。
- 编译。
范例:(Example)
又是那个单调乏味的例子:员工薪资系统。我以Employee 表示「员工」:
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;
}
我希望为Employee 提供不同的subclasses,并分别给予它们相应的type code。因此,我需要建立一个factory method :
static Employee create(int type) {
return new Employee(type);
}
然后,我要修改构造函数的所有调用点,让它们改用上述新建的factory method , 并将构造函数声明为private :
client code...
Employee eng = Employee.create(Employee.ENGINEER);
class Employee...
private Employee (int type) {
_type = type;
}
范例:根据字符串(String)创建subclass 对象
迄今为止,我还没有获得什么实质收获。目前的好处在于:我把「对象创建之调用 动作的接收者」和「被创建之对象所属的class 」分开了。如果我随后使用以子类取代型别码 把type code 转换为Employee 的subclass ,我就可以运用factory method ,将这些subclass 对用户隐藏起来:
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");
}
}
可惜的是,这里面有一个switch 语句。如果我添加一个新的subclass ,就必须记得更新这里的switch 语句,而我又偏偏很健忘。
绕过这个switch 语句的一个好办法是使用Class.forName()。第一件要做的事是修改参数型别,这从根本上说是重新命名函数 的一种变体。首先我得建 立一个函数,让它接收一个字符串引数(string argument):
static Employee create (String name) {
try {
return (Employee) Class.forName(name).newInstance();
} catch (Exception e) {
throw new IllegalArgumentException ("Unable to instantiate" + name);
}
}
然后让稍早那个「create() 函数int 版」调用新建的「create() 函数String 版」:
class Employee {
static Employee create(int type) {
switch (type) {
case ENGINEER:
return create("Engineer");
case SALESMAN:
return create("Salesman");
case MANAGER:
return create("Manager");
default:
throw new IllegalArgumentException("Incorrect type code value");
}
}
然后,我得修改create() 函数的调用者,将下列这样的语句:
Employee.create(ENGINEER)
修改为:
Employee.create("Engineer")
完成之后,我就可以将「create() 函数,int 版本」移除了。
现在,当我需要添加新的Employee subclasses,就不再需要更新create() 函数了。 但我却因此失去了编译期检验,使得一个小小的拼写错误就可能造成运行期错误。如果有必要防止运行期错误,我会使用明确函数来创建对象(见本页下)。但这样一来,每添加一个新的subclass ,我就必须添加一个新函数。这就是为了型别安全而牺牲掉的灵活性。还好,即使我做了错误选择,也可以使用令函数携带参数或以明确函数取代参数撤销决定。
另一个「必须谨慎使用(Class.forName() 」的原因是:它向用户暴露了subclass 名称。不过这并不是太糟糕,因为你可以使用其他字符串,并在factory method 中执行其他行为。这也是「不使用将函数内联化 去除factory method 的一个好理由。
范例:以明确函数(Explicit Methods)创建subclass
我可以通过另一条途径来隐藏subclass ——使用明确函数。如果你只有少数几个subclasses,而且它们都不再变化,这条途径是很有用的。我可能有个抽象的Person class,它有两个subclass :Male 和Female。首先我在superclass 中为每个subclass 定义一个factory method :
class Person...
static Person createMale(){
return new Male();
}
static Person createFemale() {
return new Female();
}
然后我可以把下面的调用:
Person kent = new Male();
替换成:
Person kent = Person.createMale();
但是这就使得superclass 必须知晓subclass 。如果想避免这种情况,你需要一个更为复杂的设计,例如 Product Trader 模式[Bäumer and Riehle]。绝大多数情况下你并不需要如此复杂的设计,上面介绍的作法已经绰绰有余。
以异常取代错误码
某个函数返回一个特定的代码(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;
}
动机(Motivation)
和生活一样,计算器偶尔也会出错。一旦事情出错,你就需要有些对策。最简单的情况下,你可以停止程序运行,返回一个错误码。这就好像因为错过一班飞机而自杀一样(如果真那么做,哪怕我是只猫,我的九条命也早赔光了)。尽管我的油腔滑调企图带来一点幽默,但这种「软件自杀」选择的确是有好处的。如果程序崩溃代价很小,用户又足够宽容,那么就放心终止程序的运行好了。但如果你的程序比较重要,就需要以比较认真的方式来处理。
问题在于:程序中发现错误的地方,并不一定知道如何处理错误。当一段副程序 (routine)发现错误时,它需要让它的调用者知道这个错误,而调用者也可能将这 个错误继续沿着调用链(call chain)传递上去。许多程序都使用特殊输出来表示错误,Unix 系统和C-based 系统的传统方式就是「以返回值表示副程序的成功或失败」。
Java 有一种更好的错误处理方式:异常(exceptions)。这种方式之所以更好,因 为它清楚地将「普通程序」和「错误处理」分开了,这使得程序更容易理解——我希望你如今已经坚信:代码的可理解性应该是我们虔诚追求的目标。
作法(Mechanics)
- 决定待抛异常应该是checked 还是unchecked。
- 如果调用者有责任在调用前检查必要状态,就抛出unchecked异常。
- 如果想抛出checked 异常,你可以新建一个exception class,也可以使用现有的exception classes。
- 找到该函数的所有调用者,对它们进行相应调整,让它们使用异常。
- 如果函数抛出unchecked 异常,那么就调整调用者,使其在调用函数 前做适当检查。每次修改后,编译并测试。
- 如果函数抛出checked 异常,那么就调整调用者,使其在try 区段中调用该函数。
- 修改该函数的签名式(sigature),令它反映出新用法。
如果函数有许多调用者,上述修改过程可能跨度太大。你可以将它分成下列数个步骤:
- 决定待抛异常应该是checked 还是unchecked 。
- 新建一个函数,使用异常来表示错误状况,将旧函数的代码拷贝到新函数中,并做适当调整。
- 修改旧函数的函数本体,让它调用上述新建函数。
- 编译,测试。
- 逐一修改旧函数的调用者,令其调用新函数。每次修改后,编译并测试。
- 移除旧函数。
范例:(Example)
现实生活中你可以透支你的账户余额,计算器教科书却总是假设你不能这样做,这不是报奇怪吗?不过下而的例子仍然假设你不能这样做:
class Account...
int withdraw(int amount) {
if (amount > _balance)
return -1;
else {
_balance -= amount;
return 0;
}
}
private int _balance;
为了让这段代码使用异常,我首先需要决定使用checked 异常还是unchecked 异常。决策关键在于:调用者是否有责任在取款之前检查存款余额,或者是否应该由 withdraw() 函数负责检查。如果「检查余额」是调用者的责任,那么「取款金额大于存款余额」就是一个编程错误。由于这是一个编程错误(也就是一只「臭虫」〕, 所以我应该使用unchecked 异常。另一方面,如果「检查余额」是withdraw() 函数的责任,我就必须在函数接口中声明它可能抛出这个异常(译注:这是一个checked 异常),那么也就提醒了调用者注意这个异常,并采取相应措施。
范例:unchecked 异常
首先考虑unchecked 异常。使用这个东西就表示应该由调用者负责检查。首先我需要检查调用端的代码,它不应该使用withdraw() 函数的返回值,因为该返回值只用来指出程序员的错误。如果我看到下面这样的代码:
if (account.withdraw(amount) == -1)
handleOverdrawn();
else doTheUsualThing();
我应该将它替换为这样的代码:
if (!account.canWithdraw(amount))
handleOverdrawn();
else {
account.withdraw(amount);
doTheUsualThing();
}
每次修改后,编译并测试。
现在,我需要移除错误码,并在程序出错时抛出异常。由于行为(根据其文本定义 得知)是异常的、罕见的,所以我应该用一个卫语句(guard clause)检查这种情况:
void withdraw(int amount) {
if (amount > _balance)
throw new IllegalArgumentException ("Amount too large");
_balance -= amount;
}
由于这是程序员所犯的错误,所以我应该使用assertion 更清楚地指出这一点:
class Account...
void withdraw(int amount) {
Assert.isTrue ("amount too large", amount > _balance);
_balance -= amount;
}
class Assert...
static void isTrue (String comment, boolean test) {
if (! test) {
throw new RuntimeException ("Assertion failed: " + comment);
}
}
范例:checked 异常
checked 异常的处理方式略有不同。首先我要建立(或使用)一个合适的异常:
class BalanceException extends Exception {}
然后,调整调用端如下:
try {
account.withdraw(amount);
doTheUsualThing();
} catch (BalanceException e) {
handleOverdrawn();
}
接下来我要修改withdraw() 函数,让它以异常表示错误状况:
void withdraw(int amount) throws BalanceException {
if (amount > _balance) throw new BalanceException();
_balance -= amount;
}
这个过程的麻烦在于:我必须一次性修改所有调用者和被它们调用的函数,否则编译器会报错。如果调用者很多,这个步骤就实在太大了,其中没有编译和测试的保障。
这种情况下,我可以借助一个临时中间函数。我仍然从先前相同的情况出发:
if (account.withdraw(amount) == -1)
handleOverdrawn();
else doTheUsualThing();
class Account ...
int withdraw(int amount) {
if (amount > _balance)
return -1;
else {
_balance -= amount;
return 0;
}
}
首先,产生一个newWithdraw() 函数,让它抛出异常:
void newWithdraw(int amount) throws BalanceException {
if (amount > _balance) throw new BalanceException();
_balance -= amount;
}
然后,调整现有的withdraw() 函数,让它调用newWithdraw() :
int withdraw(int amount) {
try {
newWithdraw(amount);
return 0;
} catch (BalanceException e) {
return -1;
}
}
完成以后,编译并测试。现在我可以逐一将「对旧函数的调用」替换为「对新函数 的调用」:
try {
account.newWithdraw(amount);
doTheUsualThing();
} catch (BalanceException e) {
handleOverdrawn();
}
由于新旧两函数都存在,所以每次修改后我都可以编译、测试。所有调用者都被我修改完毕后,旧函数便可移除,并使用重新命名函数 修改新函数名称,使它与旧函数相同。
以测试取代异常
面对一个「调用者可预先加以检查」的条件,你抛出了一个异常。
修改调用者,使它在调用函数之前先做检查。
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];
}
动机(Motivation)
异常(exception)的出现是程序语言的一大进步。运用以异常取代错误码,异常便可协助我们避免很多复杂的错误处理逻辑。但是,就像许多好东西一样,异常也会被滥用,从而变得不再让人偷快(就连味道极好的Aventinus 啤酒,喝得太多也会让我厌烦[Jackson])。「异常」只应该被用于异常 的、罕见的行为,也就是那些「产生意料外的错误」的行为,而不应该成为「条件 检查」的替代品。如果你可以合理期望调用者在调用函数之前先检査某个条件,那么你就应该提供一个测试,而调用者应该使用它。
作法(Mechanics)
- 在函数调用点之前,放置一个测试句,将函数内的catch 区段中的代码拷贝到测试句的适当if 分支中。
- 在catch 区段起始处加入一个assertion,确保catch 区段绝对不会被执行。
- 编译,测试。
- 移除所有catch 区段,然后将区段内的代码拷贝到try 之外,然后移除try 区段。
- 编译,测试,
范例:(Example)
下面的例子中,我以一个ResourcePool 对象管理「创建代价高昂、可复用」的资源(例如数据库连接,database connection)。这个对象带有两个「池」(pools), 一个用以保存可用资源,一个用以保存已分配资源。当用户索求一份资源时,ResourcePool 对象从「可用资源池』中取出一份资源交出,并将这份资源转移到 「已分配资源池」。当用户释放一份资源时,ResourcePool 对象就将该资源从「已 分配资源池」放回「可用资源池」。如果「可用资源池」不能满足用户的索求,ResourcePool 对象就创建一份新资源。
资源供应函数可能如下所示:
class ResourcePool
Resource getResource() {
Resource result;
try {
result = (Resource) _available.pop();
_allocated.push(result);
return result;
} catch (EmptyStackException e) {
result = new Resource();
_allocated.push(result);
return result;
}
}
Stack _available;
Stack _allocated;
在这里,「可用资源用尽」并不是一种意料外的事件,因此我不该使用异常 (exceptions)表示这种情况。
为了去掉这里的异常,我首先必须添加一个适当的提前测试,并在其中处理「可用 资源池为空」的情况:
Resource getResource() {
Resource result;
if (_available.isEmpty()) {
result = new Resource();
_allocated.push(result);
return result;
}
else {
try {
result = (Resource) _available.pop();
_allocated.push(result);
return result;
} catch (EmptyStackException e) {
result = new Resource();
_allocated.push(result);
return result;
}
}
}
现在getResource() 应该绝对不会抛出异常了。我可以添加assertion 保证这一点:
Resource getResource() {
Resource result;
if (_available.isEmpty()) {
result = new Resource();
_allocated.push(result);
return result;
}
else {
try {
result = (Resource) _available.pop();
_allocated.push(result);
return result;
} catch (EmptyStackException e) {
Assert.shouldNeverReachHere("available was empty on pop");
result = new Resource();
_allocated.push(result);
return result;
}
}
}
class Assert...
static void shouldNeverReachHere(String message) {
throw new RuntimeException (message);
}
编译并测试。如果一切运转正常,就可以将try 区段中的代码拷贝到try 区段之外,然后将区段全部移除:
Resource getResource() {
Resource result;
if (_available.isEmpty()) {
result = new Resource();
_allocated.push(result);
return result;
}
else {
result = (Resource) _available.pop();
_allocated.push(result);
return result;
}
}
在这之后我常常发现,我可以对条件代码(conditional code)进行整理。本例之中我可以使用合并重复的条件片段:
Resource getResource() {
Resource result;
if (_available.isEmpty())
result = new Resource();
else
result = (Resource) _available.pop();
_allocated.push(result);
return result;
}
以明确函数取代参数
你有一个函数,其内完全取决于参数值而采取不同反应。
针对该参数的每一个可能值,建立一个独立函数。
void setValue (String name, int value) {
if (name.equals("height"))
_height = value;
if (name.equals("width"))
_width = value;
Assert.shouldNeverReachHere();
}
void setHeight(int arg) {
_height = arg;
}
void setWidth (int arg) {
_width = arg;
}
动机(Motivation)
以明确函数取代参数洽恰相反于令函数携带参数。如果某个参数有离散取值,而函数内又以条件式检查这些参数值,并根据不同参数值做出不同的反应,那么就应该使用本项重构。调用者原本必须赋予参数适当的值,以决定该函数做出何种响应;现在,既然你提供了不同的函数给调用 者使用,就可以避免出现条件式。此外你还可以获得「编译期代码检验」的好处, 而且接口也更清楚。如果以参数值决定函数行为,那么函数用户不但需要观察该函数,而且还要判断参数值是否合法,而「合法的参数值」往往很少在文档中被清楚地提出。
就算不考虑「编译期检验」的好处,只是为了获得一个清晰的接口,也值得你执行本项重构。哪怕只是给一个内部的布尔(boolean)变量赋值,相较之下Switch.beOn() 也比Switch.setState(true) 要清楚得-多。
但是,如果参数值不会对函数行为有太多影响,你就不应该使用以明确函数取代参数。如果情况真是这样,而你也只需要通过参数为一个值域赋值,那么直接使用设值函数(setter)就行了。如果你的确需要「条件判断」 式的行为,可考虑使用以多态取代条件式。
作法(Mechanics)
- 针对参数的每一种可能值,新建一个明确函数。
- 修改条件式的每个分支,使其调用合适的新函数。
- 修改每个分支后,编译并测试。
- 修改原函数的每一个被调用点,改而调用上述的某个合适的新函数。
- 编译,测试。
- 所有调用端都修改完毕后,删除原(带有条件判断的)函数。
范例:(Example)
下列代码中,我想根据不同的参数值,建立Employee 之下不同的subclass。以下 代码往往是以工厂函数取代构造函数 的施行成果:
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
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");
}
}
由于这是一个factory method,我不能实施以多态取代条件式 ,因为使用该函数时我根本尚未创建出对象。我并不期待太多新的subclasses,所以一个明确的接口是合理的(译注:不甚理解作者文意)。首先,我要根据参数值建立相应的新函数:
static Employee createEngineer() {
return new Engineer();
}
static Employee createSalesman() {
return new Salesman();
}
static Employee createManager() {
return new Manager();
}
然后把「switch 语句的各个分支」替换为「对新函数的调用」:
static Employee create(int type) {
switch (type) {
case ENGINEER:
return Employee.createEngineer();
case SALESMAN:
return new Salesman();
case MANAGER:
return new Manager();
default:
throw new IllegalArgumentException("Incorrect type code value");
}
}
每修改一个分支,都需要编译并测试,直到所有分支修改完毕为止:
static Employee create(int type) {
switch (type) {
case ENGINEER:
return Employee.createEngineer();
case SALESMAN:
return Employee.createSalesman();
case MANAGER:
return Employee.createManager();
default:
throw new IllegalArgumentException("Incorrect type code value");
}
}
接下来,我把注意力转移到旧函数的调用端。我把诸如下面这样的代码:
Employee kent = Employee.create(ENGINEER)
替换为:
Employee kent = Employee.createEngineer()
修改完create() 函数的所有调用者之后,我就可以把create() 函数删掉了。同时也可以把所有常量都删掉。
以函数取代参数
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数也可以(也有能力)调用前一个函数。
让参数接受者去除该项参数,并直接调用前一个函数。
int basePrice = _quantity * _itemPrice;
discountLevel = getDiscountLevel();
double finalPrice = discountedPrice (basePrice, discountLevel);
int basePrice = _quantity * _itemPrice;
double finalPrice = discountedPrice (basePrice);
动机(Motivation)
如果函数可以通过其他途径(而非参数列〕获得参数值,那么它就不应该通过参数取得该值。过长的参数列会增加程序阅读者的理解难度,因此我们应该尽可能缩短参数列的长度。
缩减参数列的办法之一就是,看看「参数接受端(receiver)」是否可以通过「与调用端相同的计算」来取得参数携带值。如果调用端通过「其所属对象内部的另一个函数」来计算参数,并在计算过程中「未曾引用调用端的其他参数」(译注:亦就是说没有太多与外界的相依关系),那么你就应该可以将这个计算过程转移到被调用端内,从而去除该项参数。如果你所调用的函数隶属另一对象,而该对象拥有一个reference 指向调用端所属对象,前面所说的这些也同样适用。
但是,如果「参数计算过程」倚赖调用端的某个参数,那么你就无法去掉被调用端的那个参数,因为每一次调用动作中,该参数值都可能不同(当然,如果你能够运用以明确函数取代参数 将该参数替换为一个函数,又另当别论)。另外,如果参数接受端(receiver)并没有一个reference 指向参数发送端(sender),而你也不想加上这样一个reference ,那么也无法去除参数。
有时候,参数的存在是为了将来的弹性。这种情况下我仍然会把这种多余参数拿掉。是的,你应该只在必要关头才添加参数,预先添加的参数很可能并不是你所需要的。不过,对于这条规则,也有一个例外:如果修改接口会对整个程序造成非常痛苦的结果(例如需要很长时间来重建程序,或需要修改大量代码〕,那么可以考虑保留前人预先加入的参数。如果真是这样,你应该首先判断修改接口究竟会造成多严重的后果,然后考虑是否「降低系统各部位之间的依存程度」以减少「修改接口所造成的影响」。稳定的接口确实很好,但是被冻结在一个不良接 口上,也是有问题的。
作法(Mechanics)
- 如果有必要,将参数的计算过程提炼到一个独立函数中。
- 将函数本体内「对该参数的引用」替换为「对新建函数的调用」。
- 每次替换后,修改并测试。
- 全部替换完成后,使用移除参数 将该参数去掉。
范例:(Example)
以下代码用于计算定单折扣价格。虽然这么低的折扣不大可能出现在现实生活中, 不过作为一个范例,我们暂不考虑这一点:
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;
}
首先,我把计算折扣等级(discountLevel)的代码提炼成为一个独立的 getDiscountLevel() 函数:
public double getPrice() {
int basePrice = _quantity * _itemPrice;
int discountLevel = getDiscountLevel();
double finalPrice = discountedPrice (basePrice, discountLevel);
return finalPrice;
}
private int getDiscountLevel() {
if (_quantity > 100) return 2;
else return 1;
}
然后把discountedPrice() 函数中对discountLevel 参数的所有引用点,替换为getDiscountLevel() 函数的调用:
private double discountedPrice (int basePrice, int discountLevel) {
if (getDiscountLevel() == 2) return basePrice * 0.1;
else return basePrice * 0.05;
}
此时我就可以使用移除参数 去掉discountLevel 参数了 :
public double getPrice() {
int basePrice = _quantity * _itemPrice;
int discountLevel = getDiscountLevel();
double finalPrice = discountedPrice (basePrice);
return finalPrice;
}
private double discountedPrice (int basePrice) {
if (getDiscountLevel() == 2) return basePrice * 0.1;
else return basePrice * 0.05;
}
接下来可以将discountLevel 变量去除掉:
public double getPrice() {
int basePrice = _quantity * _itemPrice;
double finalPrice = discountedPrice (basePrice);
return finalPrice;
}
现在,可以去掉其他非必要的参数和相应的临时变量。最后获得以下代码:
public double getPrice() {
return discountedPrice ();
}
private double discountedPrice () {
if (getDiscountLevel() == 2) return getBasePrice() * 0.1;
else return getBasePrice() * 0.05;
}
private double getBasePrice() {
return _quantity * _itemPrice;
}
最后我还可以针对discountedPrice() 函数使用将函数内联化:
private double getPrice () {
if (getDiscountLevel() == 2) return getBasePrice() * 0.1;
else return getBasePrice() * 0.05;
}
将查询函数和修改函数分离
某个函数既返回对象状态值,又修改对象状态(state)。 建立两个不同的函数,其中一个负责査询,另一个负责修改。
动机(Motivation)
如果某个函数只是向你提供一个值,没有任何看得到的副作用(或说连带影响), 那么这是个很有价值的东西。你可以任意调用这个函数,也可以把调用动作搬到函 数的其他地方。简而言之,需要操心的事情少多了。
明确表现出「有副作用」与「无副作用」两种函数之间的差异,是个很好的想法。 下而是一条好规则:任何有返回值的函数,都不应该有看得到的副作用。有些程序 员甚至将此作为一条必须遵守的规则[Meyer]。就像对待任何东西一样,我并不绝对遵守它,不过我总是尽量遵守,而它也回报我很好的效果。
如果你遇到一个「既有返回值又有副作用」的函数,就应该试着将查询动作从修改 动作中分割出来。
你也许已经注意到了 :我使用「看得到的副作用」这种说法。有一种常见的优化办法是:将查询所得结果高速缓存(cache)于某个值域中,这么一来后续的重复查询 就可以大大加快速度。虽然这种作法改变了对象的状态,但这一修改是察觉不到的,因为不论如何査询,你总是获得相同结果[Meyer]。
作法(Mechanics)
- 新建一个查询函数,令它返回的值与原函数相同。
- 观察原函数,看它返回什么东西。如果返回的是一个临时变量,找出临时变量的位置。
- 修改原函数,令它调用查询函数,并返回获得的结果。
- 原函数中的每个return 句都应该像这样:return newQuery(),而不应该返回其他东西。
- 如果调用者将返回值赋给了一个临时变量,你应该能够去除这个临时 变量。
- 编译,测试。
- 将「原函数的每一个被调用点」替换为「对查询函数的调用」。然后,在调用査询函数的那一行之前,加上对原函数的调用。每次修改后,编译并测试。
- 将原函数的返回值改为void。丨山并删掉其中所有的return 句。
范例:(Example)
有这样一个函数:一旦有人入侵安全系统,它会告诉我入侵者的名字,并发送一个警报。如果入侵者不止一个,也只发送一条警报:
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);
}
为了将查询动作和修改动作分开,我首先建立一个适当的查询函数,使其与修改函 数返回相同的值,但不造成任何副作用:
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 "";
}
然后,我要逐一替换原函数内所有的?如皿句,改调用新建的查询函数。每次替换后,编译并测试。这一步完成之后,原函数如下所示:
String foundMiscreant(String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals ("Don")){
sendAlert();
return foundPerson(people);
}
if (people[i].equals ("John")){
sendAlert();
return foundPerson(people);
}
}
return foundPerson(people);
}
现在,我要修改调用者,将原本的单一调用动作替换为两个调用:先调用修改函数,然后调用查询函数:
void checkSecurity(String[] people) {
foundMiscreant(people);
String found = foundPerson(people);
someLaterCode(found);
}
所有调用都替换完毕后,我就可以将修改函数的返回值改为void:
void foundMiscreant (String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals ("Don")){
sendAlert();
return;
}
if (people[i].equals ("John")){
sendAlert();
return;
}
}
}
现在,为原函数改个名称可能会更好一些:
void sendAlert (String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals ("Don")){
sendAlert();
return;
}
if (people[i].equals ("John")){
sendAlert();
return;
}
}
}
当然,这种情况下,我得到了大量重复代码,因为修改函数之中使用了与查询函数相同的代码。现在我可以对修改函数实施替换你的算法 ,设法让它再简洁一些:
void sendAlert(String[] people){
if (! foundPerson(people).equals(""))
sendAlert();
}
并发(Concurrency)问题
如果你在一个多线程系统中工作,肯定知道这样一个重要的惯用手法:在同一个动作中完成检查和赋值。这是否和将查询函数和修改函数分离 互相矛盾呢? 我曾经和Doug Lea 讨论过这个问题,并得出结论:两者并不矛盾,但你需要做一 些额外工作。将查询动作和修改动作分开来仍然是很有价值的。但你需要保留第三个函数来同时做这两件事。这个「查询-修改」函数将调用各自独立的查询函数和 修改函数,并被声明为synchronized 时。如果查询函数和修改函数未被声明为synchronized ,那么你还应该将它们的可见范围限制在package 级别或private 级别。这样,你就可以拥有一个安全、同步的操作,它由两个较易理解的函数组成。 这两个较低层函数也可以用于其他场合。