重构·改善既有代码的设计.04之重构手法(下)完结

Posted 有一只柴犬

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重构·改善既有代码的设计.04之重构手法(下)完结相关的知识,希望对你有一定的参考价值。

1. 前言

本文是代码重构系列的最后一篇啦。前面三篇《重构·改善既有代码的设计.01之入门基础》、《重构·改善既有代码的设计.02之代码的“坏味道”》、《重构·改善既有代码的设计.03之重构手法(上)》介绍了基础入门,代码异味,还有部分重构手法。今天继续总结重构手法下篇,从条件表达式、函数调用、以及类继承关系上阐述了各种重构手法,希望对项目能有所帮助。另外本文更新后该系列就完结了,感谢各位看官的指点。

2. 简化条件表达式

“分支逻辑”和“操作细节”分离。

1、Decompose Conditional 分解条件表达式。

复杂的条件语句(if-then-else)。
改造前:

if(date.before(SUMMER_START) || date.after(SUMMER_END)) 
    charge = quantity * _winterRate * _winterServiceCharge;
 else 
    charge = quantity * _summerRate;

改造后:

if(notSummer(date)) 
    charge = winterCharge(quantity);
 else 
    charge = summerCharge(quantity);


private boolean notSummer(Date date) 
    return date.before(SUMMER_START) || date.after(SUMMER_END);


private double summerCharge(int quantity) 
    return quantity * _summerRate;


private double winterCharge(int quantity) 
    return quantity * _winterRate * _winterServiceCharge;

2、Consolidate Conditional Expression 合并条件表达式。

有一系列条件测试,都得到相同结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成一个独立函数。
改造前:

double disabilityAmount()
    if(_seniority < 2) 
        return 0;    
    
    if(_monthDisabled > 12) 
        return 0;    
    
    if(_isPartTime) 
        return 0;    
    
    // todo...

改造后:

double disabilityAmount()
    if((_seniority < 2) || (_monthDisabled > 12) || _isPartTime) 
        return 0;    
    
    // todo...


或:
double disabilityAmount()
    if(isDisability()) 
        return 0;    
    
    // todo...


boolean isDisability() 
    return (_seniority < 2) || (_monthDisabled > 12) || _isPartTime;

3、Consolidate Duplicate Conditional Fragments 合并重复的条件片段。

条件表达式的每个分支上有着相同的一段代码。
改造前:

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

4、Remove Control Flag 移除控制标记。

循环体中,通常需要判断何时停止条件检查。有时会引入某个控制变量来起到循环判断的作用。建议以break或continue或return 语句取代控制标记。
改造前:

void check(String[] person) 
    boolean found = false;   // 控制标记
    for(int i = 0; i < person.length; i++) 
        if(!found) 
            if(person[i] == "tom") 
                found = true;
                sendAlert();                
            
            if(person[i] == "jose") 
                found = true;
                sendAlert();                
                      
            
    
    

改造后:

void check(String[] person) 
    boolean found = false;   // 控制标记
    for(int i = 0; i < person.length; i++) 
        if(!found) 
            if(person[i] == "tom") 
                sendAlert();
                break;                
            
            if(person[i] == "jose") 
                sendAlert();    
                break;            
                      
            
    
    

5、Replace Nested Conditional with Guard Clauses 以卫语句取代嵌套条件表达式。

条件表达式中,如果两条分支都是正常行为,使用形如if…else…的条件表达式;如果某个条件极为罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回(如参数校验判断)。这样的单检查称为“卫语句”。
卫语句要么就从函数中返回,要么就抛出一个异常。
改造前:

double getPayment()
    double result;
    if(_isDead) 
        result = deadAmount();    
     else 
        if(_isSeparated) 
            result = separatedAmount();        
         else 
            if(_isRetired) 
                result = retiredAmount();            
             else 
                result = normalAmount();            
                   
            
    
    
    return result;

改造后:

