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

2 篇文章 0 订阅

楔子

学习笔记 《重构,改善既有代码的设计》

类往往会因为承担过多责任而变得臃肿不堪。这种情况下,我们使用Extract Class将一部分责任分离出去。如果一个类变得太“不负责任”,我们就会使用Inline Class将它融入另一个类。如果一个了你使用了另一个类,运用Hide Delegate将这种关系隐藏起来通常是有帮助的。有时候隐藏委托会导致拥有者的接口经常变化,此时需要使用Remove Middle Man。


7.1 Move Method(搬移函数)

在该函数最常引用的类中建立一个有类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。


动机

“搬移函数”是重构理论的支柱。如果一个类有太多的行为,或者如果一个类与另一个类有大多合作形成的高度耦合,我们就可以搬移函数。通过这种手段,可以使系统中的类更简单,这些类最终也将更干净利落地实现系统交付的任务。
我们常常会浏览类的所有函数,从中寻找这样的函数:使用另一个对象的次数比使用自己所驻对象的次数还多。一旦我们移动成功了一些字段,就该做这样的检查。一旦发现有可能搬移的函数,我们就会观察调用它的那一端、它调用的那一端。以及继承体系中它的任何一个重定义函数。然后,会根据“这个函数与哪个对象的交流比较多”,决定其移动路劲。

做法

  • 检查源类中被源函数所使用的一切特性(包括字段和函数),考虑它们是否也被搬移。
    • 如果某个特性只被你打断搬移的那个函数用到,那应该将它一并搬移。如果另有其他函数使用了这个特性,你可以考虑将使用该特性的所有函数一并搬移。有时候,搬移一组函数比逐一搬移简单些。
  • 检查源类的子类和超类,看卡是否有该函数的其他声明。
    • 如果出现其他声明,或许无法进行搬移,除非目标类也同样表现出多态性。
  • 在目标函数中声明这个函数。
    • 你可以为此函数选择一个新名词——对目标类更有意义的名称。
  • 将源函数的代码复制到目标函数中。调整后者使其能在新家中正常运行。
    • 如果目标函数使用了源类中的特性,你得决定如何从目标函数引用源对象。如果目标类中没有相应的引用机制,就把元对象的引用当做参数,传给新建立的目标函数。
    • 如果源函数包含异常处理,你得判断逻辑上应该由哪个类来处理这一异常,如果应该由源类来负责,就把异常处理留在原地。
  • 编译目标对象
  • 决定如何从源函数正确引用目标对象。
    • 可能会有一个现成的字段或函数帮助你取得目标对象,如果没有,就看能够轻松建立一个这样的函数,如果还是不行,就得在源类中新建一个字段来保存目标对象,这可能是一个永久性修改,但你也可以让它是暂时的,因为后继的其他重构项目可能会把这个新建字段去掉。
  • 修改源函数,使之成为一个纯委托函数。
  • 编译测试
  • 决定是否删除源函数,或将它当做一个委托函数保留下来
  • 如果要移除源函数,请将元类中对源函数的所有调用,替换为目标函数的调用。
  • 编译测试

范例

用一个表示“账户”的Account类说明这个重构。

public class Account {
    double overdraftChange() {
        if (_type.isPremium()) {
            double result = 10;
            if (_daysOverdrawn > 7) {
                result += (_daysOverdrawn - 7) * 0.85;
            }
            return result;
        } else {
            return _daysOverdrawn * 1.75;
        }

    }

    private AccountType _type;
    private int _daysOverdrawn;
}

假设有几种新账户,每一种都有自己的“透支金额计费规则”。我们希望将overdraftChange搬移到AccountType类中去。
第一步要做的是:观察被overdraftChange使用的每一项特性,考虑是否值得他们与overdraftChange一起移动。次例中我们需要让_daysOverdrawn字段保留在A从count类中,因为这个值会随着不同账户而变化。然后,我们将overdraftChange函数复制到AccountType中,并做相应的调整。

public class AccountType {
    double overdraftChange(int daysOverdrawn) {
        if (isPremium()) {
            double result = 10;
            if (daysOverdrawn > 7) {
                result += (daysOverdrawn - 7) * 0.85;
            }
            return result;
        } else {
            return daysOverdrawn * 1.75;
        }

    }
}

这个例子中,“调整”的意思是:1 对于使用AccouontType 特性的语句,去掉_type 。2 想办法得到依旧需要的Account类特性。当我需要使用源类的特性时,有4中选择。① 将这个特性也移动到目标类 ② 建立或使用一个从目标类到源类的引用关系 ③ 将源对象当做参数传给目标函数。 ④ 如果所需特性是个变量。将它当做参数传给目标函数。
本例中,我们将 **_daysOverdrawn **变量作为参数传给目标函数。

public class Account {
    double overdraftChange() {
        return _type.overdraftChange(_daysOverdrawn);
    }

