重构之在对象之间搬移特性

@(常识)
[toc]

如果你注定要成为厉害的人, 那问题的答案就深藏在你的血脉里。

本篇文章主要讲解 《重构—-改善既有代码的设计》 这本书中的 第七章在对象之间搬移特性中 的知识点,

Move Method(搬移函数)

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

动机

「函数搬移」是重构理论的支柱。如果一个class有太多行为,或如果一个class与另一个class有太多合作而形成高度耦合(highly coupled),我就会搬移函数。通过这种手段,我可以使系统中的classes更简单,这些classes最终也将更干净利落地实现系统交付的任务。
常常我会浏览class的所有函数,从中寻找这样的函数:使用另一个对象的次数比使用自己所驻对象的次数还多。一旦我移动了一些值域,就该做这样的检查。一旦发现「有可能被我搬移」的函数,我就会观察调用它的那一端、它调用的那一端,以及继承体系中它的任何一个重定义函数。然后,我会根据「这个函数与哪个对象的交流比较多」,决定其移动路径。
这往往不是一个容易做出的决定。如果不能肯定是否应该移动一个函数,我就会继续观察其他函数。移动其他函数往往会让这项决定变得容易一些。有时候,即使你移动了其他函数,还是很难对眼下这个函数做出决定。其实这也没什么大不了的。 如果真的很难做出决定,那么或许「移动这个函数与否」并不那么重要。所以,我会凭本能去做,反正以后总是可以修改的。

