代码重构的常用方法(C++实现)

代码重构的常用方法 (C++实现)

重构是在软件开发中改善已有代码的一种方法,通过代码重构可以改进软件的设计、使得软件更容易理解、有利于Bug的发现以及提高后续开发效率。Martin Fowler在《重构:改善既有代码的设计》一书中对“重构”提供了两种形式的定义:

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

对于何时重构,Martin Fowler认为不能为重构而重构,应该在**“当你想做别的事情,而重构可以帮助你把那些事情做好”**时,才去进行重构。并给出了四个常见的重构时机:(1) 事不过三,三则重构; (2) 添加功能时重构; (3) 修补错误时重构; (4) 复审代码时重构。

对于何时不应该重构,Martin Fowler认为如果“既有代码是在太混乱,重构它还不如重新写一个简单”时就应该重写,而不是重构。重写的一个清晰的信号是:现有代码根本不能正常运作

重构既是一件脑力活,也是一件体力活。特别是在软件中的“代码坏味道”积累到一定量时,就需要花费大量的时间和精力才能完成对代码的重构。因此,需要加强对“代码坏味道”的嗅觉,在发现“代码坏味道”时及时地去重构,通过不断的微重构来阻止代码架构的腐化,从而避免走上代码重写的艰难之路。

本文主要对《重构》一书中提到的一些常见的“代码坏味道”和对应重构手法的总结。

1 代码坏味道

1.1 Duplicated Code(重复代码)

现象
  1. 同一个类的两个函数含有相同的表达式。
  2. 两个互为兄弟的子类内含相同的表达式(可能其中对应的某些函数以相同顺序执行类似的操作,但在各个操作的细节上有所不同)。
重构手法
  1. Extract Method
  2. Extract Method->Pull Up Method (->Form Template Method)

1.2 Long Method(过长函数)

现象
  1. 一个函数所承担的事情太多,导致代码行数过长(可能有临时变量存在)。
重构手法
  1. Extract Method (->Replace Temp with Query)

1.3 Large Class(过大的类)

现象
  1. 一个类做了太多的事情,不符合单一职责原则,导致代码过多。
重构手法
  1. Extract Class

1.4 Long Parameter List(过长参数列)

现象

总体表现为一个函数的参数列表太长,可能伴随着以下两种现象:

  1. 向已有的对象发出一条请求就可以取代一个参数。
  2. 某些参数数据缺乏合理的对象归属。
重构手法
  1. Replace Parameter with Explicit Method
  2. Introduce Parameter Object

1.5 Divergent Change(发散式变化)

现象
  1. 某个类经常因为不同的原因在不同的方向上发生变化。
重构手法
  1. Extract Class

1.6 Shotgun Surgery(霰弹式修改)

现象
  1. 每遇到某种变化,都必须在许多不同的类内作出许多小修改。
重构手法
  1. Move Method->Move Field

1.7 Feature Envy(依恋情结)

现象
  1. 函数对某个类的兴趣高过对所驻类的兴趣。
  2. 函数中的一部分对某个类的兴趣高过对所驻类的兴趣。
重构手法
  1. Move Method
  2. Extract Method->Move Method

1.8 Data Clumps(数据泥团)

现象
  1. 多个类中有这相同的字段。
  2. 多个函数的签名中有着相同的字段。
重构手法
  1. Extract Class
  2. Extract Class->Introduce Parameter Object

1.9 Primitive Obsession(基本类型偏执)

现象

总体表现为以基本类型表示一些具有业务性质的概念,又分成两种情况:

  1. 基本类型都是独立的出现
  2. 几个基本都行总是一起出现(可能出现在函数参数列表中)
重构手法
  1. Replace Data Value with Object
  2. Extract Class (->Introduce Parameter Object)

1.10 Switch Statements(switch惊悚现身)

现象

代码中出现switch表达式,分以下两种场景

  1. 根据类型码来选择不同的行为。
  2. 只是单一函数中有些选择事例(可能选择条件之一是NULL)
重构手法
  1. Extract Method->Move Method->Replace Conditional with Polymorphism
  2. Replace Parameter with Explicit Method (->Introduce Null Object)

1.11 Parallel Inheritance Hierarchies(平行继承体系)

现象
  1. 每当为某个类增加一个子类,也必须为另一个类相应增加一个子类(Shotgun Surgery的特殊情况)
重构手法
  1. Move Method->Move Field

1.12 Lazy Class(冗赘类)

现象

