重构(Martin Fowler)——简化函数调用

在对象技术中,最重要的概念莫过于“接口”(interface)。

容易被理解和被使用的接口,是开发良好面向对象软件的关键。

 

最简单也最重要的一件事就是修改函数名称。名称是程序写作者与阅读者交流的关键工具。只要你理解一段程序的功能,应该大胆使用Rename Method将你所知道的东西传给其他人。

函数参数在接口之中扮演十分重要的角色。Add Parameter Remove Parameter 都是很常见的重构手法。可以对参数列进行增删。

如果同一个对象的多个值被当作参数传递,你可以运用Preserve Whole Object将它们替换为单一对象,从而缩短参数列。

如果并不存在这么一个对象,你可以运用Introduce Parameter Object将它创建出来。

如果函数参数来自该函数可获取的一个对象,则可以使用Replace Parameter with Method避免传递参数。

如果某些参数被用来在条件表达式中做选择依据,可以实施Replace Parameter with Explicit Method

还可以使用Parameterize Method为数个相似函数添加参数,将它们合并到一起。

良好的接口只向用户展现必要的东西,如果一个接口暴露过多细节,你可以将不必要暴露的东西隐藏起来,从而改进接口的质量。Hide MethodRemove Setting Method 将它们隐藏起来

有时候可以使用工厂函数 Factory Method 创建对象,而不是构造函数。

 

目录

1.1Rename method(函数改名)

1.2Add Parameter(添加函数)

1.3Remove Parameter(移除参数)

1.4Separate Query from Modifier(将查询函数和修改函数分离)

1.5Parameterize Method(令函数携带参数)

1.6Replace Parameter with Explicit Methods(以明确函数取代参数)

1.7Preserve Whole Object(保持对象完整)

1.8Preserve Parameter with Methods(以函数取代参数)

1.9Introduce Parameter Object(保持对象完整)

1.10Remove Setting Method(移除设值函数)

1.11Hide Method(隐藏函数)

1.12Replace Constructor with Factory Method(以工厂函数取代构造函数)

1.13Encapsulate Downcast(封装向下转型)

1.14Replace Error Code with Exception(以异常取代错误码)

1.15Replace Exception with Test(以测试取代异常)


1.1Rename method(函数改名)

函数的名称未能揭示函数的用途

动机

MF非常提倡:将复杂的处理过程分解成小函数。

但是如果没有一个好的函数命名,将会事倍功半。

1.2Add Parameter(添加函数)

某个函数需要从调用端得到更多信息

为此函数添加一个对象参数,让该对象带进函数所需信息

动机

Add Parameter是一个很常见的重构手法。

现在你需要修改一个函数,而修改后的函数需要一些过去没有的信息,因此你需要给该函数添加一个参数。

1.3Remove Parameter(移除参数)

函数本体不再需要某个参数

将该参数去除

动机

但出现多余的参数时,还是需要及时去掉的,毕竟函数的调用者需要知道每一个参数都代表这什么意义

万万不可有:以后说不定还会用到就暂且留着吧的心态面对多余的参数

1.4Separate Query from Modifier(将查询函数和修改函数分离)

某个函数既返回对象状态值,又修改对象状态

建立两个不同的函数,其中一个负责查询;另一个负责修改

做法

string foundMiscreat(string people){
    for(int i = 0;i<people.length();++i){
        if(people[i] = 'D'){
            sendAlert();
            return 'D';
        }
        if(people[i] = 'J'){
            sendAlert();
            return 'J';
        }
    }
    return "";
}

被下面函数代码调用:

void checkSecurity(string people){
    string found = foundMiscreat(peolpe);
    someLaterCode(found);
}

下面将查询和修改分开,建立查询函数

string foundPerson(string people){
    for(int i = 0;i<people.length();++i){
        if(people[i] = 'D'){
            return 'D';
        }
        if(people[i] = 'J'){
            return 'J';
        }
    }
    return "";
}

之后更改修改函数 和代码 调用 情况

