如果你注定要成为厉害的人, 那问题的答案就深藏在你的血脉里。
本篇文章主要讲解 《重构---改善既有代码的设计》 这本书中的 第七章在对象之间搬移特性中 的知识点,
Move Method(搬移函数)
问题:你的程序中,有个函数与其所驻class之外的另一个class进行更多交流:调用后者,或被后者调用。
解决:在该函数最常引用的class中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数(delegating method),或是将旧函数完全移除。
动机
「函数搬移」是重构理论的支柱。如果一个class有太多行为,或如果一个class与另一个class有太多合作而形成高度耦合(highly coupled),我就会搬移函数。通过这种手段,我可以使系统中的classes更简单,这些classes最终也将更干净利落地实现系统交付的任务。
常常我会浏览class的所有函数,从中寻找这样的函数:使用另一个对象的次数比使用自己所驻对象的次数还多。一旦我移动了一些值域,就该做这样的检查。一旦发现「有可能被我搬移」的函数,我就会观察调用它的那一端、它调用的那一端,以及继承体系中它的任何一个重定义函数。然后,我会根据「这个函数与哪个对象的交流比较多」,决定其移动路径。
这往往不是一个容易做出的决定。如果不能肯定是否应该移动一个函数,我就会继续观察其他函数。移动其他函数往往会让这项决定变得容易一些。有时候,即使你移动了其他函数,还是很难对眼下这个函数做出决定。其实这也没什么大不了的。 如果真的很难做出决定,那么或许「移动这个函数与否」并不那么重要。所以,我会凭本能去做,反正以后总是可以修改的。
作法
- 检查source class定义之source method所使用的一切特性(features),考虑它们是否也该被搬移。(译注:此处所谓特性泛指class定义的所有东西,包括值域和函数。)
- 如果某个特性只被你打算搬移的那个函数用到,你应该将它一并搬移。如果另有其他函数使用了这个特性,你可以考虑将使用该特性的所有函数全都一并搬移。有时候搬移一组函数比逐一搬移简单些。
范例
我用一个表示「帐户」的account class来说明这项重构:
class Account...
//用户类型类
private AccountType _type;
//透支天数
private int _daysOverdrawn;
//透支费用
double overdraftCharge() { //译注:透支金计费,它和其他class的关系似乎比较密切。
//判断保险
if (_type.isPremium()) {
double result = 10;
if (_daysOverdrawn > 7) result += (_daysOverdrawn - 7) * 0.85;
return result;
}
else return _daysOverdrawn * 1.75;
}
//银行操作
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > 0) result += overdraftCharge();
return result;
}
假设有数种新帐户,每一种都有自己的「透支金计费规则」。
所以我希望将overdraftCharge()搬移到AccountType class去。
第一步要做的是:观察被overdraftCharge()使用的每一特性(features),考虑是否值得将它们与overdraftCharge()—起移动。此例之中我需要让daysOverdrawn值域留在Account class,因为其值会随不同种类的帐户而变化。然后,我将overdraftCharge()函数码拷贝到AccountType中,并做相应调整。
class AccountType...
double overdraftCharge(int daysOverdrawn) {
if (isPremium()) {
double result = 10;
if (daysOverdrawn > 7) result += (daysOverdrawn - 7) * 0.85;
return result;
}
else return daysOverdrawn * 1.75;
}
在这个例子中,「调整」的意思是:(1)对于「使用AccountType特性」的语句,去掉_type;(2)想办法得到依旧需要的Account class特性。当我需要使用source class特性,我有四种选择:(1)将这个特性也移到target class;(2)建立或使用一个从target class到source的引用〔指涉)关系;(3)将source object当作参数传给target class;(4)如果所需特性是个变量,将它当作参数传给target method。
本例中我将_daysOverdrawn变量作为参数传给target method(上述(4))。
调整target method使之通过编译,而后我就可以将source method的函数本体替换为一个简单的委托动作(delegation),然后编译并测试:
class Account...
double overdraftCharge() {
return _type.overdraftCharge(_daysOverdrawn);
}
我可以保留代码如今的样子,也可以删除source method。如果决定删除,就得找出source method的所有调用者,并将这些调用重新定向,改调用Account的bankCharge():
class Account...
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > 0) result += _type.overdraftCharge(_daysOverdrawn);
return result;
}
所有调用点都修改完毕后,我就可以删除source method在Account中的声明了。我可以在每次删除之后编译并测试,也可以一次性批量完成。如果被搬移的函数不是private,我还需要检查其他classes是否使用了这个函数。在强型(strongly typed) 语言中,删除source method声明式后,编译器会帮我发现任何遗漏。
此例之中被移函数只取用(指涉〕一个值域,所以我只需将这个值域作为参数传给target method就行了。如果被移函数调用了Account中的另一个函数,我就不能这么简单地处理。这种情况下我必须将source object传递给target method:
class AccountType...
double overdraftCharge(Account account) {
if (isPremium()) {
double result = 10;
if (account.getDaysOverdrawn() > 7)
result += (account.getDaysOverdrawn() - 7) * 0.85;
return result;
}
else return account.getDaysOverdrawn() * 1.75;
}
如果我需要source class的多个特性,那么我也会将source object传递给target method。不过如果target method需要太多source class特性,就得进一步重构。通常这种情况下我会分解target method,并将其中一部分移回source class。
Move Field(搬移值域)
问题:你的程序中,某个field(值域〕被其所驻class之外的另一个class更多地用到。
解决:在target class 建立一个new field,修改source field的所有用户,令它们改用此new field。
动机
在classes之间移动状态(states)和行为,是重构过程中必不可少的措施。
随着系统发展,你会发现自己需要新的class,并需要将原本的工作责任拖到新的class中。这个星期中合理而正确的设计决策,到了下个星期可能不再正确。这没问题;如果你从来没遇到这种情况,那才有问题。
如果我发现,对于一个field(值域),在其所驻class之外的另一个class中有更多函数使用了它,我就会考虑搬移这个field。上述所谓「使用」可能是通过设值/取值(setting/getting)函数间接进行。我也可能移动该field的用户(某函数),这取决于是否需要保持接口不受变化。
如果这些函数看上去很适合待在原地,我就选择搬移field。
使用Extract Class 时,我也可能需要搬移field。此时我会先搬移field,然后再搬移函数。
作法
- 如果field的属性是public,首先使用Encapsulate Field(封装字段) 将它封装起来。
Ø 如果你有可能移动那些频繁访问该field的函数,或如果有许多函数访问某个field,先使用Self Encapsulate Field 也许会有帮助。 - 编译,测试。
- 在target class中建立与source field相同的field,并同时建立相应的设值/取值 (setting/getting)函数。
- 编译target class。
- 决定如何在source object中引用target object。
Ø 一个现成的field或method可以助你得到target object。如果没有,就看能否轻易建立这样一个函数。如果还不行,就得在source class中新建一个field来存放target object。这可能是个永久性修改,但你也可以暂不公开它,因为后续重构可能会把这个新建field除掉。 - 删除source field。
- 将所有「对source field的引用」替换为「对target适当函数的调用」。
Ø 如果是「读取」该变量,就把「对source field的引用」替换为「对target取值函数(getter)的调用」;如果是「赋值」该变量,就把对source field的引用」替换成「对设值函数(setter)的调用」。
Ø 如果source field不是private,就必须在source class的所有subclasses中查找source field的引用点,并进行相应替换。
· 编译,测试。
范例
下面是Account class的部分代码:
class Account...
private AccountType _type;
private double _interestRate;
double interestForAmount_days (double amount, int days) {
return _interestRate * amount * days / 365;
}
我想把表示利率的_interestRate搬移到AccountType class去。
目前已有数个函数引用了它,interestForAmount_days() 就是其一。
下一步我要在AccountType中建立_interestRate field以及相应的访问函数:
class AccountType...
private double _interestRate;
void setInterestRate (double arg) {
_interestRate = arg;
}
double getInterestRate () {
return _interestRate;
}
这时候我可以编译新的AccountType class。
现在,我需要让Account class中访问此_interestRate field的函数转而使用AccountType对象,然后删除Account class中的_interestRate field。
我必须删除source field,才能保证其访问函数的确改变了操作对象,因为编译器会帮我指出未正确获得修改的函数。
private double _interestRate;
double interestForAmount_days (double amount, int days) {
return _type.getInterestRate() * amount * days / 365;
}
范例:使用Self Encapsulate(自我封装)
如果有很多函数已经使用了_interestRate field,我应该先运用Self Encapsulate Field:
class Account...
private AccountType _type;
private double _interestRate;
double interestForAmount_days (double amount, int days) {
return getInterestRate() * amount * days / 365;
}
private void setInterestRate (double arg) {
_interestRate = arg;
}
private double getInterestRate () {
return _interestRate;
}
这样,在搬移field之后,我就只需要修改访问函数(accessors)就行了 :
double interestForAmountAndDays (double amount, int days) {
return getInterestRate() * amount * days / 365;
}
private void setInterestRate (double arg) {
_type.setInterestRate(arg);
}
private double getInterestRate () {
return _type.getInterestRate();
}
如果以后有必要,我可以修改访问函数(accessors)的用户,让它们使用新对象。 Self Encapsulate Field 使我得以保持小步前进。如果我需要对做许多处理,保持小步前进是有帮助的。特别值得一提的是:首先使用Self Encapsulate Field 使我得以更轻松使用Move Method 将函数搬移到target class中。如果待移函数引用了field的访问函数(accessors),那么那些引用点是无须修 改的。
Extract Class(提炼类)
问题:某个class做了应该由两个classes做的事。
解决:建立一个新class,将相关的值域和函数从旧class搬移到新class。
动机
你也许听过类似这样的教诲:一个class应该是一个清楚的抽象(abstract),处理一些明确的责任。
但是在实际工作中,class会不断成长扩展。你会在这儿加入一些功能,在那儿加入一些数据。给某个class添加一项新责任时,你会觉得不值得为这项责任分离出一个单独的class。于是,随着责任不断増加,这个class会变得过份复杂。很快,你的class就会变成一团乱麻。
这样的class往往含有大量函数和数据。这样的class往往太大而不易理解。此时你需要考虑哪些部分可以分离出去,并将它们分离到一个单独的class中。如果某些数据和某些函数总是一起出现,如果某些数据经常同时变化甚至彼此相依,这就表示你应该将它们分离出去。一个有用的测试就是问你自己,如果你搬移了某些值域和函数,会发生什么事?其他值域和函数是否因此变得无意义?
另一个往往在开发后期出现的信号是class的「subtyped方式」。如果你发现subtyping只影响class的部分特性,或如果你发现某些特性「需要以此方式subtyped」,某些特性「需要以彼方式subtyped」,这就意味你需要分解原来的class。
作法
- 决定如何分解c;ass所负责任。
- 建立一个新class,用以表现从旧class中分离出来的责任。
Ø 如果旧class剩下的责任与旧class名称不符,为旧class易名。 - 建立「从旧class访问新class」的连接关系(link)。
Ø 也许你有可能需要一个双向连接。但是在真正需要它之前,不要建立 「从新class通往旧class」的连接。 - 对于你想搬移的每一个值域,运用Move Field 搬移之。
- 每次搬移后,编译、测试。
- 使用Move Method 将必要函数搬移到新class。先搬移较低层函数(也就是「被其他函数调用」多于「调用其他函数」者),再搬移较高层函数。
- 每次搬移之后,编译、测试。
- 检查,精简每个class的接口。
Ø 如果你建立起双向连接,检查是否可以将它改为单向连接。 - 决定是否让新class曝光。如果你的确需要曝光它,决定让它成为reference object (引用型对象〕或immutable value object(不可变之「实值型对象」)。
范例
让我们从一个简单的Person class开始
class Person...
private String _name;
private String _officeAreaCode;
private String _officeNumber;
public String getName() {
return _name;
}
public String getTelephoneNumber() {
return ("(" + _officeAreaCode + ") " + _officeNumber);
}
String getOfficeAreaCode() {
return _officeAreaCode;
}
void setOfficeAreaCode(String arg) {
_officeAreaCode = arg;
}
String getOfficeNumber() {
return _officeNumber;
}
void setOfficeNumber(String arg) {
_officeNumber = arg;
}
在这个例子中,我可以将「与电话号码相关」的行为分离到一个独立class中。首 先我耍定义一个TelephoneNumber class来表示「电话号码」这个概念:
class TelephoneNumber {
}
易如反掌!然后,我要建立从Person到TelephoneNumber的连接:
class Person
private TelephoneNumber _officeTelephone = new TelephoneNumber();
现在,我运用Move Field 移动一个值域:
class Person...
public String getName() {
return _name;
}
public String getTelephoneNumber(){
return _officeTelephone.getTelephoneNumber();
}
TelephoneNumber getOfficeTelephone() {
return _officeTelephone;
}
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
class TelephoneNumber...
public String getTelephoneNumber() {
return ("(" + _areaCode + ") " + _number);
}
String getAreaCode() {
return _areaCode;
}
void setAreaCode(String arg) {
_areaCode = arg;
}
String getNumber() {
return _number;
}
void setNumber(String arg) {
_number = arg;
}
private String _number;
private String _areaCode;
下一步要做的决定是:要不要对客户揭示这个新口class?我可以将Person中「与电 话号码相关」的函数委托(delegating)至TelephoneNumber,从而完全隐藏这个新class;也可以直接将它对用户曝光。我还可以将它暴露给部分用户(位于同一个package中的用户),而不暴露给其他用户。
如果我选择暴露新class,我就需要考虑别名(aliasing)带来的危险。如果我暴露了TelephoneNumber ,而有个用户修改了对象中的_areaCode值域值,我又怎么能知道呢?而且,做出修改的可能不是直接用户,而是用户的用户的用户。
面对这个问题,我有下列数种选择:
- 允许任何对象修改TelephoneNumber 对象的任何部分。这就使得TelephoneNumber 对象成为引用对象(reference object),于是我应该考虑使用 Change Value to Reference。这种情况下,Person应该是TelephoneNumber的访问点。
- 不许任何人「不通过Person对象就修改TelephoneNumber 对象」。为了达到目的,我可以将TelephoneNumber「设为不可修改的(immutable),或为它提供一个不可修改的接口(immutable interface)。
- 另一个办法是:先复制一个TelephoneNumber 对象,然后将复制得到的新对象传递给用户。但这可能会造成一定程度的迷惑,因为人们会认为他们可以修改TelephoneNumber对象值。此外,如果同一个TelephoneNumber 对象 被传递给多个用户,也可能在用户之间造成别名(aliasing)问题。
Extract Class 是改善并发(concurrent)程序的一种常用技术,因为它使你可以为提炼后的两个classes分别加锁(locks)。如果你不需要同时锁定两个对象, 你就不必这样做。这方面的更多信息请看Lea[Lea], 3.3节。
这里也存在危险性。如果需要确保两个对象被同时锁定,你就面临事务(transaction)问题,需要使用其他类型的共享锁〔shared locks〕。正如Lea[Lea] 8.1节所讨论, 这是一个复杂领域,比起一般情况需要更繁重的机制。事务(transaction)很有实用性,但是编写事务管理程序(transaction manager)则超出了大多数程序员的职责范围。
Inline Class(将类内联化)
问题:你的某个class没有做太多事情(没有承担足够责任)。
解决:将class的所有特性搬移到另一个class中,然后移除原class。
动机
Inline Class正好与Extract Class 相反。如果一个class不再承担足够 责任、不再有单独存在的理由〔这通常是因为此前的重构动作移走了这个class的 责任),我就会挑选这一「萎缩class」的最频繁用户(也是个class),以Inline Class手法将「妻缩class」塞进去。
作法
- 在absorbing class(合并端的那个class)身上声明source class的public协议, 并将其中所有函数委托(delegate)至source class。
Ø 如果「以一个独立接口表示source class函数」更合适的话,就应该在inlining之前先使用Extract Interface。 - 修改所有source class引用点,改而引用absorbing class。
Ø 将source class声明为private,以斩断package之外的所有引用可能。 同时并修改source class的名称,这便可使编译器帮助你捕捉到所有对于source class的"dangling references "(虚悬引用点)。 - 编译,测试。
- 运用Move Method 和 Move Field ,将source class的特性全部搬移至absorbing class。
- 为source class举行一个简单的丧礼。
范例
先前(上个重构项〉我从TelephoneNumber「提炼出另一个class,现在我要将它inlining塞回到Person去。一开始这两个classes是分离的:
class Person...
private String _number;
private String _areaCode;
public String getName() {
return _name;
}
public String getTelephoneNumber(){
return _officeTelephone.getTelephoneNumber();
}
TelephoneNumber getOfficeTelephone() {
return _officeTelephone;
}
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
class TelephoneNumber...
public String getTelephoneNumber() {
return ("(" + _areaCode + ") " + _number);
}
String getAreaCode() {
return _areaCode;
}
void setAreaCode(String arg) {
_areaCode = arg;
}
String getNumber() {
return _number;
}
void setNumber(String arg) {
_number = arg;
}
首先我在Person中声明TelephoneNumber「的所有「可见」(public)函数:
class Person...
String getAreaCode() {
return _officeTelephone.getAreaCode(); //译注:请注意其变化
}
void setAreaCode(String arg) {
_officeTelephone.setAreaCode(arg); //译注:请注意其变化
}
String getNumber() {
return _officeTelephone.getNumber(); //译注:请注意其变化
}
void setNumber(String arg) {
_officeTelephone.setNumber(arg); //译注:请注意其变化
}
现在,我要找出TelephoneNumber的所有用户,让它们转而使用Person接口。于是下列代码:
Person martin = new Person();
martin.getOfficeTelephone().setAreaCode ("781");
就变成了:
Person martin = new Person();
martin.setAreaCode ("781");
现在,我可以持续使用Move Method 和 Move Field ,直到TelephoneNumber不复存在。
Hide Delegate(隐藏「委托关系」)
问题:客户直接调用其server object(服务对象)的delegate class。
解决:在server端(某个class〕建立客户所需的所有函数,用以隐藏委托关系(delegation)。
动机
封装」即使不是对象的最关键特征,也是最关键特征之一。「封装」意味每个对象都应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一 变化的对象就会比较少——这会使变化比较容易进行。
任何学过对象技术的人都知道:虽然Java允许你将值域声明为public,但你还是应该隐藏对象的值域。随着经验日渐丰富,你会发现,有更多可以(并值得)封装的东西。
如果某个客户调用了「建立于server object (服务对象)的某个值域基础之上」的函数,那么客户就必须知晓这一委托对象(delegate object。译注:即server object的那个特殊值域)。万一委托关系发生变化,客户也得相应变化。你可以在server 端放置一个简单的委托函数(delegating method),将委托关系隐藏起来,从而去除这种依存性(图7.1)。这么一来即便将来发生委托关系上的变化,变化将被限制在server中,不会波及客户。
对于某些客户或全部客户,你可能会发现,有必要先使用Extract Class 。一旦你对所有客户都隐藏委托关系(delegation),你就可以将server 接口中的所有 委托都移除。
作法
- 对于每一个委托关系中的函数,在server端建立一个简单的委托函数(delegating method)。
- 调整客户,令它只调用server 提供的函数(译注:不得跳过径自调用下层)。
Ø 如果client (客户〕和server不在同一个package,考虑修改委托函数 (delegate method)的访问权限,让client得以在package之外调用它。 - 每次调整后,编译并测试。
- 如果将来不再有任何客户需要取用图7.1的Delegate (受托类),便可移除server中的相关访问函数(accessor for the delegate)。
- 编译,测试。
范例
本例从两个classes开始,代表「人」的Person和代表「部门」的Department:
class Person {
Department _department;
public Department getDepartment() {
return _department;
}
public void setDepartment(Department arg) {
_department = arg;
}
}
class Department {
private String _chargeCode;
private Person _manager;
public Department (Person manager) {
_manager = manager;
}
public Person getManager() {
return _manager;
}
...
如果客户希望知道某人的经理是谁,他必须先取得Department对象:
manager = john.getDepartment().getManager();
这样的编码就是对客户揭露了Department的工作原理,于是客户知道:Department用以追踪「经理」这条信息。
如果对客户隐藏Department,可以减少耦合(coupling)。 为了这一目的,我在Person中建立一个简单的委托函数:
public Person getManager() {
return _department.getManager();
}
现在,我得修改Person的所有客户,让它们改用新函数:
manager = john.getManager();
只要完成了对Department所有函数的委托关系,并相应修改了Person的所有客 户,我就可以移除Person中的访问函数getDepartment()了。
Remove Middle Man(移除中间人)
问题:某个class做了过多的简单委托动作(simple delegation)。
解决:让客户直接调用delegate(受托类)。
动机
在Hide Delegate的「动机」栏,我谈到了「封装 delegated object(受托对 象)」的好处。
但是这层封装也是要付出代价的,它的代价就是:每当客户要使用 delegate(受托类)的新特性时,你就必须在server 端添加一个简单委托函数。随着delegate的特性(功能)愈来愈多,这一过程会让你痛苦不己。server 完全变成了一 个「中间人」,此时你就应该让客户直接调用delegate。
很难说什么程度的隐藏才是合适的。还好,有了Hide Delegate和Remove Middle Man,你大可不必操心这个问题,因为你可以在系统运行过程中不断进行调整。随着系统的变化,「合适的隐藏程度」这个尺度也相应改变。六个月 前恰如其分的封装,现今可能就显得笨拙。重构的意义就在于:你永远不必说对不起——只要把出问题的地方修补好就行了。
做法
- 建立一个函数,用以取用delegate(受托对象)。
- 对于每个委托函数(delegate method),在server中删除该函数,并将「客户对该函数的调用」替换为「对delegate(受托对象)的调用」。
- 处理每个委托函数后,编译、测试。
范例
我将以另一种方式使用先前用过的「人与部门」例子。还记得吗,上一项重构结束时,Person将Department隐藏起来了:
class Person...
Department _department;
public Person getManager() {
return _department.getManager();
class Department...
private Person _manager;
public Department (Person manager) {
_manager = manager;
}
为了找出某人的经理,客户代码可能这样写:
manager = john.getManager();
像这样,使用和封装Department都很简单。但如果大量函数都这么做,我就不得不在Person之中安置大量委托行为(delegations)。这就是移除中间人的时候了。 首先在Person建立一个「受托对象(delegate)取得函数」:
class Person...
public Department getDepartment() {
return _department;
}
然后逐一处理每个委托函数。针对每一个这样的函数,我要找出通过Person使用的函数,并对它进行修改,使它首先获得受托对象(delegate),然后直接使用之:
manager = john.getDepartment().getManager();
然后我就可以删除Person的getManager() 函数。如果我遗漏了什么,编译器会 告诉我。
为方便起见,我也可能想要保留一部分委托关系(delegations)。此外我也可能希望对某些客户隐藏委托关系,并让另一些用户直接使用受托对象。基于这些原因,一些简单的委托关系(以及对应的委托函数)也可能被留在原地。
Introduce Foreign Method(引入外加函数)
问题:你所使用的server class需要一个额外函数,但你无法修改这个class。
解决:在client class中建立一个函数,并以一个server class实体作为第一引数(argument)
Date newStart = new Date (previousEnd.getYear(),
previousEnd.getMonth(), previousEnd.getDate() + 1);
Date newStart = nextDay(previousEnd);
private static Date nextDay(Date arg) {
return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
}
动机
这种事情发生过太多次了:你正在使用一个class,它真的很好,为你提供了你想要的所有服务。
而后,你又需要一项新服务,这个class却无法供应。
于是你开始咒骂:「为什么不能做这件事?」如果可以修改源码,你便可以自行添加一个新函数; 如果不能,你就得在客户端编码,补足你要的那个函数。
如果client class只使用这项功能一次,那么额外编码工作没什么大不了,甚至可能根本不需要原本提供服务的那个class。然而如果你需要多次使用这个函数,你就得不断重复这些代码。还记得吗,重复的代码是软件万恶之源。这些重复性代码应该被抽出来放进同一个函数中。进行本项重构时,如果你以外加函数实现一项功能, 那就是一个明确信号:这个函数原本应该在提供服务的(server)class中加以实现。
如果你发现自己为一个server class建立了大量外加函数,或如果你发现有许多classes都需要同样的外加函数,你就不应该再使用本项重构,而应该使用 Introduce Local Extension。
但是不要忘记:外加函数终归是权宜之计。如果有可能,你仍然应该将这些函数搬移到它们的理想家园。如果代码拥有权(code ownership)是个需要考量的问题, 就把外加函数交给server class的拥有者,请他帮你在此server class中实现这个函数。
作法
- 在client class中建立一个函数,用来提供你需要的功能。
Ø 这个函数不应该取用client class的任何特性。如果它需要一个值,把该值当作参数传给它。 - 以server class实体作为该函数的第一个参数。
- 将该函数注释为:「外加函数(foreign method),应在server class实现。」
Ø 这么一来,将来如果有机会将外加函数搬移到server class中,你便可以轻松找出这些外加函数。
范例
程序中,我需要跨过一个收费周期(billing period)。原本代码像这样:
Date newStart = new Date (previousEnd.getYear(),
previousEnd.getMonth(), previousEnd.getDate() + 1);
我可以将赋值运算右侧代码提炼到一个独立函数中。这个函数就是Date class的一个外加函数:
Date newStart = nextDay(previousEnd);
private static Date nextDay(Date arg) {
// foreign method, should be on date
return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
}
Introduce Local Extension(引入本地扩展)
问题:你所使用的server class需要一些额外函数,但你无法修改这个class。
解决:建立一个新class,使它包含这些额外函数。让这个扩展品成为source class的subclass (子类〕或wrapper(外覆类)。
动机
很遗憾,classes的作者无法预知未来,他们常常没能为你预先准备一些有用的函数。
如果你可以修改源码,最好的办法就是直接加入自己需要的函数。但你经常无法修改源码。如果只需要一两个函数,你可以使用Introduce Foreign Method。
但如果你需要的额外函数超过两个,外加函数(foreign methods)就很难控制住它 们了。所以,你需要将这些函数组织在一起,放到一个恰当地方去。要达到这一目 的,标准对象技术subclassing和wrapping是显而易见的办法。这种情况下我把 subclass 或wrapper称为local extention(本地扩展〕。
所谓本地扩展是一个独立的class,但也是被扩展的子类型。这意味它提供original class的一切特性,同时并额外添加新特性。在任何使用original class的地方,你都可以使用local extention取而代之。
作法
- 建立一个extension class,将它作为原物(原类〉的subclass或wrapper。
- 在extension class 中加入转型构造函数(converting constructors )。
Ø 所谓「转型构造函数」是指接受原物(original)作为参数。如果你釆用subclassing方案,那么转型构造函数应该调用适当的subclass构造函数;如果你采用wrapper方案,那么转型构造函数应该将它所获得之引数(argument)赋值给「用以保存委托关系(delegate)」的那个值域。 - 在extension class中加入新特性。
- 根据需要,将原物(original)替换为扩展物(extension)。
- 将「针对原始类(original class)而定义的所有外加函数(foreign methods)」 搬移到扩展类extension中。
范例
我将以Java 1.0.1的Date class为例。Java 1.1已经提供了我想要的功能,但是在它到来之前的那段日子,很多时候我需要扩展Java 1.0.1的Date class。
第一件待决事项就是使用subclass或wrapper。subclassing是比较显而易见的办法:
Class mfDate extends Date {
public nextDay()...
public dayOfYear()...
wrapper则需要用上委托(delegation):
class mfDate {
private Date _original;
范例:是用Subclass(子类)
首先,我要建立一个新的MfDateSub class来表示「日期」(译注:"Mf"是作者Martin Fowler的姓名缩写),并使其成为Date的subclass:
class MfDateSub extends Date
然后,我需要处理Date 和我的extension class之间的不同处。MfDateSub 构造函数需要委托(delegating)给Date构造函数:
public MfDateSub (String dateString) {
super (dateString);
};
现在,我需要加入一个转型构造函数,其参数是一个隶属原类的对象:
public MfDateSub (Date arg) {
super (arg.getTime());
}
现在,我可以在extension class中添加新特性,并使用Move Method 将所有外加函数(foreign methods)搬移到extension class。于是,下面的代码:
client class...
private static Date nextDay(Date arg) {
// foreign method, should be on date
return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
}
经过搬移之后,就成了:
class MfDate...
Date nextDay() {
return new Date (getYear(),getMonth(), getDate() + 1);
}
范例:是用wrapper(外覆类)
首先声明一个wrapping class:
class mfDate {
private Date _original;
}
使用wrapping方案时,我对构造函数的设定与先前有所不同。现在的构造函数将只是执行一个单纯的委托动作(delegation):
public MfDateWrap (String dateString) {
_original = new Date(dateString);
};
而转型构造函数则只是对其instance变量赋值而己:
public MfDateWrap (Date arg) {
_original = arg;
}
接下来是一项枯燥乏味的工作:为原始类的所有函数提供委托函数。我只展示两个函数,其他函数的处理依此类推。
public int getYear() {
return _original.getYear();
}
public boolean equals (MfDateWrap arg) {
return (toDate().equals(arg.toDate()));
}
完成这项工作之后,我就可以后使用Move Method 将日期相关行为搬移到新class中。于是以下代码:
client class...
private static Date nextDay(Date arg) {
// foreign method, should be on date
return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
}
经过搬移之后,就变成:
class MfDate...
Date nextDay() {
return new Date (getYear(),getMonth(), getDate() + 1);
}
使用wrappers有一个特殊问题:如何处理「接受原始类之实体为参数」的函数?例如:
public boolean after (Date arg)
由于无法改变原始类〔original),所以我只能以一种方式使用上述的after() :
aWrapper.after(aDate) // can be made to work
aWrapper.after(anotherWrapper) // can be made to work
aDate.after(aWrapper) // will not work
这样覆写(overridden)的目的是为了向用户隐藏wrapper 的存在。这是一个好策略,因为wrapper 的用户的确不应该关心wrapper 的存在,的确应该可以同样地对待wrapper(外覆类)和orignal((原始类)。但是我无法完全隐藏此一信息,因为某些系统所提供的函数(例如equals() 会出问题。
你可能会认为:你可以在MfDateWrap class 中覆写equals(),像这样:
public boolean equals (Date arg) // causes problems
但这样做是危险的,因为尽管我达到了自己的目的,Java 系统的其他部分都认为equals() 符合交换律:如果a.equals(b)为真,那么b.equals(a)也必为真。违反这一规则将使我遭遇一大堆莫名其妙的错误。
要避免这样的尴尬境地,惟一办法就是修改Date class。但如果我能够修改Date ,我又何必进行此项重构?所以,在这种情况下,我只能(必须〕向用户暴露「我进行了包装」这一事实。我将以一个新函数来进行日期之间的相等性检查(equality tests):
public boolean equalsDate (Date arg)
我可以重载equalsDate() ,让一个重载版本接受Date 对象,另一个重载版本接受MfDateWrap 对象。这样我就不必检查未知对象的型别了:
public boolean equalsDate (MfDateWrap arg)
subclassing方案中就没有这样的问题,只要我不覆写原函数就行了。
但如果我覆写了original class 中的函数,那么寻找函数时,我会被搞得晕头转向。一般来说,我不会在extension class 中覆写0original class 的函数,我只会添加新函数。
译注:equality(相等性)是一个很基础的大题目。《Effective Java》 by Joshua Bloch 第3章,以及《Practical Java》by Peter Haggar 第2章,对此均有很深入的讨论。这两本书对于其他的基础大题目如Serizable,Comparable,Cloneable,hashCode() 也都有深刻讨论。
感谢观看 你肯定有收获对吧