double getPayment()
    if(_isDead) 
        return deadAmount();    
    
    if(_isSeparated) 
       return separatedAmount();        
    
    if(_isRetired) 
       return retiredAmount();    
            
        
    return normalAmount(); 

6、Replace Conditional with Polymorphism 以多态取代条件表达式。

改造前:

class Employee 
    private EmployeeType _type;
    
    int payment() 
        switch(getType()) 
            case EmployeeType.ENGINEER:
                return _monthlySalary;
            case EmployeeType.SALESMAN:
                return _monthlySalary + _commission;
            case EmployeeType.MANAGER:
                return _monthlySalary + _bonus;
            default:
                throw new RuntimeException("error");                                                    
            
    
    
    int getType() 
        return _type.getTypeCode();    
    


abstract class EmployeeType 
    abstract int getTypeCode();


class Engineer extends EmployeeType 
    int getTypeCode()
        return EmployeeType.ENGINEER;    
    


class Salesman extends EmployeeType 
    int getTypeCode()
        return EmployeeType.SALESMAN;    
    


class Manager extends EmployeeType 
    int getTypeCode()
        return EmployeeType.MANAGER;    
    

改造后:

class Employee 
    private EmployeeType _type;
    
    int payment() 
        return _type.payment();  
    


abstract class EmployeeType 
    abstract int payment(Employee emp);


class Engineer extends EmployeeType 
    int payment(Employee emp)
        return emp.getMonthlySalary();   
    


class Salesman extends EmployeeType 
    int payment(Employee emp)
        return emp.getMonthlySalary() + emp.getCommission();   
    


class Manager extends EmployeeType 
    int payment(Employee emp)
        return emp.getMonthlySalary() + emp.getBonus();   
    

7、Introduce Null Object 引入Null对象。

将null替换为null对象。
改造前:

class Site 
    private Customer _customer;
    Customer getCustomer() 
        return _customer;    
    


class Customer 
    public Stirng getName() ...
    public BillingPlan getPlan()...


// 调用
Customer customer = site.getCustomer();
if(customer == null) 
    plan = BillingPlan.basic();
 else 
    plan = customer.getPlan();

String customerName;
if(customer == null) 
    customerName = "default";
 else 
    customerName = customer.getName();

改造后:

class Site 
    private Customer _customer;
    Customer getCustomer() 
        return _customer == ull ? Customer.newNull() : _customer;   
    


class Customer 
    public Stirng getName() ...
    public BillingPlan getPlan()...
    
    public boolean isNull() 
        return false;    
    
    
    static Customer newNull() 
        return new NullCustomer();    
    


// 定义NullCustomer空对象
class NullCustomer extends Customer
    public boolean isNull() 
        return true;    
    


// 调用
Customer customer = site.getCustomer();
if(customer.isNull()) 
    plan = BillingPlan.basic();
 else 
    plan = customer.getPlan();

String customerName;
if(customer.isNull()) 
    customerName = "default";
 else 
    customerName = customer.getName();

8、Introduce Assertion 引入断言。

一段代码需要对程序状态做出某种假设。以断言明确表现这种假设。
改造前:

double getExpenseLimit() 
    return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit : _primaryProject.getMemberExpenseLimit();

改造后:

double getExpenseLimit() 
    Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null);
    return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit : _primaryProject.getMemberExpenseLimit();

断言,请不要用它来检查“你认为应该为真”的条件,请只使用它来检查“一定必须为真”的条件。请勿滥用。
如果断言所指示的约束条件不能满足,代码是否仍能正常运行? 如果可以,就把断言去掉。

3. 简化函数调用

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

1、Rename Method 函数改名。

将复杂的处理过程分解成小函数。但是如果做的不好,会使你费尽周折却弄不清楚这些小函数各自的用途。要避免这种麻烦,关键在于给函数起一个好名称。
1、尽可能起一个良好名称的函数,顾名思义表达该函数的作用,而不是表达该函数如何做。
2、合理安排函数签名,如果重新安排参数顺序,能够帮助提供代码的清晰度。