void foundMiscreat(string people){
    for(int i = 0;i<people.length();++i){
        if(people[i] = 'D'){
            sendAlert();
            return;
        }
        if(people[i] = 'J'){
            sendAlert();
            return;
        }
    }
}
void checkSecurity(string people){
    foundMiscreat(peolpe);
    string found = foundPerson(peolpe);
    someLaterCode(found);
}

下面对Rename Method 和 foundMiscreat() 进行Subsititete Algorithm(因为查询函数和修改函数内容重合度过高)

void sendAlert(string people){
    if(foundPerson() != ""){
        sendAlert();
    }
}

 

1.5Parameterize Method(令函数携带参数)

若干函数做了类似的工作,但在函数本体中却包含了不同的值

建立单一函数,以参数表达那些不同的值

关键问题:可以将少量数值视为参数

动机

你可能会发现这样的两个函数:它们做着类似的工作,但因少数几个值致使行为略有不同。

在这种情况下,你可以将各自分离的函数统一起来,并通过参数来处理那些变化情况,用以简化问题。

class Employee{
public:
    void tenPercentRaise(){
        salary *= 1.1;
    }
    void fivePercentRaise(){
        salary *= 1.05;
    }
};

以上代码可以替换为:

class Employee{
public:
    void raise(double factor){
        salary *= (1 + factor);
    }
};

 eg2:

Dollars baseCharge(){
    double result = min(lastUsage(),100) * 0.03;
    if(lastUsage() > 100){
        result += (min(lastUsage()),200 - 100) * 0.05;
    }
    if(lastUsage() > 200){
        result += (lastUsage() - 200) * 0.07;
    }
    return new Dollars(result);
}

将其进行替换:

Dollars baseCharge(){
    double result = usageInRange(0,100) * 0.03;
    result += usageInRange(100,200) * 0.05;
    result += usageInRange(200,Integer.MAX_Value) * 0.07;
    return new Dollars(result);
}
int usageInRange(int start,int end){
    if(lastUsage() > start){
        return min(lastUsage(),end) - start;
    }
    else{
        return 0;
    }
}

 

1.6Replace Parameter with Explicit Methods(以明确函数取代参数)

你有一个函数,其中完全取决于参数值而采取不同行为

针对该参数的每一个可能值,建立一个独立函数

void setValue(string name,int value){
    if(name == "height"){
        _height = value;
        return;
    }
    if(name == "width"){
        _width = value;
        return;
    }
}

重构为:

void setHeight(int arg){
    _height = arg;
}

void setWidth(){
    _width = arg;
}

动机

与 Parameterize Method 正好相反,如果某个参数有多种可能的值,而函数内又一以条件表达式检查这些参数值,并根据不同参数值做出不同的行为,那么就应该使用本项重构

使用本重构来替换掉 switch 语句

Employee * Employee::create(int type){
    switch(type){
    case occupation::getEngineer():
            return new Engineer();
        break;
    case occupation::getSalesman():
            return new Salesman();
        break;
    case occupation::...:
            return new Manager();
        break;
    default:
        return nullptr;
    }
}

下面我们对上述代码进行重构:

首先创建构造函数:

    static Employee createEngineer(){
        return new Engineer();
    }
    static Employee createSalesman(){
        return new Salesman();
    }
    static Employee createManager(){
        return new Manager();
    }

之后,在switch语句中进行替换,并测试:

    Employee * Employee::create(int type){
        switch(type){
        case occupation::getEngineer():
                return new Employee::createEngineer();
            break;
        case occupation::getSalesman():
                return new Employee::createSalesman();
            break;
        case occupation::...
                return new Employee::createManager();
            break;
        default:
            return nullptr;
        }
    }

之后,更改旧函数的调用端:

    Employee kent = new Employee.create(ENGINEER);
    Employee kent = new Employee.createEngineer();

之后,直接删除create工厂函数,和常量值。

1.7Preserve Whole Object(保持对象完整)

你从某个对象中取出若干值,将它们作为某一次函数调用时的参数

改为传递整个对象

    int low = daysTempRange().getLow();
    int high = daysTempRange().getHigh();
    withinPlan = plan.withinRange(low,high);