某个类在重构后变得冗余,或者这个类是为未来变化服务的,当前并没有用。可能属于以下两种场景:

  1. 属于继承体系
  2. 属于组合体系
重构手法
  1. Collapse Hierarchy
  2. Inline Class

1.13 Speculative Generality(夸夸其谈未来性)

现象

某个类是为未来变化服务的,当前并没有用。可能属于以下两种场景:

  1. 属于继承体系
  2. 属于组合体系
重构手法
  1. Collapse Hierarchy
  2. Inline Class

1.14 Temporary Field(令人迷惑的临时字段)

现象
  1. 在一个类中,某个字段只在特定的情况下才有用,其余情况下容易令人迷惑。
重构手法
  1. Extract Class (将该字段以及使用到它的代码提取到一个新的类中,使得代码高内聚)

1.15 Message Chains(过度耦合的消息链)

现象
  1. 向一个对象请求另一个对象,然后在向后者请求另一个对象,…
重构手法
  1. Hide Delegate

1.16 Middle Man(中间人)

现象
  1. 某个类接口有一半的函数都委托给其他类。
重构手法
  1. Remove Middle Man

1.17 Inappropriate Intimacy(不适当的亲密关系)

现象
  1. 两个类过于亲密,花费太多时间去探究彼此的private成分。
重构手法

有以下两种重构思路:

  1. Move Method->Move Field
  2. Extract Class (将两者共同点提炼到一个新的类中)

1.18 Data Class(纯数据类)

现象
  1. 某些类拥有一些字段,以及对应的getter/setter函数,除此之外一无长处。这些类通常被其他类过分琐碎的操控者。

只有getter/setter函数,没有业务行为函数的类就是DDD中提到的贫血模型。在软件开发过程中,除非框架要求,应该杜绝贫血模型的出现。

重构手法
  1. Remove Setting Method(对那些不该被其他类修改的字段使用)->Move Method(把在其他类上的调用行为搬移到数据类中)

1.19 Comments(过多的注释)

现象
  1. 如果一段代码有着长长的注释,那么就会发现,这些注释之所以存在是因为代码很糟糕,注释本身并不是一种坏味道。
重构手法
  1. Extract Method

2 重新组织函数

2.1 Extract Method(提炼函数)

Extract Method应该是最常用的重构手法了,当遇到一个过长的代码或者需要添加注释才能让人理解其用途的代码时,就可以运用Extract Method将代码提炼到一个函数上。该方法的一个重点时函数命名,只有给函数起个适当的名字时,他们才能真正起作用。对于是否使用该重构手法,代码长度不是问题,关键在于函数名称和函数本体之间的语义距离。

如果提炼可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。

重构示例1
// 重构前
void PrintOwing(double amount)
{
    PrintBanner();

    // print details
    cout << "name: " << m_name << endl;
    cout << "amount: " << amount << endl;
}
// 重构后
void PrintOwing(double amount)
{
    PrintBanner();
    PrintDetails(amount);
}

void PrintDetails(double amount)
{
    cout << "name: " << m_name << endl;
    cout << "amount: " << amount << endl;
}

当重构的代码中带有临时变量时,可以将局部变量作为提炼函数的入参(参见重构示例1)。如果涉及到对临时变量的再赋值,可能还需要返回处理结果(参见重构示例2)

重构示例2
// 重构前
void PrintOwing(double amount)
{
    PrintBanner();

    double outstanding = amount * 1.2;
    for (auto& order : m_orders) {
        outstanding += order.Amount();
    }

    PrintDetails(outstanding);
}
// 重构后
void PrintOwing(double amount)
{
    PrintBanner();
    double outstanding = amount * 1.2;
    outstanding = CalOutstanding(outstanding);
    PrintDetails(outstanding);
}

double CalOutstanding(double val)
{
    double result = val;
    for (auto& order : m_orders) {
        result += order.Amount();
    }
    return result;
}

在临时变量过多的情况下,可以采用Replace Temp with Query减少临时变量。

2.2 Replace Temp with Query(以查询取代临时变量)

由于临时变量只能在所属函数内可见,因此会驱使你写出更长的函数。如果把临时变量替换为一个查询函数,那么同一个类中的所有函数都将可以获得这份信息。除此之外,Replace Temp with Query还可以使代码可维护性更好,如果临时变量的计算方式改变了,只需修改查询函数即可。

重构示例3
// 重构前
...
    double basePrice = m_quantity + m_itemPrice;
    if (basePrice > 1000)
    {
        return basePrice * 0.95;
    } else
    {
        return basePrice * 0.98;
    }