2、Add Parameter 添加参数。

为函数添加一个对象参数,让该对象带进函数所需信息。
动机:
你必须修改一个函数,而修改后的函数需要一些过去没有的信息,因此你需要给该函数添加一个参数。
改造前:

double getExpenseLimit(double limit) 
    // todo...


// 需要添加一个参数
double getExpenseLimit(double limit, Date date) 
    // todo...

改造后:

double getExpenseLimit(ExpenseLimit limitObj) 
    // todo...
    double limit = limitObj.getLimit();
    Date date = limitObj.getDate();


class ExpenseLimit 
    double limit;
    Date date;

但是需要警惕引用传递。其实我并不推荐整个对象传参。当你传整个参数时,对于这个函数你不能准确的说出这个函数所使用的参数。有可能对象包含了5个参数,而你理论上只需要3个。 这时候宁可将参数依次卸载参数列表中。不过所带来的影响是代码参数过长。如果过长,也是不太友好的。
推荐:适当使用参数列表和对象参数,必要时可以进行函数重载更简洁说明函数意图。比如:

double getExpenseLimit(double limit) 
    this.getExpenseLimit(limit, new Date());


double getExpenseLimit(double limit, Date date) 
    ExpenseLimit limitObj = new ExpenseLimit(limit, date);
    this.getExpenseLimit(limitObj);


double getExpenseLimit(ExpenseLimit limitObj) 
    // todo...
    double limit = limitObj.getLimit();
    Date date = limitObj.getDate();


class ExpenseLimit 
    double limit;
    Date date;

3、Remove Parameter 移除参数。

移除函数体无用参数。
改造前:

double getExpenseLimit(double limit, Date date) 
    return limit * 0.98;

改造后:

double getExpenseLimit(double limit) 
    return limit * 0.98;

4、Separate Query from Modifier 将查询函数和修改函数分离。

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

5、Parameterize Method 令函数携带参数。

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

class Employee 
    void tenPercentRaise() 
        salary *= 1.1;    
    
    void fivePercentRaise() 
        salary *= 1.05;    
    

改造后:

class Employee 
    void raise(double factor) 
        salary *= (1 + factor);    
    

6、Replace Parameter with Explicit Methods 以明确函数取代参数。

有一个函数,其中完全取决于参数值而采取不同行动。
改造前:

void setValue(String name, int value) 
    if(name.equals("height")) 
        _height = value;
        return ;    
    
    if(name.equals("width")) 
        _width = value;
        return ;    
    

改造后:

void setHeight(int value) 
    _height = value;

void setWidth(int value) 
    _width = value;

7、Preserve Whole Object 保持对象完整。

从某个对象中取出若干值,将它们作为某一个函数调用时的参数。
改造前:

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

改造后:

withinPlan = plan.withinRange(daysTempRange());

注:
该方法总有两面。如果你传的是数值,被调用函数就只依赖于这些数值,而不依赖它们所属的对象。但如果你传递的是整个对象,被调用函数所在的对象就需要依赖参数对象。如果这样,会使你的依赖结构恶化,那么就不该使用该方法。

8、Replace Parameter with Methods 以函数取代参数。

对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。
改造前:

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

改造后:

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

double discountedPrice(int basePrice) 
    discountLevel = getDiscountLe

重构改善既有代码设计--重构手法01:Extract Method (提炼函数)

背景:

你有一段代码可以被组织在一起并独立出来。将这段代码放进一个独立函数,并让函数名称解释该函数的用途。      

void PrintOwing(double amount)

        {

            PrintBanner();

            //print details

            Console.WriteLine("name:"+_name);

            Console.WriteLine("amount:"+_amount);

        }

 

                           

      