重构为:

    withinPlan = plan.wothinRange(daysTempRange());

动机

优点1:

有时候,你会将来自同一对象的若干项数据作为参数,传递给某个函数,这样做的问题在于:万一将来被调用函数需要新的数据项,你就必须查询并修改对此函数的所有调用。

如果你把这些数据所属的整个对象传给函数,可以避免这种尴尬的处境,因为被调用函数可以向那个参数对象请求任何它想要的信息。这就是单纯传递值的劣势,也是传递参数的优势,可以直接使用。

优点2:

过长的参数列很难使用,需要调用者和被调用者必须记住这些参数的用途。传递对象可以提高代码的可读性。

优点3:

此外,不使用完整对象也会造成重复代码,因为被调用函数无法利用完整对象中的函数来计算中间值。

优点4:

如果传递数值,那么函数只是依赖数值,如果传递对象,那么函数只是依赖对象。依赖对象会让你的依赖结构恶化,那么就不应该使用 Preserve Whole Object(保持对象完整)

 

如果被调用函数使用了来自另一个对象的很多项数据,可能意味该函数实际上应该被定义在别处。

做法

以一个Room表示房间,负责记录房间一天中的最高温度和最低温度,然后这个对象需要将实际的温度范围与预先规定的温度控制计划相互比较,告诉顾客当天温度是否符合计划需求。

class Room{
public:
    bool withinPlan(HeatingPlan plan){
        int low = daysTempRange().getLow();
        int high = daysTempRange().getHigh();
        return plan.withRange(low,high);
    }
};
class HeatingPlan{
public:
    bool withinPlan(int low,int high){
        return (low >= _range.getLow() && high <= _range.getHigh() );
    }
};
class TempRange{
public:
    TempRange * daysTempRange();
    int getLow();
    int getHigh();
};

 其实不必将daysTempRange对象的信息拆开传递,只需将整个对象传递给withinPlan函数即可。

class Room{
public:
    bool withinPlan(HeatingPlan plan){
        return plan.withRange(daysTempRange());
    }
};
class HeatingPlan{
public:
    bool withinPlan(TempRange * arg){
        return (arg->getLow() >= this->getLow() && arg->getHigh() <= this->getHigh() );
    }
    
};

之后,可以将某个函数移到TempRange里面

class HeatingPlan{
public:
    bool withinPlan(TempRange * arg){
        return (_range->includes(arg));
    }
private:
    TempRange * _range;
};

class TempRange{
public:
    bool includes(TempRange * arg){
        return (arg->getLow() >= this->getLow() && arg->getHigh() <= this->getHigh() );
    }
};

下面这段要注意:

arg->getLow() >= this->getLow()

 是参数所属的字段和HeatingPlan_range对象所属的字段之间进行对比。

1.8Preserve Parameter with Methods(以函数取代参数)

对象调用某个函数,并将所得结果作为参数,传递给另一个函数

而接受该参数的函数本身也能够调用前一个函数

让参数接受者去除该项参数,并直接调用前一个函数

    int basePrie = _quantity * _itemPrice;
    discountLevel = getDiscountLevel();
    double fianlPrice = discountedPrice(basePrice,discountLevel);

重构为:

    int basePrice = _quantity * _itemPrice;
    double finalPrice = discountedPrice(basePrice);

动机

如果函数可以通过其他途径获得参数值,那么它就不应该通过参数取得该值。过长的参数列会增加程序阅读者的理解难度,因此我们应该尽可能缩短参数列的长度

缩短参数列的方法之一就是:看看参数接受端是否可以通过与调用端相同的计算来取得参数值

如果调用端通过其所属对象内部的另一个函数来计算参数,并在计算过程中未曾引用调用端的其他参数,那么你就应该可以将这个计算过程转移到被调用端内,从而去除该项参数。

 

但是如果参数值的计算过程依赖于调用端的某个参数,那么就无法去掉。

做法

一下代码用于计算订单折扣和价格

private:
    double discountedPrice(int basePrice,int discountLevel){
        if(discountLevel == 2){
           return basePrice * 0.1;
        }
        else {
            return basePrice * 0.05;
        }
    }