    private AccountType _type;
    private int _daysOverdrawn;
}
我们可以保留代码如今的样子,也可以删除源函数,如果决定删除,就得找出源函数的所有调用者,并将这些调用重新定向。该为调用Account 的bankCharge()。
public class Account {
    double bankCharge() {
        double result = 4.5;
        if (_daysOverdrawn > 0) {
            result += _type.overdraftChange(_daysOverdrawn);
        }
        return result;
    }
}

所有的调用点都修改完毕后,就可以删除源函数在Account中的声明了。我们可以在每次删除之后编译并测试,也可以一次性批量完成。如果被搬移的函数不是private的,我们还需要检查其他类是否使用了这个函数。<br />次例之中被搬移函数只引用了一个字段,所以只需要将这个字段作为参数传给目标函数就行了。如果被搬移函数调用了Account中的另一个函数,我们就不能这么简单地处理,。这种情况下必须将源对象传递给目标函数。
public class AccountType {
    double overdraftChange(Account account) {
        if (isPremium()) {
            double result = 10;
            if (account.get_daysOverdrawn() > 7) {
                result += (account.get_daysOverdrawn() - 7) * 0.85;
            }
            return result;
        } else {
            return account.get_daysOverdrawn() * 1.75;
        }
    }
}

如果需要源类的多个特性,那么我们也会将源对象传递给目标函数。不过如果目标函数需要太多源类特征,就得进一步重构。通常情况下,我们会分解目标函数,并将其中一部分移回源类。


7.2 Move Field(搬移字段)

在程序中,某个字段被其所驻类之外的另一个类更多地用到。在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。

动机

在类之间移动状态和行为,是重构过程中必不可少的措施。随着系统的发展,你会发现自己需要新的类,并需要将现有的工作责任拖到新的类中。
如果我们发现。对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,可以考虑搬移这个字段。

做法

  • 如果字段的访问基本是public。使用 Encapsulate Field 将它封装起来。
    • 如果你有可能移动那些频繁访问该字段的函数,或如果有许多函数访问某个字段,先使用Self Encapsulate Field 也许会有帮助。
  • 编译测试
  • 在目标类中建立与源字段相同的字段,并同时建立相应的设值和取值函数。
  • 编译目标类。
  • 决定如何在源目标对象中引用目标对象。
    • 首先看是否有一个现成的字段或函数可以帮助你得到目标对象。如果没有,就看能否轻易建立这样一个函数。如果还不行,就得在源类中新建一个字段来存放目标对象。这可能是个永久性修改,但是你也可以让它是暂时的,因为后续重构可能会把这个新建字段删除掉。
  • 删除源字段
  • 将所有对源字段的引用替换为某个目标函数的调用。
    • 如果需要读取该变量,就把对源字段的引用替换为对目标取值函数的调用;如果要对该变量赋值,就把对源字段的引用替换成设值函数的调用。
  • 编译测试

### 范例 下面是Account类的部分代码。 ```java public class Account { private AccountType _type; private double _interestRate;
double interestForAmount_days(double amount, int days) {
    return _interestRate * amount * days / 365;
}

}

	想把表示利率的 ` **_interestRate**` 搬到 ` AccountType` 类去。目前已有数个函数引用了它,`interestForAmount_days` 就是其中之一。下一步要在 `AccountType ` 中建立 `**_interestRate**` 字段以及相应的访问函数。
