重构(Martin Fowler)——在对象之间搬移特性

核心内容源于《重构 改善既有代码的设计》(Refactoring Improving the Design of Existing Code——Martin Fowler著)。

初衷

Martin Fowler设计本章的目的是为了称述,把责任放置在合适的地方

常常使用Move MethodMove Field简单地移动对象行为,就可以简单解决这些问题。如果这两个重构方法都需要用到,那么首先使用Move Field,再使用Move Method

一个类往往会因为承担过多的责任而变得臃肿不堪。这种情况下,使用Extract Class将一部分责任分离出去,如果一个类变得责任较少,那么使用Inline Class将它融入另一个类中。

如果一个类使用了另一个类,运用Hide Delegate将这种关系隐藏起来通常是有帮助的。

有些时候隐藏委托会导致拥有者的接口经常变化,此时需要使用Remove Middle Man

 

本章最后两项重构,Introduce Foreign Method 和 Introduce Local Extension比较特殊,只有当不能访问某个类的源码,却又想把责任移动进这个不可修改的类时,建议使用这种方法。

                                                                                                                                                                                                                                                                                                                                                                                                    目录

初衷

1.1Move Method(搬移函数)

动机

做法

1.2Move Field(搬移字段)

动机

做法: 

1.3Extract Class(提炼类)

动机

做法

1.4Inline Class(将类内联化)

动机

做法

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

动机

做法

1.6Remove Middle Man(移除中间人)

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

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


1.1Move Method(搬移函数)

程序中,有个函数(aMethod)与其所驻类(Class 1)之外的另一个类(Class 2)进行更多交流:调用或者,或者被后者调用。

动机

“搬移函数”是重构理论的支柱。如果一个类有太多行为,如果一个类与另一个类有太多合作而形成高度耦合,请使用Move Method方法。

遵守单一职责原则

如果出现下面这种函数,需要注意:

使用另一个对象的次数比使用自己所驻对象的次数还多。

之后判断,这个函数和哪儿个对象的交流比较多,之后决定其移动路径。

做法

 现在假设有一个Account的类:

class Account{
public:
    double overdraftCharge(){
        if(_type.isPremium()){
            double result = 10;
            if(_daysOverdrawn > 7){
                result += (_daysOverdrawn -7) * 0.85;
            }
            return result;
        }
        else {
            return _daysOverdraw * 1.75;
        }
    }
    double bankCharge(){
        double result = 4.5;
        if(_daysOverdrawn > 0){
            result += overdraftCharge();
            return result;
        }
    }

    bool isPremium();
private:
    Account _type;
    int _dayOverdrawn;
};

假设有几种新账户,每一种都有自己的“透支金额计费规则”,现在要把Account 中的overdraftCharge()搬移到AccountType类中。

①观察overdraftCharge()使用的每一项特性,考虑是否和overdraftCharge()一起移动。

此例中,我们需要将_daysOverdrawn字段留在Account类中,因为这个值会随着不同账户而变化。

现在将overdraftCharge()移动代AccountType类中。

注意,因为在使用了Account的两个字段(成员变量),_type_dayOverdrawn。要想办法建立联系,且把_daysOverdrawn字段留在Account类中。

_daysOverdrawn按值传递给新函数。

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

以上使用字段_dayOverdrawn显然有问题, 毕竟成员变量属于对象,此处调用的_dayOverdrawn是否是我们想要的,不得而知。

①建立一个函数去获得_dayOverdrawn的数值。

class AccountType{
public:
    double overdraftCharge(Account account){
        if(account.isPremium()){
            double result = 10;
            if(account.getDaysOverDrawn() > 7){
                result += (account.getDaysOverDrawn() -7) * 0.85;
            }
            return result;
        }
        else {
            return account.getDaysOverDrawn() * 1.75;
        }
    }
};

②直接将 _dayOverdrawn声明为静态成员变量。

现在将源函数的函数本体替换为一个简单的委托动作,测试完成后,直接在被调用的地方进行替换,完全替换结果如下所示。

class Account{
public:
    double bankCharge(){
        double result = 4.5;
        if(_daysOverdrawn > 0){
            Account account;
            result += _type.overdraftCharge(account);
            return result;
        }
    }
    bool isPremium();
private:
    AccountType _type;
    int _daysOverdrawn;
};

 

1.2Move Field(搬移字段)

动机

在类之间移动状态和行为,是重构过程中必不可少的措施。

对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,就需要考虑搬移这个字段。

上述所谓的使用,可能是通过设置/取值函数间接进行的。

做法: 

class Account{
private:
    AccountType _type;
    double _interestRate;
public:
    double interestForAmount_days(double amount,int days){
        return _interestRate * amount * days/365;
    }
};