...
// 重构后
...
    if (BasePrice() > 1000)
    {
        return BasePrice() * 0.95;
    } else
    {
        return BasePrice() * 0.98;
    }
...

double BasePrice()
{
    return m_quantity + m_itemPrice;
}  

2.3 Introduce Explaining Variable(引入解释性变量)

Introduce Explaining Variable也是一个很常用的重构手法,如果表达式非常复杂而难以阅读,引入临时变量可以将表达式分解成容易管理的形式,可读性更强。该手法在条件逻辑中特别有价值——将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。

重构示例4
// 重构前
if ((platform.find("MAC") != platform.end()) &&
    (browser.find("IE") != browser.end()) &&
    IsInit() && resize > 0) {
    // do something
}
// 重构后
bool isMacOs = platform.find("MAC") != platform.end();
bool isIEBrowser = browser.find("IE") != browser.end();
bool isResized = resize > 0;
if (isMacOs && isIEBrowser && IsInit() && isResized) {
    // do something
}

2.4 Split Temporary Variable(分解临时变量)

如果有某个临时变量被赋值超过一次,则可以使用Split Temporary Variable进行重构,针对每一次赋值,创造一个独立、对应的临时变量。

重构示例5
// 重构前
double temp = 2 * (m_height + m_width);
cout << temp << endl;
temp = m_height * m_width;
cout << temp << endl;
// 重构后
double perimeter = 2 * (m_height + m_width);
cout << perimeter << endl;
double area = m_height * m_width;
cout << area << endl;

在软件开发过程中应该避免使用temp/tmp作为变量名。

2.5 Replace Method with Method Object(以函数对象取代函数)

对于一个大型的函数,如果有太多的临时变量导致无法采用Extract Method,而采用Replace Temp with Query

又产生过多的查询函数时,就可以采用Replace Method with Method Object对函数进行重构。

将函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解成多个小型函数。

重构示例6
// 重构前
class Account {
    ...
    int GetGamma(int inputVal, int quantity, int yearToDate)
    {
        int importantVal1 = (inputVal * quantity) + Delta();
        int importantVal2 = (inputVal * yearToDate) + 100;
        if ((yearToDate - importantVal1) > 100) {
            importantVal2 -= 20;
        }
        int importantVal3 = importantVal2 * 7;
        ...
        return importantVal3 - 2 * importantVal1;
    }
}
// 重构后
class Account {
    // ...
    int GetGamma(int inputVal, int quantity, int yearToDate)
    {
        Gamma gamma(this, inputVal, quantity, yearToDate);
        return gamma.Compute();
    }
}

class Gamma {
public:
    Gamma(Account* account, int inputVal, int quantity, int yearToDate);
    int Compute()
    {
        importantVal1 = (inputVal * quantity) + account->Delta();
        importantVal2 = (inputVal * yearToDate) + 100;
        if ((yearToDate - importantVal1) > 100) {
            importantVal2 -= 20;
        }
        importantVal3 = importantVal2 * 7;
        // ...
        return importantVal3 - 2 * importantVal1;
    }
private:
    Account* account;
    int inputVal;
    int quantity;
    int yearToDate;
    int importantVal1;
    int importantVal2;
    int importantVal3;
}

3 在对象之间搬移特性

3.1 Move Method(搬移函数)

如果某个函数使用另一个对象的次数比使用自己所驻对像的次数多,就可以使用Move Method将函数搬移到另一个对象。当函数中用到了原来对象的属性时,搬移后可以把原来对象this作为入参。

重构示例7
// 重构前
class Account {
public:    
    double OverdraftCharge()
    {
        if (m_type->IsPremium()) {
            double result = 10;
            if (m_dayOverdrawn > 7) {
                result += (m_dayOverdrawn - 7) * 0.85;
            }
            return result;
        }
        return m_dayOverdrawn * 1.75;
    }
  
    double BankCharge()
    {
        double result = 4.5;
        if (m_dayOverdrawn > 0) {
            result += OverdraftCharge();
        }
        return result;
    }
private:
    AccountType* m_type;
    int m_dayOverdrawn;
}
// 重构后
class Account {
public:
    double BankCharge()
    {
        double result = 4.5;
        if (m_dayOverdrawn > 0) {
            result += m_type->OverdraftCharge(this);
        }
        return result;
    }
private:
    AccountType* m_type;
    int m_dayOverdrawn;
}