void PrintOwing(double amount)

        {

            PrintBanner();

            //print details

            PrintDetails();

        }

 

        private void PrintDetails()

        {

            Console.WriteLine("name:" + _name);

            Console.WriteLine("amount:" + _amount);

        }

 

动机:

Extract Method (提炼函数)是最常用的重构手法之一。当看见一个过长的函数或者一段需要注释才能让人理解用途的代码,就应该将这段代码放进一个独立函数中。

       简短而命名良好的函数的好处:首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就想一系列注释;再次,如果函数都是细粒度,那么函数的覆写也会更容易些。

       一个函数多长才算合适?长度不是问题,关键在于函数名称和函数本体之间的语义距离。如果提炼可以强化代码的清晰度,那就去做,就算函数名称必提炼出来的代码还长也无所谓。

做法:

1、创造一个新函数,根据这个函数的意图对它命名(以它“做什么“命名,而不是以它“怎样做”命名)。

即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼它。但如果你想不出一个更有意义的名称,就别动。

2、将提炼出的代码从源函数复制到新建的明白函数中。

3、仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。

4、检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。

5、检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码处理为一个查询,并将结果赋值给修改变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动提炼出来。你可能需要先使用 Split Temporary Variable (分解临时变量),然后再尝试提炼。也可以使用 Replace Temp with Query (以查询取代临时变量)将临时变量消灭掉。

6、将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。

7、处理完所有局部变量后,进行编译。

8、在源函数中,将被提炼代码段替换给对目标函数的调用。

     如果你将如何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在可以删除这些声明式了。

9、编译,测试。

 

代码演示:

实例代码如下:

 1 private string myName;
 2 public void printPeople(int Age)
 3 {
 4     printFamily();
 5     //无数代码//
 6 
 7     //打印个人信息
 8     Console.WriteLine("Name:" + myName);
 9         Console.WriteLine("Age:" + Age);
10 }


重构后的代码如下:

 1 private string myName;
 2 public void printPeople(int Age)
 3 {
 4     printFamily();
 5     //无数代码//
 6     printMyInfo(Age);
 7 }
 8 
 9 void printMyInfo(int Age)
10 {
11     Console.WriteLine("Name:" + myName);
12         Console.WriteLine("Age:" + Age);
13 }


为什么要这样重构?当一个函数很大的时候,第一对代码的修改起来非常的不方便.
第二,会对你读代码有障碍,试想一下当你看到一个很多行代码的方法,你还有心情看下去吗?
第三,方法与方法之间的复用性会非常的好,方法的重写也会更容易些.

那么我们应该怎么做呢?
看第一个例子:
无局部变量的方法提炼.

 1 void printOwing()
 2 {
 3     ArrayList al = myOrders.GetOrderList();
 4     double outstanding = 0.0;
 5 
 6     //打印头部信息
 7     Console.WriteLine("*****************");
 8     Console.WriteLine("**Customer Owes**");
 9     Console.WriteLine("*****************");
10 
11     //计算
12     foreach(Object o in al)
13     {
14         Order each = (Order)o;
15         outstanding += each.Amount;
16     }
17 
18     //打印具体信息
19     Console.WriteLine("Name:" + myName);
20     Console.WriteLine("Age:" + age);
21 }


好了我们开始先提最简单的部分.提出后的代码如下:

 1 void printOwing()
 2 {
 3     ArrayList al = myOrders.GetOrderList();
 4     double outstanding = 0.0;
 5 
 6     printBanner();
 7 
 8     //计算
 9     foreach(Object o in al)
10     {
11         Order each = (Order)o;
12         outstanding += each.Amount;
13     }
14 
15     //打印具体信息
16     Console.WriteLine("Name:" + myName);
17     Console.WriteLine("Age:" + age);
18 }
19 
20 void printBanner()
21 {
22     //打印头部信息
23     Console.WriteLine("*****************");
24     Console.WriteLine("**Customer Owes**");
25     Console.WriteLine("*****************");
26 }


