重构改善既有代码设计--重构手法10:Move Method (搬移函数)

时间:2021-08-21 14:05:56

你的程序中,有个函数与其所驻类之外的另一个类进行更多的交流:调用后者,或被后者调用。在该函数最常用引用的类中建立一个有着类似行为的新函数。将旧函数编程一个单纯的委托函数,或是将旧函数完全移除。

动机:“搬移函数”是重构理论的支柱。如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,就需要搬移函数。通过这种手段,可以使系统中的类更简单,这些类最终也将更干净利落的实现系统交付的工作。

浏览类的所有函数,从中找出这样的函数:使用另一个对象的次数比使用自己所驻对象的次数还多。一旦移动了一些字段,就该做这样的检查。一旦发现有可能搬移的函数,就观察调用它的那一端、它调用的那一端,已经继承体系中它的任何一个重定义函数。然后,会根据“这个函数与哪个对象的交流比较多”,决定其移动路径。

这往往不是容易做出的决定。如果不能肯定是否应该移动一个函数,就继续观察其他函数。移动其他函数往往会让这项决定变得容易一些。有时候,即使你移动了其他函数,还是很难对眼下这个函数做出决定。其实这也没什么大不了的。如果真的很难做出决定,那么也许“移动这个函数与否”并不是那么重要。所以,就凭本能去做,反正以后总是可以修改的。

做法:1、检查源类中被源函数使用的一切特性(包括字段和函数),考虑它们是否也该被搬移。如果某个特性只被你打算搬移的那个函数用到,就应该将它一并搬移。如果另有其他函数使用了这个特性,你可以考虑将使用该特性的所有函数全都一并搬移。有时候,搬移一组函数必逐一搬移简单些。

2、检查源类的子类和超类,看看是否有该函数的其他声明。如果出现其他声明,你或许无法进行搬移。除非目标类也同样表现出多态性。

3、在目标类中声明这个函数。可以为此函数选择一个新名称—对目标类更有意义的名称。

4、将源函数的代码复制到目标函数中。调整后者,使其能在新家中正常运行。如果目标函数使用了源类中的特性,你得决定任何从目标函数引用源对象。如果目标类中没用相应的引用机制,就把源对象的引用当做参数,传给新建立的目标函数。

5、编译目标类。

6、决定任何从源函数正确引用目标对象。可能会有一个现成的字段或函数帮助你取得目标对象。如果没有,就看能否轻松建立一个这样的函数。如果还是不行,就得在源类中新建一个字段来保存对象。这可能是一个永久性修改,但你也可以让它是暂时的,因为后续的其他重构项目可能会把这个新建字段去掉。

7、修改源函数,使之成为纯委托函数。

8、编译,测试。

9、决定是否删除源函数,或将它当做一个委托函数保留下来。如果你经常要在源对象中引用目标函数,那么将源函数作为委托函数保留下来比较简单。

10、如果要移除源函数,请将源类中对源函数的所有调用,替换为对目标函数的调用。你可以没修改一个引用点就编译并测试一次。也可以通过一次“查找/替换”改掉所有的引用点。

范例(Examples)

我用一个表示「帐户」的account class来说明这项重构:

class Account...

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;

}

private AccountType _type;

private int _daysOverdrawn;

假设有数种新帐户,每一种都有自己的「透支金计费规则」。所以我希望将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。