class AccountType {
public:
    double OverdraftCharge(Account* account)
    {
        if (IsPremium()) {
            double result = 10;
            if (account->GetDayOverdrawn() > 7) {
                result += (account->GetDayOverdrawn() - 7) * 0.85;
            }
            return result;
        }
        return account->GetDayOverdrawn() * 1.75;
    }    
}

3.2 Move Field(搬移字段)

与Move Method类似,对于一个字段,如果在其所驻类之外的另一个类中有更多函数使用了它,就可以使用Move Field将该字段搬移到另一个对象中。需要注意的是,搬移之后需要在新的类中提供该字段的访问函数,否则原对象就无法访问到该字段了。

3.3 Extract Class(提炼类)

当一个类所承担的责任太多时,就可以使用Extract Class手法,根据单一职责原则,将类的一些函数和字段抽离出来,封装成一个新的类。

重构示例8
// 重构前
class Person {
public:
    string GetName() 
    {
        return m_name;
    }

    string GetTelephoneNumber()
    {
        return "(" + m_officeAreaCode + ") " + m_officeNumber;
    }

    string GetOfficeAreaCode()
    {
        return m_officeAreaCode;
    }

    string GetOfficeNumber()
    {
        return m_officeNumber;
    }
private:
    string m_name;
    string m_officeAreaCode;
    string m_officeNumber;
}
// 重构后
class Person {
public:
    string GetName() 
    {
        return m_name;
    }

    string GetTelephoneNumber()
    {
        return m_officeTelephone->GetTelephoneNumner();
    }

    TelephoneNumber* GetOfficeTelephone()
    {
        return m_officeTelephone;
    }
private:
    string m_name;
    TelephoneNumber* m_officeTelephone;
}

class TelephoneNumber {
public:
    string GetTelephoneNumner()
    {
        return "(" + m_areaCode + ") " + m_number;
    }

    string GetAreaCode()
    {
        return m_areaCode;
    }

    string GetNumber()
    {
        return m_number;
    }
private:
    string m_areaCode;
    string m_number;
}

3.4 Inline Class(将类内联化)

Inline Class与Extract Class相反,如果某个类所做的事情太少以至于不值得作为一个类存在,则将这个类的所有属性/函数搬移到另一个类中,然后删除原来的类。

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

Hide Delegate可以防止信息泄漏,避免模块之间的耦合。如果某个Client先通过服务对象A的字段得到另一个对象B,然后调用对象B的函数,那么Client就必须知晓这一层的委托关系。这时就产生了信息的泄漏,对象A将对象B泄漏给了Client。如果委托关系变了,Client也必须跟着改变,这样模块之间的耦合就太多了。

在服务类上建立Client所需的所有函数,可以隐藏委托关系。
代码重构的常用方法(C++实现)_第1张图片

重构示例9
// 重构前
class Person {
public:
    Department* GetDepartment()
    {
        return m_department;
    } 
private:
    Department* m_department;
}

class Department {
public:
    Person* GetManager()
    {
        return m_manager;
    }
private:
    Person* m_manager;
}
// 如果需要知道Person* john的经理是谁,必须先取得Department对象
manager = john->GetDepartment()->GetManager();
// 重构后
class Person {
public:
    Person* GetManager()
    {
        return m_department->GetManager();
    }
private:
    Department* m_department;
}

class Department {
public:
    Person* GetManager()
    {
        return m_manager;
    }
private:
    Person* m_manager;
}
// 现在可以通过Person* john直接知道经理了
manager = john->GetManager();

3.6 Remove Middle Man(移除中间人)

该重构手法与Hide Delegate刚好相反,如果某个类做了过多的简单委托动作,则把该类移除,让客户端直接调用受托类。

4 重新组织数据

4.1 Self Encapsulate Field(自封装字段)

直接访问一个字段,后面如果与该字段的耦合关系逐渐变得笨拙时,可以使用Self Encapsulate Field手法进行重构,为这个字段建立取值/设值函数,并且只以这些函数来访问字段。

重构示例10
// 重构前
class Range {
public:
    bool Includes(int arg)
    {
        return arg  >= m_low && arg <= m_high;
    }
private:
    int m_low;
    int m_high;
}
// 重构后
class Range {
public:
    bool Includes(int arg)
    {
        return arg  >= GetLow() && arg <= GetHigh();
    }
private:
    int GetLow()
    {
        return m_low;
    }
    int GetHigh()
    {
        return m_high;
    }
private:
    int m_low;
    int m_high;
}

应该更多的赋予对象业务行为函数而不是过多的使用getter/setter函数,否则对象就会变成领域驱动设计里面所提到的贫血模型