最简单的提炼方法结束了.
下来我们看有局部变量的方法提炼.就拿上面的的代码开刀.

 1 void printOwing()
 2 {
 3     ArrayList al = myOrders.GetOrderList();
 4     double outstanding = 0.0;
 5 
 6     printBanner();
 7 
 8     //计算
 9     foreach(Object o in al)
10     {
11         Order each = (Order)o;
12         outstanding += each.Amount;
13     }
14 
15     printInfo(outstanding);
16 }
17 
18 void printBanner()
19 {
20     //打印头部信息
21     Console.WriteLine("*****************");
22     Console.WriteLine("**Customer Owes**");
23     Console.WriteLine("*****************");
24 }
25 
26 void printInfo(double OutStanding)
27 {
28     //打印具体信息
29     Console.WriteLine("Name:" + myName);
30     Console.WriteLine("Age:" + age);   
31 }


我们再来看下对局部变量再赋值方法的提炼.继续拿上面代码开刀.

 1 void printOwing()
 2 {
 3     double outstanding = GetOutStanding();
 4 
 5     printBanner();
 6 
 7     printInfo(outstanding);
 8 }
 9 
10 void printBanner()
11 {
12     //打印头部信息
13     Console.WriteLine("*****************");
14     Console.WriteLine("**Customer Owes**");
15     Console.WriteLine("*****************");
16 }
17 
18 void printInfo(double OutStanding)
19 {
20     //打印具体信息
21     Console.WriteLine("Name:" + myName);
22     Console.WriteLine("Age:" + age);   
23 }
24 
25 double GetOutStanding()
26 {
27     ArrayList al = myOrders.GetOrderList();
28     double outstanding = 0.0;
29     //计算
30     foreach(Object o in al)
31     {
32         Order each = (Order)o;
33         outstanding += each.Amount;
34     }
35     return outstanding
36 }


Extract Method方法讲解玩了.有人会问为什么要这样写?这样写的好处我没有看到啊.
那么现在有个这样的需求,我要设置outstanding的初始值,那么我们只要修改GetOutStanding方法,代码

如下:

 1 double GetOutStanding(double previousAmount)
 2 {
 3     ArrayList al = myOrders.GetOrderList();
 4     double outstanding = previousAmount;
 5     //计算
 6     foreach(Object o in al)
 7     {
 8         Order each = (Order)o;
 9         outstanding += each.Amount;
10     }
11     return outstanding
12 }


主要方法修改如下:

1 void printOwing()
2 {
3     double outstanding = GetOutStanding(500.5);

5     printBanner();

7     printInfo(outstanding);
8 }


如果需求继续增加,我们修改起来是不是方便了许多?

 

 

读后感:

1.如果说没有任何局部变量,那么这个函数提炼就非常容易提炼.

2.如果说提炼的时候有局部变量,即用到了提炼函数之外的局部变量,那么如果仅仅是内部函数使用的,直接放到内部函数中;第二种,如果提炼的函数内部没有对此变量赋值的情况,仅仅是读取使用,那么直接从外面作为参数传递进来。

3.如果提炼的函数,不仅仅有局部变量,并且还要对其赋值,那么同样要看,这个局部变量是不是只是内部使用,如果只是内部使用,直接放进来,如果不是,那就说外面还要用到,那么需要经过提炼函数运算后,将值返回去.

 

以上是关于重构·改善既有代码的设计.04之重构手法(下)完结的主要内容,如果未能解决你的问题,请参考以下文章

重构·改善既有代码的设计.03之重构手法(上)

重构:改善既有代码的设计读书笔记——开篇

重构改善既有代码设计--重构手法04:Replace Temp with Query (以查询取代临时变量)

重构改善既有代码设计--重构手法01:Extract Method (提炼函数)

重构改善既有代码设计--重构手法09:Substitute Algorithm (替换算法)

重构改善既有代码设计--重构手法06:Split Temporary Variable (分解临时变量)