```java
public class AccountType {
    private double _interestRate;

    public double get_interestRate() {
        return _interestRate;
    }

    public void set_interestRate(double _interestRate) {
        this._interestRate = _interestRate;
    }
}


现在,我们需要让Account类中访问 **_interestRate** 字段的函数转移而使用 AccountType对象。然后删除Account类中的 **_interestRate**

public class Account {
    private AccountType _type;

    double interestForAmount_days(double amount, int days) {
        return _type.get_interestRate() * amount * days / 365;
    }
}

范例:使用Self-Encapsulation

如果有很多函数已经使用了 **_interestRate** 字段,我们该先运用 SelfEncapsulateField (自我封装)

public class Account {
    private AccountType _type;
    private double _interestRate;

    double interestForAmount_days(double amount, int days) {
        return getTnterestRate() * amount * days / 365;
    }
    public void set_interestRate(double arg) {
        this._interestRate = arg;
    }
    public double getTnterestRate() {
        return _interestRate;
    }
}
这样,搬移字段后,我们就只需要修改访问函数
public class Account {
    private AccountType _type;

    double interestForAmount_days(double amount, int days) {
        return getTnterestRate() * amount * days / 365;
    }
    public void set_interestRate(double arg) {
      _type.set_interestRate(arg);
    }
    public double getTnterestRate() {
        return _type.get_interestRate();
    }
}

7.3 Extract Class 提炼类

某个类做了应该由两个类做的事。建立一个新类,将相关的字段和函数从旧类搬移到新类。


动机

一个类应该是一个清楚的抽象。处理一个明确的责任。但是在实际工作中,类会不断成长扩展。你会在这儿加入一些功能,在那儿加入一些数据。给某个类添加一项新责任时,你会觉得不值得为这项责任分离出一个单独的类。于是,随着责任不断增加,这类变得过分复杂。很快,你的类就变成一团乱麻。
这样的类往往含有大量函数和数据。这样的类往往大二不易理解。此时你需要考虑那些部分可以分离出去。并将他们分离到一个单独的类中。如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示你应该将它们分离出去。
另一个往往在开发后期出现的型号是类的子类化方式,如果你发现出现了子类化只影响类的部分特性,或如果你发现某些特性需要以一种方式来子类化,某些特性需要以另一种方式子类化,这就意味着你需要分解原来的类。


做法

  • 决定如何分写类所负的责任
  • 建立一个新类,用以表现从旧类中分离出来的责任。
    • 如果旧类剩下的责任与旧类名称不符,为旧类更名。
  • 建立“从旧类访问新类”的连接关系
    • 有可能需要一个双向连接。但是在真正需要它之前,不要建立“从新类通往旧类”的连接
  • 对于你想搬移的每一个字段,运用Move Field搬移
  • 每次搬移 编译 测试
  • 使用Move Method 将必要函数搬移到新类,先搬移较低层次函数,在搬移较高层函数。
  • 每次搬移之后,编译测试
  • 检查,精简每个类的接口
    • 如果建立起双向连接,检查是否可以将它该为单向连接
  • 决定是否公开新类。如果你的确需要公开它,就要决定让它成为引用对象还是不可变的值对象。

### 范例 从一个简单的Person开始。 ```java public class Person { public String get_name() { return _name; } public String getTelephoneNumber(){ return ("("+_officeAreaCode+")"+_officeNumber); } public void set_officeAreaCode(String _officeAreaCode) { this._officeAreaCode = _officeAreaCode; } public String get_officeNumber() { return _officeNumber; } public void set_officeNumber(String _officeNumber) { this._officeNumber = _officeNumber; } private String _name; private String _officeAreaCode; private String _officeNumber; }
	这个例子中,我们可以将与电话号码相关的行为分离到一个独立类中。<br />先建立从Phone到TelephoneNumber的连接