4.2 Replace Data Value with Object(以对象取代数据值)

软件开发初期,你往往会以简单的基本类型来表示某一概念,随着开发的迭代,这些概念不再是简单的基本类型就能表示的,这时就需要使用Replace Data Value with Object进行重构,封装一个新的对象来取代原有的基本类型数据值。比如重构示例8中,刚开始使用一个字符串来表示“电话号码”的概念,但随后就会发现,电话号码需要“格式化”、“抽取区号”之类的特殊行为,这些就需要将“电话号码”封装成一个对象了。
代码重构的常用方法(C++实现)_第2张图片

更好的方法是在设计阶段对通用语言进行领域建模,并赋予对象业务行为函数,这样可以使复杂的系统更加清晰。

4.3 Replace Type Code with Subclasses(以子类取代类型码)

如果一个类中有一个不可变的类型码,而且它会影响到类的行为,这时就可以以子类来取代这个类型码。一般来说,这种情况的标志就是出现switchif/else结构,它们检查类型码的值,并根据不同的值执行不同的动作。

Replace Type Code with Subclasses的好处在于,它把“对不同行为的了解”从类用户转移到了类自身。如果需要加入新的行为变化,只需添加一个子类即可。如果没有多态机制,就必须找到所有的条件表达式,并逐一修改它们。
代码重构的常用方法(C++实现)_第3张图片

重构示例11
// 重构前
class Employee {
public:
    Employee(int type) : m_type(type) {}
    int GetType()
    {
        return m_type;
    }
public:
    static constexpr int ENGINEER = 0;
    static constexpr int SALESMAN = 1;
    static constexpr int MANAGER = 2;
private:
    int  m_type;
}
// 重构后
class Employee {
public:
    static Employee* Create(int type)
    {
        switch (type) {
        case ENGINEER:
            return new Engineer;
        case SALESMAN:
            return new Salesman;
        case MANAGER:
            return new Manager;
        default:
            return nullptr;
        }
    }
    virtual int GetType() = 0;
private:
    Employee() = default;
public:
    static constexpr int ENGINEER = 0;
    static constexpr int SALESMAN = 1;
    static constexpr int MANAGER = 2;
}

class Engineer : public Employee {
public:
    int GetType() override
    {
        return Employee::ENGINEER;
    }
}

4.4 Replace Type Code with State/Strategy(以State/Strategy取代类型码)

如果一个类中有一个不可变的类型码,而且它会影响到类的行为,但是无法通过继承手法消除它时,就可以使用Replace Type Code with State/Strategy进行重构。如果是通过重构来简化一个算法,则Strategy模式比较合适;如果是打算搬移与状态相关的数据,而且把新建对象视为一种变迁状态,则State状态比较合适。
代码重构的常用方法(C++实现)_第4张图片

重构示例12
// 重构前
class Employee {
public:
    Employee(int type) : m_type(type) {}
    int GetType()
    {
        return m_type;
    }
    int PayAmount()
    {
        switch (m_type) {
            case ENGINEER:
                return m_monthlySalary;
            case SALESMAN:
                return m_monthlySalary + m_commission;
            case MANAGER:
                return m_monthlySalary + m_bonus;
            default:
            		return -1;
        }
    }
public:
    static constexpr int ENGINEER = 0;
    static constexpr int SALESMAN = 1;
    static constexpr int MANAGER = 2;
private:
    int  m_type;
}
// 重构后
class Employee {
public:
    Employee(int type) : m_type(EmployeeType::ValueOf(type)) {}
    int GetType()
    {
        return m_type->GetTypeCode();
    }
    int PayAmount()
    {
        switch (GetType()) {
            case EmployeeType::ENGINEER:
                return m_monthlySalary;
            case EmployeeType::SALESMAN:
                return m_monthlySalary + m_commission;
            case EmployeeType::MANAGER:
                return m_monthlySalary + m_bonus;
            default:
                return -1;
        }
    }
private:
    EmployeeType*  m_type;
}

class EmployeeType {
public:
    static EmployeeType* ValueOf(int code)
    {
        switch (code) {
            case ENGINEER:
                return new Engineer;
            case SALESMAN:
                return new Salesman;
            case MANAGER:
                return new Manager;
            default:
                return nullptr;
        }       
    }
    virtual int GetTypeCode() = 0;
public:
    static constexpr int ENGINEER = 0;
    static constexpr int SALESMAN = 1;
    static constexpr int MANAGER = 2;
}

class Engineer : public EmployeeType {
public:
    int GetTypeCode() override
    {
        return EmployeeType::ENGINEER;
    }
}