现在要把利率_interestRate搬运动AccountType类中。但是Account类中有数个函数已经使用了该字段,那么我们需要在AccountType中建立_interestRate字段以及相应的访问函数

class AccountType{
public:
    void setInterestRate(double arg){
        _interestRate = arg;
    }
    double getInteresrRate(){
        return _interestRate;
    }
private:
    double _interestRate;
};

那么Account类就会变为:

class Account{
private:
    AccountType _type;
    double _interestRate;
public:
    double interestForAmount_days(double amount,int days){
        return _type.getInteresrRate() * amount * days/365;
    }
};

1.3Extract Class(提炼类)

某个类做了过多的内容,不符合“单一职责原则”,责任冗余,需要进一步去分清楚责任。

建立一个新类,将相关的字段和函数从旧类中搬移到新类中。

动机

一个类应该是一个清楚的抽象,处理一些明确的责任。

当一个类责任冗余时,我们需要对其进行重构,重新划分责任。

做法

我们从Person这个类入手

class Person{
public:
    string getName(){
        return this->_name;
    } 
    string getTelephoneNumber(){
        return ( "(" + this->_officeAreaCode + ")"+ this->_officeNumber);
    }
    string getOfficeAreaCode(){
        return _officeAreaCode;
    }
    void setofficeAreaCode(string arg){
        this->_officeAreaCode = arg;
    }
    string getOfficeNumber(){
        return _officeNumber;
    }
    void setOfficeNumber(string arg){
        this->_officeNumber = arg;
    }
private:
    string _name;
    string _officeAreaCode;
    string _officeNumber;
}

这个类中,我可以将与电话号码有关的行为单独分离到一个独立的类中。首先我们定义一个TelephoneNumber类来表示“电话号码”这个概念。

class TelephoneNumber{
    
};

现在要建立从PersonTelephoneNumber的连接。

现在先移动一个字段_areaCode

class TelephoneNumber{
public:
    string getAreaCode(){
        return _areaCode;
    }
    string setAreaCode(string arg){
        this->_areaCode = arg;
    }
private:
    string _areaCode;
};

Person类中进行相应的改变: 

class Person{
public:
 
    
    string getTelephoneNumber(){
        return ( "(" + getOfficeAreaCode() + ")"+ this->_officeNumber);
    }
    string getOfficeAreaCode(){
        return _officeTelephone.getAreaCode();
    }
    void setofficeAreaCode(string arg){
        _officeTelephone.setAreaCode(arg)
    }

private:
    
    TelephoneNumber _officeTelephone = new TelephoneNumber();
    
};

下面继续移动其他字段和函数

class TelephoneNumber{
public:
    string getTelephoneNumber(){
        return ("(" + _areaCode + ")"+ _number);
    }
    string getAreaCode(){
        return _areaCode;
    }
    string setAreaCode(string arg){
        this->_areaCode = arg;
    }
    string getNumber(){
        return _number;
    }
    void setNumber(string arg){
        this->_number = arg;
    }
private:
    string _areaCode;
    string _number;
};

Person类如下:

class Person{
public:
    string getName(){
        return this->_name;
    } 
    string getTelephoneNumber(){
        return _officeTelephone.getTelephoneNumber();
    }
    TelephoneNumber getOfficephone(){
        return _officeTelephone;
    }

private:
    string _name;
    TelephoneNumber _officeTelephone = new TelephoneNumber();
    
};

函数:

    TelephoneNumber getOfficephone(){
        return _officeTelephone;
    }

意义非凡!神来之笔! 

使用Person类就可以调用TelephoneNumber类中的内容,比如:

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

1.4Inline Class(将类内联化)

某个类没有做太多事情

将这个类的所有特性搬移到另一个类中,然后移除原类

动机

Inline Class正好与Extract Class相反。如果一个类不再承担足够责任,不再有单独存在的理由,缩减不必要的类。

做法

class Person{
public:
    string getName(){
        return this->_name;
    } 
    string getTelephoneNumber(){
        return _officeTelephone.getTelephoneNumber();
    }
    TelephoneNumber getOfficephone(){
        return _officeTelephone;
    }

private:
    string _name;
    TelephoneNumber _officeTelephone = new TelephoneNumber();
    
};
class TelephoneNumber{
public:
    string getTelephoneNumber(){
        return ("(" + _areaCode + ")"+ _number);
    }
    string getAreaCode(){
        return _areaCode;
    }
    string setAreaCode(string arg){
        this->_areaCode = arg;
    }
    string getNumber(){
        return _number;
    }
    void setNumber(string arg){
        this->_number = arg;
    }
private:
    string _areaCode;
    string _number;
};

先将TelephoneNumber中所有“可见”(public)函数:

class Person{
public:
    string getName(){
        return this->_name;
    } 
    string getTelephoneNumber(){
        return _officeTelephone.getTelephoneNumber();
    }
    TelephoneNumber getOfficephone(){
        return _officeTelephone;
    }
    