```java
 class Person  
    private TelephoneNumber _officeTelepone=new TelephoneNumber();
先移动一个字段
public class TelephoneNumber {
    public String getAreaCode() {
        return _areaCode;
    }
    public void setAreaCode(String _areaCode) {
        this._areaCode = _areaCode;
    }
    private String _areaCode;
}

public class Person {
    private TelephoneNumber _officeTelepone = new TelephoneNumber();

    public String get_name() {
        return _name;
    }

    public String getTelephoneNumber() {
        return ("(" + getOfficeAreaCode() + ")" + _officeNumber);
    }
    String getOfficeAreaCode() {
        return _officeTelepone.getAreaCode();
    }
    public String get_officeNumber() {
        return _officeNumber;
    }
    public void set_officeNumber(String _officeNumber) {
        this._officeNumber = _officeNumber;
    }

    private String _name;
    private String _officeNumber;
}

然后移动其他字段
public class Person {

    public String get_name() {
        return _name;
    }

    public String getTelephoneNumber() {
        return _officeTelepone.getTelephoneNumber();
    }

    TelephoneNumber get_officeTelepone() {
        return _officeTelepone;
    }

    private TelephoneNumber _officeTelepone = new TelephoneNumber();
    private String _name;
}
public class TelephoneNumber {
    public String getTelephoneNumber() {
        return ("(" + _areaCode + ")" + _number);
    }
    public String getAreaCode() {
        return _areaCode;
    }
    public void setAreaCode(String _areaCode) {
        this._areaCode = _areaCode;
    }
    public void set_number(String _number) {
        this._number = _number;
    }

    public String get_number() {
        return _number;
    }

    private String _areaCode;
    private String _number;
}

7.4 Inline Class(将类内联化)

某个类没有做太多的事情。将这个类的所有特性搬移到另外一个类中,然后移除原类。


7.5 Hide Delegate(隐藏“委托关系”)

![重构.png](https://img-blog.csdnimg.cn/img_convert/077a6326aa3ffb63975dcfe071166858.png#clientId=ud9b6b387-1960-4&from=ui&id=ucb96630d&margin=[object Object]&name=重构.png&originHeight=322&originWidth=621&originalType=binary&ratio=1&size=18304&status=done&style=stroke&taskId=u99853e34-0fbe-4401-a292-7cd241b7514)
客户端通过一个委托函数来调用另一个对象。在服务类上建立客户所需要的所有函数,用以隐藏委托关系。

动机

如果某个客户先通过服务对象得到另一个对象,然后调用后者的函数。那么客户就必须知晓这一层委托关系。万一委托关系发生变化,客户也得相应变化。可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而除去这种依赖。这么一来,即便将来委托关系发生变化,变化也将被限制在服务对象内,不会涉及客户。


做法

  • 对于每一个委托关系中的函数,在服务对象建立一个简单的委托函数。
  • 调整客户,令它只调用服务对象提供的函数。
  • 每次调整后,编译测试。
  • 如果将来不再有任何客需要取用委托类。便可移除服务对象中的相关访问函数。

范例

public class Person {
    Department _department;

    public Department getDepartment() {
        return _department;
    }

    public void set_department(Department _department) {
        this._department = _department;
    }

}

class Department {
    private String _chargeCode;
    private Person _manger;

    public Person get_manger() {
        return _manger;
    }

    public Department(Person manger) {
        this._manger = manger;
    }
}

如果客户希望知道某人的经历是谁,他必须先取得Department对象。

manager = john.getDepartment().get_manger();
这样的编码就是对客户揭露了Department的工作原理,于是客户知道:Department用以追踪“经理”这条信息。如果对客户隐藏Department,可以减少耦合,为了这一目的,在Person中建立一个简单的委托函数。
Person getManager() {
    return _department.get_manger();
}

7.6 Remove Middle Man(移除中间人)

某个类做了过多的简单委托动作,让客户直接调用受委托。


7.7 Introduce Foreign Method(引入外加函数)

需要为提供服务的类增加一个函数,但无法修改这个类。在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。

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);
}

动机

外加函数只是权宜之计。如果有可能,仍然应该将这些函数搬移到它们的理想家园。


7.8 Introduce Local Extension(引入本地扩展)

需要为服务类提供一些额外函数,但你无法修改这个类。新建一个类,使它包含额外函数。让这个扩展品成为源类的子类或包装类。


动机

如果需要的额外函数超过2个,外加函数就很难控制它们了。所以,需要将这些函数组织在一起,放到一个恰当的地方去。要达到这一目的,两种标准对象技术——子类化(subclassing)和包装(wrapping)。这种情况下,我们把子类或包装类统称为本地扩展。

使用本地扩展使你得以坚持“函数和数据应该被同一封装”的原则。如果你一直把该放在扩展类中的代码零散地放置于其他类中,最终只会让其他这些类变得过分复杂,并使得其中函数难以被复用。
在子类和包装类之间做选择时,我们通常选择子类,因为这样的工作量比较少。制作子类的最大障碍在于,它必须在对象创建期实施。如果我们可以接管对象创建过程,那当然没问题;但如果你想在你对象创建之后再使用本地扩展。就有问题了。此外,子类化方案还必须产生一个子类对象,这种情况下,如果有其他对象引用了旧对象,我们就同时有两个对象保存了原数据!如果原数据是不可修改的,那也没问题,可以放心复制;但如果原数据是运行被修改,问题就来了,因为一个修改动作无法同时改变两份副本。这时候我们就必须使用包装类。使用包装类时,对本地扩展的修改会波及原对象。

做法

  • 建立一个扩展类,将它作为原始类的子类或包装类。
  • 在扩展类中加入转型构造函数。
    • 所谓转型构造函数是指“接受原对象作为参数”的构造函数。如果采用了子类化方案,那么转型构造函数应该调用适当的超类构造函数;如果采用包装类方案,那么转型构造函数应该将它得到的传入参数以实例变量的形式包装起来,用作接受委托的原对象。
  • 在扩展类中加入新特性
  • 根据需要,将原对象替换为扩展对象。
  • 将针对原始类定义的所有外加函数搬移到扩展类中。

范例

使用JavaDate类为例。在此类上提供我们想要的功能,。
第一件待决定的就是:使用子类还是包装类。
子类显而易见的办法

class MfDateSub extend Date{
    public MfDateSub nextDay(){public int dayOfYear(){...
        
        

包装类则需要向上委托

class MfDateWrap{
    private Date _original;
    

范例:使用子类

先建立MfDateSub,使其称为Date的子类;
构造函数需要委托给Date构造函数

public class MfDateSub extends Date {
    public MfDateSub(String dateString) {
        super(dateString);
    }

    public MfDateSub(Date date) {
        super(date.getTime());
    }
    private static Date nextDay(Date arg){
        return new Date(arg.getYear(),arg.getMonth(),arg.getDate()+1);
    }
}

范例:使用包装类

首先声明一个包装类

public class MfDateWrap {
    private Date _original;

    public MfDateWrap(String dateString) {
        _original = new Date(dateString);
    }

    public MfDateWrap (Date arg){
        _original=arg;
    }
    public int getYear(){
        return _original.getYear();
    }

}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值