public:
    double getPrice(){
        int basePrice = _quantity * _itmPrice;
        int discountLevel;
        if(_quantity > 100) discountLevel = 2;
        else discountLevel = 1;
        double finalPrice  = discountedPrice(basePrice,discountLevel);
        return finalPrice;
    }

首先将折扣等级的计算单独提炼:

private:
    int getDiscountLevel(){
        if(_quantity > 100) {
            return 2;
        }
        else {
            return 1;
        }
    }
public:
    double getPrice(){
        int basePrice = _quantity * _itmPrice;
        int discountLevel = getDiscountLevel();
        double finalPrice  = discountedPrice(basePrice,discountLevel);
        return finalPrice;
    }

此时可见,discountLevel已经没有用处了

class discount{
private:
    double discountedPrice(int basePrice){
        if(getDiscountLevel() == 2){
           return basePrice * 0.1;
        }
        else {
            return basePrice * 0.05;
        }
    }
    int getDiscountLevel(){
        if(_quantity > 100) {
            return 2;
        }
        else {
            return 1;
        }
    }
public:
    double getPrice(){
        int basePrice = _quantity * _itmPrice;
        double finalPrice  = discountedPrice(basePrice);
        return finalPrice;
    }
};

下面对临时变量和basePrice参数进行剔除:

class discount{
private:
    double discountedPrice(){
        if(getDiscountLevel() == 2){
           return getBasePrice() * 0.1;
        }
        else {
            return getBasePrice() * 0.05;
        }
    }
    double getBasePrice(){
        return _quantity * _itmPrice;
    }
    int getDiscountLevel(){
        if(_quantity > 100) {
            return 2;
        }
        else {
            return 1;
        }
    }
public:
    double getPrice(){
        return discountPrice();
    }
};

 

1.9Introduce Parameter Object(保持对象完整)

某些参数总是很自认地同时出现

以一个对象取代这些参数

动机

 

做法

你常会看到特定的一组参数总是一起被传递。

可能有好几个函数都使用这一组参数,这些函数可能隶属同一个类,也可能隶属不同的类

这样一组参数就是所谓的 Data Clumps(数据泥团)

我们可以运用一个对象包装所有数据,再以该对象取代它们,哪怕是为了将这些数据放在一起,也是值得的。

下面以最常见表示范围的函数参数进行演示

做法

下面是一个“账目和账项”范例。

class Entry{
public:
    Entry(double value,Date chargeDate){
        _value = value;
        _chargeDate = chargeDate;
    }
    Date getDate(){
        return _chargeDate;
    }
    double getValue(){
        reutrn _value;
    }
private:
    Date _chargeDate;
    double _value;
};

我关注的焦点是用以表示“账目”的Account,它保存了一组Entry对象,并有一个函数用来计算两个日期间的帐项总量:

class Account{
public:
    double getFlowBetween(Date start,Date end){
        double result = 0;
        //...........
    }
private:
    
};

有太多的情况会使用到范围,比如日期范围,数值范围等等,下面声明一个简单的数据容器,用来表示范围:

class DateRange{
    DateRange(Date start,Date end){
        _start = start;
        _end = end;
    }
    Date getStart(){
        return _start;
    }
    Date getEnd(){
        return _end;
    }
private:
    Date _start;
    Date _end;
};

DateRange中的字段,是由构造函数进行赋值的,其余情况不提供修改方式。

之后尝试把DateRange对象加入函数中:

class Account{
public:
    double getFlowBetween(DateRange * arg){
        double result = 0;
        //可以使用接口去调用_start和_end两个字段
        arg->getStart();
        arg->getEnd();
    }
private:
    
};

 

1.10Remove Setting Method(移除设值函数)

类中某个字段应该在对象创建时被设置,然后就不再改变

去掉该字段的所有设置函数

动机

如果你为了某个字段提供了设置函数,这就暗示了这个字段值可以被改变

如果你不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数。

做法

class Account{
private:
    string _id;
public:
    Account(string id){
        setID(id);
    }
    void setId(string arg){
        _id = arg;
    }
    
};