    string getAreaCode(){
        return _officeTelephone.getAreaCode();
    }
    string setAreaCode(string arg){
        _officeTelephone.setAreaCode(arg);
    }
    string getNumber(){
        return _officeTelephone.getNumber();
    }
    void setNumber(string arg){
        _officeTelephone.setNumber(arg);
    }

private:
    string _name;
    TelephoneNumber _officeTelephone = new TelephoneNumber();
    
};

找出所有的TelephoneNumber的所有用户

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

变成了:

    Person martin = new Person();
    martin.setAreaCode("781");

之后不断使用1.1和1.2的方法,消减多余的类。

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

客户通过一个委托来调用另一个对象

在服务类上建立客户所需的所有函数,用以隐藏委托关系

动机

封装是对象的关键特征之一,封装意味着每个对象都应该尽可能减少了解系统的其他部分。如此,一旦发生变化,需要了解这一变化的对象就会比较少,这会使变化比较容易进行。

除去Private,我们会发现,还有更多值得封装的东西。

如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一委托关系发生变化,客户也得相应变化。

万一委托关系发生变化,客户也要进行变化,不易于抵御变化。

做法

class Person{
    Department _department;
public:

    Department getDepartment(){
        return _department;
    }
    void setDepartment(Department){
        _department = arg;
    }
};
class Department{
public:
    Department(Person manager){
        _manager = manager;
    }
    Person getManager(){
        return _manager;
    }
private:
    string _chargeCode;
    Person _manager;
};

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

 manager = john.getDepartmen().getManager();

如此,编码对客户揭露了Department的工作原理,于是客户知道:Department用以追踪“经理”这条信息。

如果对客户隐藏Department,可以减少耦合。

为了这一个目的,我在Person中建立一个简单的委托函数

class Person{
    Department _department;
public:
    //....
    Person getManager(){
        return _department.getManager();
    }
    //....
};

现在,我得修改Person的所有客户,让它们改用新函数:

manager = john.getManager();

 

1.6Remove Middle Man(移除中间人)

与1.5的内容正好对应,某个类做了太多的委托,让客户直接调用委托类。

1.6与1.5的内容交相辉映,二者相互组合使用,不断调整,让代码更加的舒服和自然。

具体过程和1.5刚好相反,现在我们有PersonDepartment 两个部分

class Person{
    Department _department;
public:

    Person getManager(){
        return _department.getManager();
    }
};
class Department{
public:
    Department(Person manager){
        _manager = manager;
    }
private:
    Person _manager;
};

如果要移除中间人,我们在Person中建立一个函数获得委托对象:

class Person{
    Department _department;
public:

    Department getDepartment(){
        return _department;
    }

};

然后就逐一处理委托函数,转换为如下形式:

manager = john.getDepartmen().getManager();

 

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

你需要为提供服务的类增加一个函数,但你无法修改这个类

在客户类中建立一个函数,并以第一参数形式传入一个服务类实例

做法:

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

将上述内容改为:

Date newStart = nextDay(previousEnd);

Date nextDay(Date arg){
    return new Date(previousEnd.getYear(),previousEnd.getMonth(),previousEnd.getDate() + 1);
}

 

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

你需要为服务类提供一些额外函数,但是你无法修改这个类。

建立一个新类,使它包含这些额外函数,让这个扩展品成为源类的子类或者包装类(多次出现1.7的情况时,需要将扩展函数放入一个类中)

如果只是增加一个函数,那么使用1.7,如果增加的函数超过两个,就很难办了。

你需要将这些外加函数放到一个合适的地方

两种标准对象技术——子类化(subclassing)和包装(wrapping)是显而易见的办法。

本地扩展是一个独立的类,也是被扩展类的子类型:它提供源类的一切特性,同时额外添加新特性。

子类化:

class Date{
public:
    int getYear();
    int getMonth();
    int getDate();
};

class MfDateSub:public Date
{
public:
    //构造函数需要委托给Date的构造函数

    //使用Move Method将内容移动到类中
    Date nextDay(){
        return new Date(getYear(),getMonth(),getDate() + 1);
    }
};

使用包装类:

class MfDateWrap{
public:
    MfDateWrap(string dateString){
        _original = new Date(dateString);
    }
    MfDateWrap(Date arg){
        this->_original = arg;
    }
    //将扩展函数中需要使用到的Date的函数内容全部进行一次委托
    int getYear(){
        return _original.getYear();
    }
    int getMonth(){
        return _original.getMonth();
    }
    int getDate(){
        return _original.getDate();
    }
    //将使用Move Method将日期相关的内容全部进行移动
    Date nextDay(){
        return new Date(getYear(),getMonth(),getDate() + 1);
    }
private:
    Date _original;

};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值