5 简化条件表达式

5.1 Decompose Conditional(分解条件表达式)

复杂的条件逻辑会降低代码的可读性,通过从if/else if/else三个段落中分别提炼出独立的函数,根据每一段落的用途命名函数,从而更清晰地表达自己的意图。

重构示例13
// 重构前
if (date.Before(SUMMER_START) || date.After(SUMMER_END)) {
    charge = quantity * m_winterRate + m_winterServiceCharge;
} else {
    charge = quantity * m_summerRate;
}
// 重构后
if (NotSummer(date)) {
    charge = WinterCharge(quantity);
} else {
    charge = SummerCharge(quantity);
}

bool NotSummer(Date date)
{
    return date.Before(SUMMER_START) || date.After(SUMMER_END)
}

int WinterCharge(quantity)
{
    return quantity * m_winterRate + m_winterServiceCharge;
}

int SummerCharge(quantity)
{
    return quantity * m_summerRate;;
}

5.2 Consolidate Conditional Expression(合并条件表达式)

有时候,一系列的条件分支都得到相同的结果,可以用Consolidate Conditional Expression手法将这些条件分支合为一个条件表达式,并提炼成一个独立的函数。

重构示例14
// 重构前
double DisabilityAmount()
{
    if (m_seniority < 2) {
        return 0;
    }
    if (m_monthsDisabled > 12) {
        return 0;
    }
    if (m_isPartTime) {
        return 0;
    }
    // ...
}
// 重构后
double DisabilityAmount()
{
    if (IsNotEligibleForDisability()) {
        return 0;
    }
    // ...
}

bool IsNotEligibleForDisability()
{
    return m_seniority < 2 || m_monthsDisabled > 12 || m_isPartTime;
}

5.3 Consolidate Duplicate Conditional Fragments(合并重复的条件片段)

如果在条件表达式的每个分支上有着相同的一段代码,可以使用Consolidate Duplicate Conditional Fragments将这段重复代码搬移到条件表达式之外。

重构示例15
// 重构前
if(IsSpecialDeal()) {
    total = price * 0.95;
    Send();
} else {
    total = price * 0.98;
    Send();
}
// 重构后
if(IsSpecialDeal()) {
    total = price * 0.95;
} else {
    total = price * 0.98;
}
Send();

5.4 Remove Control Flag (移除控制标记)

在一系列的条件表达式中,常常存在一个用于判断何时停止条件检查的控制标志。这源于结构化编程原则的“每个子程序只能有一个入口和一个出口”,但这样也降低了代码的可读性。可以通过break/continue/return来替换掉控制标志,提升代码可读性。

重构示例16
// 重构前
void CheckSecurity(vector& peoples)
{
    bool isFound = false;
    for (auto& people : peoples) {
        if (!isFound) {
            if (people == "Don") {
                SendAlert();
                isFound = true;
            }
            if (people == "John") {
                SendAlert();
                isFound = true;
            }
        }
    }
}
// 重构后
void CheckSecurity(vector& peoples)
{
    for (auto& people : peoples) {
        if (people == "Don") {
            SendAlert();
            break;
        }
        if (people == "John") {
            SendAlert();
            break;
        }
    }
}

5.5 Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)

嵌套的if/else语句式造成代码可读性差的罪魁祸首之一,它让人难以看清正常的执行路径。这时,可以通过使用卫语句表现所有特殊情况(最常见的就是对条件进行反转)来消除嵌套的条件表达式,提高代码可读性。

重构示例17
// 重构前
double GetAdjustedCapital()
{
    double result = 0.0;
    if (m_capital > 0.0) {
        if (m_intRate > 0.0 && m_duration > 0.0) {
            result = (m_income / m_duration) * ADJ_FACTOR;
        }
    }
    return result;
}
// 重构后
double GetAdjustedCapital()
{
    if (m_capital <= 0.0) {
        return 0.0;
    }
    if (m_intRate <= 0.0 || m_duration <= 0.0) {
        return 0.0;
    }
    return (m_income / m_duration) * ADJ_FACTOR;
}

5.6 Replace Conditional with Polymorphism(以多态取代条件表达式)

该手法有点类似于Replace Type Code with Subclasses,如果有个条件表达式,根据类型码的不同而选择不同的行为。这时可以通过Replace Conditional with Polymorphism,将这个条件表达式的每一个分支放进一个子类内的覆写函数中,然后将原市函数声明为抽象函数。继续以重构示例12中的代码示例为例子,我们采用Move Method将PayAmount()函数迁移到EmployeeType,并以多态来取代在其中的switch语句。

