隐藏委托关系(Hide Delegate)
反向重构:移除中间人(192
manager = aPerson.department.manager;
重构后
manager = aPerson.manager;
class Person {
get manager() { return this.department.manager; }
}
动机
一个好的模块化的设计,“封装”即使不是其最关键特征,也是最关键特征之一。“封装”意味着每个模块都应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一变化的模块就会比较少——这会使变化比较容易进行。
当我们初学面向对象技术时就被教导,封装意味着应该隐藏自己的字段。随着经验日渐丰富,你会发现,有更多可以(而且值得)封装的东西。
如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一受托类修改了接口,变化会波及通过服务对象使用它的所有客户端。我可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即使将来委托关系发生变化,变化也只会影响服务对象,而不会直接波及所有客户端。
做法
- 对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数
- 调整客户端,令它只调用服务对象提供的函数。每次调整后运行测试
- 如果将来不再有任何客户端需要取用Delegate(受托类),便可移除服务对象中的相关访问函数
- 测试
范例
本例从两个类开始,代表“人”的Person和代表“部门”的Department。
class Person...
class Person {
constructor(name) {
this._name = name;
}
get name() { return this._name; }
get department() { return this._department; }
set department(arg) { this._department = arg; }
}
class Department...
class Department {
get chargeCode() { return this._chargeCode; }
set chargeCode(arg) { this._chargeCode = arg; }
get manager() { return this._manager; }
set manager(arg) { this._manager = arg; }
}
有些客户端希望知道某人的经理是谁,为此,它必须先取得Department对象。
客户端代码...
manager = aPerson.department.manager;
这样的编码就对客户端揭露了Department的工作原理,于是客户知道:Department负责追踪“经理”这条信息。如果对客户隐藏Department,可以减少耦合。为了这一目的,我在Person中建立一个简单的委托函数。
class Person...
get manager() {return this._department.manager;}
现在修改调用Person的所有客户端,改用新函数:
manager = aPerson.manager;
只要完成了对Department所有函数的修改,并相应修改了Person的所有客户端,我就可以移除Person中的department访问函数了。
移除中间人(Remove Middle Man)
反向重构:隐藏委托关系(189)
manager = aPerson.manager;
class Person {
get manager() { return this.department.manager; }
}
重构后
manager = aPerson.department.manager;
动机
在隐藏委托关系(189)的“动机”一节中,我谈到了“封装受托对象”的好处。但是这层封装也是有代价的。每当客户端要使用受托类的新特性时,你就必须在服务端添加一个简单委托函数。随着受托类的特性(功能)越来越多,更多的转发函数就会使人烦躁。服务类完全变成了一个中间人(81),此时就应该让客户直接调用受托类。(这个味道通常在人们狂热地遵循迪米特法则时悄然出现。我总觉得,如果这条法则当初叫作“偶尔有用的迪米特建议”,如今能少很多烦恼。)
很难说什么程度的隐藏才是合适的。还好,有了隐藏委托关系(189)和删除中间人,我大可不必操心这个问题,因为我可以在系统运行过程中不断进行调整。随着代码的变化,“合适的隐藏程度”这个尺度也相应改变。6个月前恰如其分的封装,现今可能就显得笨拙。重构的意义就在于:你永远不必说对不起——只要把出问题的地方修补好就行了。
做法
- 为受托对象创建一个取值函数
- 对于每个委托函数,让其客户端转为连续的访问函数调用。每次替换后运行测试
替换完委托方法的所有调用点后,你就可以删掉这个委托方法了。
这能通过可自动化的重构手法来完成,你可以先对受托字段使用封装变量(132),再应用内联函数(115)内联所有使用它的函数。
范例
我又要从一个Person类开始了,这个类通过维护一个部门对象来决定某人的经理是谁。(如果你一口气读完本书的好几章,可能会发现每个“人与部门”的例子都出奇地相似。)
客户端代码...
manager = aPerson.manager;
class Person...
get manager() {return this._department.manager;}
class Department...
get manager() {return this._manager;}
像这样,使用和封装Department都很简单。但如果大量函数都这么做,我就不得不在Person之中安置大量委托行为。这就该是移除中间人的时候了。首先在Person中建立一个函数,用于获取受托对象。
class Person...
get department() {return this._department;}
然后逐一处理每个客户端,使它们直接通过受托对象完成工作。
客户端代码...
manager = aPerson.department.manager;
完成对客户端引用点的替换后,我就可以从Person中移除manager方法了。我可以重复此法,移除Person中其他类似的简单委托函数。
我可以混用两种用法。有些委托关系非常常用,因此我想保住它们,这样可使客户端代码调用更友好。何时应该隐藏委托关系,何时应该移除中间人,对我而言没有绝对的标准——代码环境自然会给出该使用哪种手法的线索,具备思考能力的程序员应能分辨出何种手法更佳。
如果手边在用自动化的重构工具,那么本手法的步骤有一个实用的变招:我可以先对department应用封装变量(132)。这样可让manager的取值函数调用department的取值函数。
class Person...
get manager() {return this.department.manager;}
在JavaScript中,调用取值函数的语法跟取用普通字段看起来很像,但通过移除department字段的下划线,我想表达出这里是调用了取值函数而非直接取用字段的区别。
然后我对manager方法应用内联函数(115),一口气替换它的所有调用点。
替换算法(Substitute Algorithm)
function foundPerson(people) {
for (let i = 0; i < people.length; i++) {
if (people[i] === "Don") {
return "Don";
}
if (people[i] === "John") {
return "John";
}
if (people[i] === "Kent") {
return "Kent";
}
}
return "";
}
重构后
function foundPerson(people) {
const candidates = ["Don", "John", "Kent"];
return people.find(p => candidates.includes(p)) || '';
}
动机
我从没试过给猫剥皮,听说有好几种方法,我敢肯定,其中某些方法会比另一些简单。算法也是如此。如果我发现做一件事可以有更清晰的方式,我就会用比较清晰的方式取代复杂的方式。“重构”可以把一些复杂的东西分解为较简单的小块,但有时你就必须壮士断腕,删掉整个算法,代之以较简单的算法。随着对问题有了更多理解,我往往会发现,在原先的做法之外,有更简单的解决方案,此时我就需要改变原先的算法。如果我开始使用程序库,而其中提供的某些功能/特性与我自己的代码重复,那么我也需要改变原先的算法。
有时我会想修改原先的算法,让它去做一件与原先略有差异的事。这时候可以先把原先的算法替换为一个较易修改的算法,这样后续的修改会轻松许多。
使用这项重构手法之前,我得确定自己已经尽可能分解了原先的函数。替换一个巨大且复杂的算法是非常困难的,只有先将它分解为较简单的小型函数,我才能很有把握地进行算法替换工作。
做法
- 整理一下待替换的算法,保证它已经被抽取到一个独立的函数中
- 先只为这个函数准备测试,以便固定它的行为
- 准备好另一个(替换用)算法
- 执行静态检查
- 运行测试,比对新旧算法的运行结果。如果测试通过,那就大功告成;否则,在后续测试和调试过程中,以旧算法为比较参照标准