重构为:

class Account{
private:
    string _id;
public:
    Account(string id){
        _id = id;
    } 
};

但是有时候情况会复杂,比如设置函数对参数要进行运算,可以增加一个初始化函数:

class Account{
private:
    string _id;
    void initializeId(string id){
        _id = "zz" + id;
    }
public:
    Account(string id){
        initializeId(id);
    } 
};

如果子类需要对超类private变量赋值,情况会比较复杂:

class InterestAccount :Account{
private:
    double _interestRate;
    InterestAccount(string id,double rate){
        setId(id);
        __interestRate = rate;
    }
};

现在问题是,我无法直接访问id,最好的办法,就是Rename method

class InterestAccount :Account{
private:
    double _interestRate;
    InterestAccount(string id,double rate){
        initializeId(id);
        __interestRate = rate;
    }
};

1.11Hide Method(隐藏函数)

有一个函数,从来没有被其实任何类用到

将这个函数修改为private

动机

重构往往促使你修改函数的可见度。

此时就需要把握是否要将函数转化为可见或者不可见

1.12Replace Constructor with Factory Method(以工厂函数取代构造函数)

你希望在创建对象时不仅仅是做简单的建构动作

将构造函数替换为工厂函数

Employee (int type){
    _type = type;
}

重构为:

static Employee create(int type){
    return new Employee(type);
}

动机

根据整数(实际是类型码)创建对象

还是拿员工薪资系统举例子

class Employee{
public:
    Employee(int type){
        _type = type;
    }
    static Employee * create(int _type);
private:
    int _type;
    static int ENGINEER;
    static int SALESMAN;
    static int MANAGER;

};

我希望为Employee提供不同的子类,并分别给予它们相应的类型码,因此,我们需要建立一个工厂函数

Employee * Employee::create(int type){
    return new Employee(type);
}

然后修改函数的调用点,将其更换为工厂函数:

Employee * test = Employee::create(Employee::ENGINEER);

之后将构造函数变成private:

private:
    int _type;
    Employee(int type){
        _type = type;
    }

根据字符串创建子类对象

如果使用 Replace Type Code with Subclasses 把类型码转化为Employee的子类,就可以运用工厂函数,将这些子类对用户隐藏起来,以此做到封装的效果。

class Employee{
public:
    Employee(){}
    static Employee * create(int _type);
    const static int ENGINEER;
    const static int SALESMAN;
    const static int MANAGER;
private:
    int _type;
    Employee(int type){
        _type = type;
    }
};
class Engineer : public Employee{
public:
    Engineer(){}
};
class Salesman : public Employee{
public:
    Salesman(){}
};
class Manager : public Employee{
public:
    Manager(){}
};
.h
const int Employee::ENGINEER = 0;
const int Employee::SALESMAN = 1;
const int Employee::MANAGER = 2;

工厂函数的具体部分:

Employee * Employee::create(int type){
    switch(type){
    case Employee::ENGINEER:
        return new Engineer();
    case Employee::SALESMAN:
        return new Salesman();
    case Employee::MANAGER:
        return new Manager();
    default:
        return nullptr;
    }
}

如果添加一个新的子类,那么也需要增加一个case

以明确函数创建子类

现在我们可以通过另外一种形式来隐藏子类

class  Person{
public:
    static Person createMale();
    static Person createFemale();
};
class Male : public Person{
public:
    Male(){}    
};
class Female : public Person{
public:
    Female(){}
};
Person * Person::createMale(){
    return new Male();
}
Person * Person::createFemale(){
    return new Female();
}
Person * kent = Person::createFemale();

 

1.13Encapsulate Downcast(封装向下转型)

某个函数返回的对象,需要由函数调用者执行向下转型(downcast)

将向下转型动作移到函数中

Object lastReading(){
    return reading.lastElement();
}

重构为:

Reading lastReading(){
    return (Reading) reading.lastElement();
}

某些情况需要对类型进行强制转换

 

1.14Replace Error Code with Exception(以异常取代错误码)

待更新

1.15Replace Exception with Test(以测试取代异常)

待更新

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值