重构示例18
// 重构前
class Employee {
public:
    Employee(int type) : m_type(EmployeeType::ValueOf(type)) {}
    int GetType()
    {
        return m_type->GetTypeCode();
    }
    int PayAmount()
    {
        switch (GetType()) {
            case EmployeeType::ENGINEER:
                return m_monthlySalary;
            case EmployeeType::SALESMAN:
                return m_monthlySalary + m_commission;
            case EmployeeType::MANAGER:
                return m_monthlySalary + m_bonus;
            default:
                return -1;
        }
    }
private:
    EmployeeType*  m_type;
}

class EmployeeType {
public:
    static EmployeeType* ValueOf(int code)
    {
        switch (code) {
            case ENGINEER:
                return new Engineer;
            case SALESMAN:
                return new Salesman;
            case MANAGER:
                return new Manager;
            default:
                return nullptr;
        }       
    }
    virtual int GetTypeCode() = 0;
public:
    static constexpr int ENGINEER = 0;
    static constexpr int SALESMAN = 1;
    static constexpr int MANAGER = 2;
}

class Engineer : public EmployeeType {
public:
    int GetTypeCode() override
    {
        return EmployeeType::ENGINEER;
    }
}
// 重构后
class Employee {
public:
    Employee(int type) : m_type(EmployeeType::ValueOf(type)) {}
    int GetType()
    {
        return m_type->GetTypeCode();
    }
    int PayAmount()
    {
        return m_type->PayAmount();
    }
private:
    EmployeeType*  m_type;
}

class EmployeeType {
public:
    static EmployeeType* ValueOf(int code)
    {
        switch (code) {
            case ENGINEER:
                return new Engineer;
            case SALESMAN:
                return new Salesman;
            case MANAGER:
                return new Manager;
            default:
                return nullptr;
        }       
    }
    virtual int GetTypeCode() = 0;
    virtual int PayAmount() = 0;
public:
    static constexpr int ENGINEER = 0;
    static constexpr int SALESMAN = 1;
    static constexpr int MANAGER = 2;
}

class Engineer : public EmployeeType {
public:
    int GetTypeCode() override
    {
        return EmployeeType::ENGINEER;
    }
    int PayAmount() override
    {
        return m_monthlySalary;
    }
}

5.7 Introduce Null Object(引入Null对象)

引入Null对象主要是为了消除随处可见的判空逻辑,通过新建一个Null对象,并在原来返回Null的地方改成返回新建的Null对象。
代码重构的常用方法(C++实现)_第5张图片

Java 8中新增了一个Optional接口,相对于新建一个Null对象,更推荐使用Optional,除了可以表示Null对象的语义之外,它还提供了很多很强大的功能。C++14中也新增了std::optional,提供了类似的功能。

6 简化函数调用

6.1 Introduce Parameter Object(引入参数对象)

在代码中,可能有一组参数总是一起被传递到好几个函数中,这样的一组参数就是所谓的Data Clumps(数据泥团)。最常见的就是指代一个时间范围的startTime/endTime。可以通过Introduce Parameter Object手法,以一个对象取代这些参数。