作法

  • 检查source class定义之source method所使用的一切特性(features),考虑它们是否也该被搬移。(译注:此处所谓特性泛指class定义的所有东西,包括值域和函数。)
  • 如果某个特性只被你打算搬移的那个函数用到,你应该将它一并搬移。如果另有其他函数使用了这个特性,你可以考虑将使用该特性的所有函数全都一并搬移。有时候搬移一组函数比逐一搬移简单些。

    范例

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

    1. class Account...
    2. //用户类型类
    3. private AccountType _type;
    4. //透支天数
    5. private int _daysOverdrawn;
    6. //透支费用
    7. double overdraftCharge() { //译注:透支金计费,它和其他class的关系似乎比较密切。
    8. //判断保险
    9. if (_type.isPremium()) {
    10. double result = 10;
    11. if (_daysOverdrawn > 7) result += (_daysOverdrawn - 7) * 0.85;
    12. return result;
    13. }
    14. else return _daysOverdrawn * 1.75;
    15. }
    16. //银行操作
    17. double bankCharge() {
    18. double result = 4.5;
    19. if (_daysOverdrawn > 0) result += overdraftCharge();
    20. return result;
    21. }

    假设有数种新帐户,每一种都有自己的「透支金计费规则」。
    所以我希望将overdraftCharge()搬移到AccountType class去。
    第一步要做的是:观察被overdraftCharge()使用的每一特性(features),考虑是否值得将它们与overdraftCharge()—起移动。此例之中我需要让daysOverdrawn值域留在Account class,因为其值会随不同种类的帐户而变化。然后,我将overdraftCharge()函数码拷贝到AccountType中,并做相应调整。

    1. class AccountType...
    2. double overdraftCharge(int daysOverdrawn) {
    3. if (isPremium()) {
    4. double result = 10;
    5. if (daysOverdrawn > 7) result += (daysOverdrawn - 7) * 0.85;
    6. return result;
    7. }
    8. else return daysOverdrawn * 1.75;
    9. }

    在这个例子中,「调整」的意思是:(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),然后编译并测试:

    1. class Account...
    2. double overdraftCharge() {
    3. return _type.overdraftCharge(_daysOverdrawn);
    4. }

    我可以保留代码如今的样子,也可以删除source method。如果决定删除,就得找出source method的所有调用者,并将这些调用重新定向,改调用Account的bankCharge():

    1. class Account...
    2. double bankCharge() {
    3. double result = 4.5;
    4. if (_daysOverdrawn > 0) result += _type.overdraftCharge(_daysOverdrawn);
    5. return result;
    6. }

    所有调用点都修改完毕后,我就可以删除source method在Account中的声明了。我可以在每次删除之后编译并测试,也可以一次性批量完成。如果被搬移的函数不是private,我还需要检查其他classes是否使用了这个函数。在强型(strongly typed) 语言中,删除source method声明式后,编译器会帮我发现任何遗漏。
    此例之中被移函数只取用(指涉〕一个值域,所以我只需将这个值域作为参数传给target method就行了。如果被移函数调用了Account中的另一个函数,我就不能这么简单地处理。这种情况下我必须将source object传递给target method:

    1. class AccountType...
    2. double overdraftCharge(Account account) {
    3. if (isPremium()) {
    4. double result = 10;
    5. if (account.getDaysOverdrawn() > 7)
    6. result += (account.getDaysOverdrawn() - 7) * 0.85;
    7. return result;
    8. }
    9. else return account.getDaysOverdrawn() * 1.75;
    10. }

    如果我需要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的部分代码:

    1. class Account...
    2. private AccountType _type;
    3. private double _interestRate;
    4. double interestForAmount_days (double amount, int days) {
    5. return _interestRate * amount * days / 365;
    6. }

    我想把表示利率的_interestRate搬移到AccountType class去。
    目前已有数个函数引用了它,interestForAmount_days() 就是其一。
    下一步我要在AccountType中建立_interestRate field以及相应的访问函数:

    1. class AccountType...
    2. private double _interestRate;
    3. void setInterestRate (double arg) {
    4. _interestRate = arg;
    5. }
    6. double getInterestRate () {
    7. return _interestRate;
    8. }

    这时候我可以编译新的AccountType class。
    现在,我需要让Account class中访问此_interestRate field的函数转而使用AccountType对象,然后删除Account class中的_interestRate field。
    我必须删除source field,才能保证其访问函数的确改变了操作对象,因为编译器会帮我指出未正确获得修改的函数。

    1. private double _interestRate;
    2. double interestForAmount_days (double amount, int days) {
    3. return _type.getInterestRate() * amount * days / 365;
    4. }

    范例:使用Self Encapsulate(自我封装)

    如果有很多函数已经使用了_interestRate field,我应该先运用Self Encapsulate Field:

    1. class Account...
    2. private AccountType _type;
    3. private double _interestRate;
    4. double interestForAmount_days (double amount, int days) {
    5. return getInterestRate() * amount * days / 365;
    6. }
    7. private void setInterestRate (double arg) {
    8. _interestRate = arg;
    9. }
    10. private double getInterestRate () {
    11. return _interestRate;
    12. }

    这样,在搬移field之后,我就只需要修改访问函数(accessors)就行了 :

    1. double interestForAmountAndDays (double amount, int days) {
    2. return getInterestRate() * amount * days / 365;
    3. }
    4. private void setInterestRate (double arg) {
    5. _type.setInterestRate(arg);
    6. }
    7. private double getInterestRate () {
    8. return _type.getInterestRate();
    9. }

    如果以后有必要,我可以修改访问函数(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开始
    1. class Person...
    2. private String _name;
    3. private String _officeAreaCode;
    4. private String _officeNumber;
    5. public String getName() {
    6. return _name;
    7. }
    8. public String getTelephoneNumber() {
    9. return ("(" + _officeAreaCode + ") " + _officeNumber);
    10. }
    11. String getOfficeAreaCode() {
    12. return _officeAreaCode;
    13. }
    14. void setOfficeAreaCode(String arg) {
    15. _officeAreaCode = arg;
    16. }
    17. String getOfficeNumber() {
    18. return _officeNumber;
    19. }
    20. void setOfficeNumber(String arg) {
    21. _officeNumber = arg;
    22. }
    在这个例子中,我可以将「与电话号码相关」的行为分离到一个独立class中。首 先我耍定义一个TelephoneNumber class来表示「电话号码」这个概念:
    1. class TelephoneNumber {
    2. }
    易如反掌!然后,我要建立从Person到TelephoneNumber的连接:
    1. class Person
    2. private TelephoneNumber _officeTelephone = new TelephoneNumber();
    现在,我运用Move Field 移动一个值域:
    1. class Person...
    2. public String getName() {
    3. return _name;
    4. }
    5. public String getTelephoneNumber(){
    6. return _officeTelephone.getTelephoneNumber();
    7. }
    8. TelephoneNumber getOfficeTelephone() {
    9. return _officeTelephone;
    10. }
    11. private String _name;
    12. private TelephoneNumber _officeTelephone = new TelephoneNumber();
    13. class TelephoneNumber...
    14. public String getTelephoneNumber() {
    15. return ("(" + _areaCode + ") " + _number);
    16. }
    17. String getAreaCode() {
    18. return _areaCode;
    19. }
    20. void setAreaCode(String arg) {
    21. _areaCode = arg;
    22. }
    23. String getNumber() {
    24. return _number;
    25. }
    26. void setNumber(String arg) {
    27. _number = arg;
    28. }
    29. private String _number;
    30. private String _areaCode;
    下一步要做的决定是:要不要对客户揭示这个新口class?我可以将Person中「与电 话号码相关」的函数委托(delegating)至TelephoneNumber,从而完全隐藏这个新class;也可以直接将它对用户曝光。我还可以将它暴露给部分用户(位于同一个package中的用户),而不暴露给其他用户。
    如果我选择暴露新class,我就需要考虑别名(aliasing)带来的危险。如果我暴露了TelephoneNumber ,而有个用户修改了对象中的_areaCode值域值,我又怎么能知道呢?而且,做出修改的可能不是直接用户,而是用户的用户的用户。
    面对这个问题,我有下列数种选择:
  1. 允许任何对象修改TelephoneNumber 对象的任何部分。这就使得TelephoneNumber 对象成为引用对象(reference object),于是我应该考虑使用 Change Value to Reference。这种情况下,Person应该是TelephoneNumber的访问点。
  2. 不许任何人「不通过Person对象就修改TelephoneNumber 对象」。为了达到目的,我可以将TelephoneNumber「设为不可修改的(immutable),或为它提供一个不可修改的接口(immutable interface)。
  3. 另一个办法是:先复制一个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是分离的:

    1. class Person...
    2. private String _number;
    3. private String _areaCode;
    4. public String getName() {
    5. return _name;
    6. }
    7. public String getTelephoneNumber(){
    8. return _officeTelephone.getTelephoneNumber();
    9. }
    10. TelephoneNumber getOfficeTelephone() {
    11. return _officeTelephone;
    12. }
    13. private String _name;
    14. private TelephoneNumber _officeTelephone = new TelephoneNumber();
    15. class TelephoneNumber...
    16. public String getTelephoneNumber() {
    17. return ("(" + _areaCode + ") " + _number);
    18. }
    19. String getAreaCode() {
    20. return _areaCode;
    21. }
    22. void setAreaCode(String arg) {
    23. _areaCode = arg;
    24. }
    25. String getNumber() {
    26. return _number;
    27. }
    28. void setNumber(String arg) {
    29. _number = arg;
    30. }

    首先我在Person中声明TelephoneNumber「的所有「可见」(public)函数:

    1. class Person...
    2. String getAreaCode() {
    3. return _officeTelephone.getAreaCode(); //译注:请注意其变化
    4. }
    5. void setAreaCode(String arg) {
    6. _officeTelephone.setAreaCode(arg); //译注:请注意其变化
    7. }
    8. String getNumber() {
    9. return _officeTelephone.getNumber(); //译注:请注意其变化
    10. }
    11. void setNumber(String arg) {
    12. _officeTelephone.setNumber(arg); //译注:请注意其变化
    13. }

    现在,我要找出TelephoneNumber的所有用户,让它们转而使用Person接口。于是下列代码:

    1. Person martin = new Person();
    2. martin.getOfficeTelephone().setAreaCode ("781");

    就变成了:

    1. Person martin = new Person();
    2. 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:

    1. class Person {
    2. Department _department;
    3. public Department getDepartment() {
    4. return _department;
    5. }
    6. public void setDepartment(Department arg) {
    7. _department = arg;
    8. }
    9. }
    10. class Department {
    11. private String _chargeCode;
    12. private Person _manager;
    13. public Department (Person manager) {
    14. _manager = manager;
    15. }
    16. public Person getManager() {
    17. return _manager;
    18. }
    19. ...

    如果客户希望知道某人的经理是谁,他必须先取得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)

    1. Date newStart = new Date (previousEnd.getYear(),
    2. previousEnd.getMonth(), previousEnd.getDate() + 1);
    3. Date newStart = nextDay(previousEnd);
    4. private static Date nextDay(Date arg) {
    5. return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
    6. }

    动机

    这种事情发生过太多次了:你正在使用一个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)。原本代码像这样:

    1. Date newStart = new Date (previousEnd.getYear(),
    2. previousEnd.getMonth(), previousEnd.getDate() + 1);

    我可以将赋值运算右侧代码提炼到一个独立函数中。这个函数就是Date class的一个外加函数:

    1. Date newStart = nextDay(previousEnd);
    2. private static Date nextDay(Date arg) {
    3. // foreign method, should be on date
    4. return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
    5. }

    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() 也都有深刻讨论。

    感谢观看 你肯定有收获对吧