重构示例19
// 重构前
class Account {
public:
    double GetFlowBetween(Date& startTime, Date& endTime)
    {
        double result = 0.0;
        for (auto& entry : m_entries) {
            if (entry.GetDate() == startTime || entry.GetDate() == endTime ||
                (entry.GetDate().After(startTime) && entry.GetDate().Before(endTime)) {
                result += entry.GetValue();
            }
        }
        return result;
    }
}
// 重构后
class Account {
public:
    double GetFlowBetween(DateRange& dateRange)
    {
        double result = 0.0;
        for (auto& entry : m_entries) {
            if (dateRange.Includes(entry.GetDate())) {
                result += entry.GetValue();
            }
        }
        return result;
    }
}

class DateRange {
public:
    bool Includes(Date& date)
    {
        return date == m_startTime || date == m_endTime ||
                (date.After(m_startTime) && date.Before(m_endTime));
    }
private:
    Date m_startTime;
    Date m_endTime;
}

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

如果希望在创建对象时不仅仅是做简单的构建动作,可以将构造函数替换为静态工厂函数,并将原来的构造函数设为私有。静态工厂函数不仅在语义上更加符合人的思维,使代码可读性更强,它还能降低对象与对象使用者之间的耦合。比如,后续想要把对象改成单例模式,只需修改一下静态工厂方法,对象的使用者无感知。

Java 8新增的接口都采用了静态工厂函数用于创建对象,比如Optional接口、新的时间接口等。

6.3 Replace Parameter with Explicit Method(以明确函数取代参数)

如果某个参数有多种可能的值,而函数内又以条件表达式检查这些参数值,并根据不同参数值作出不同的行为,那么就可以使用Replace Parameter with Explicit Method进行重构了。该手法是提供了不同的函数给调用者使用,避免出现条件表达式。

重构示例20
// 重构前
static Employee* Create(int type)
{
    switch (type) {
    case ENGINEER:
        return new Engineer;
    case SALESMAN:
        return new Salesman;
    case MANAGER:
        return new Manager;
    default:
        return nullptr;
    }
}
// 重构后
static Employee* CreateEngineer()
{
    return new Engineer;
}
static Employee* CreateSalesman()
{
    return new Salesman;
}
static Employee* CreateManager()
{
    return new Manager;
}

6.4 Remove Setting Method(移除设置函数)

某些类中的某些字段应该在初始化时就确认值,后续都不会在变化。如果这样的字段拥有setter函数,就应该把它去掉,然后把字段设置为const/final。

过多无用的getter/setter函数是造成贫血模型的罪恶源头

重构示例21
// 重构前
class Account {
public:
    Account(string id)
    {
        SetId(id);
    }
    void SetId(string id)
    {
        m_id = id;
    }
private:
    string m_id;

// 重构后
class Account {
public:
    Account(string id) m_id(id) {}
private:
    const string m_id;
}

7 处理继承关系

7.1 Pull Up Field(字段上移)

如果两个子类有相同的字段,则将该字段上移至超类中。
代码重构的常用方法(C++实现)_第6张图片

7.2 Pull Up Method(函数上移)

如果两个子类有相同的函数而且产生完全相同的结果,则将该函数上移至超类。

代码重构的常用方法(C++实现)_第7张图片

7.3 Extract Superclass(提炼超类)

如果两个类有相似特性,可以为这两个类建立一个超类,将相同特性移至超类。如果继承不合适,可以使用[Extract Class](#Extract Class)来提取重复代码。

代码重构的常用方法(C++实现)_第8张图片

7.4 Form Template Method(塑造模板函数)

该重构的手法其实就是设计模式中的模板模式,如果有一些子类,其中对应的某些函数以相同顺序执行类似的操作,但在各个操作的细节上有所不同。可以将这些操作分别放到独立的函数中,替换在原函数中原有的操作代码,并上移至超类。

重构示例22
// 重构前
class Site {
public:
    virtual double GetBillableAmount() = 0;
    // ...
}

class ResidentialSite : public Site {
public:
    double GetBillableAmount() override
    {
        double base = m_units * m_rate;
        double tax = base * Site::TAX_RATE;
        return base + tax;
    }
    // ...
}

class LifelineSite : public Site {
public:
    double GetBillableAmount() override
    {
        double base = m_units * m_rate * 0.5;
        double tax = base * Site::TAX_RATE * 0.2;
        return base + tax;
    }
    // ...
}
// 重构后
class Site {
public:
    virtual double GetBaseAmount() = 0;
    virtual double GetTaxAmount() = 0;
    double GetBillableAmount()
    {
        return GetBaseAmount() + GetTaxAmount();
    }
    // ...
}

class ResidentialSite : public Site {
public:
    double GetBaseAmount() override
    {
        return m_units * m_rate;
    }
    double GetTaxAmount() override
    {
        return GetBaseAmount() * Site::TAX_RATE;
    }
    // ...
}

class LifelineSite : public Site {
public:
    double GetBaseAmount() override
    {
        return m_units * m_rate * 0.5;
    }
    double GetTaxAmount() override
    {
        return GetBaseAmount() * Site::TAX_RATE * 0.2;
    }
    // ...
}

7.3 Replace Inheritance with Delegation(以委托取代继承)

有时候继承会使代码变得复杂,特别是继承层次很深时,这时通过组合来取代继承(用has-a取代is-a)更加合适。如果某个类只使用了超类接口中的一部分或者根本就不需要继承而来的数据,就可以使用Replace Inheritance with Delegation进行重构了。

代码重构的常用方法(C++实现)_第9张图片

7.5 Collapse Hierarchy(折叠继承体系)

如果超类和子类实现的功能没有太大的差别,则使用Collapse Hierarchy将它们合并成一个类。

你可能感兴趣的:(软件设计,重构)