要资料 文章 文库 视频 Code iProcess 课程 认证 咨询 工具 讲座吧   专家招募  
会员   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
 
 
   
分享到
改善代码设计
 

作者:Create Chen ,发布于2012-10-19,来源:博客园

 

目录

1. 改善代码设计 —— 优化函数的构成(Composing Methods)

2. 改善代码设计 —— 优化物件之间的特性(Moving Features Between Objects)

3. 改善代码设计 —— 组织好你的数据(Composing Data)

4. 改善代码设计 —— 简化条件表达式(Simplifying Conditional Expressions)

5. 改善代码设计 —— 简化函数调用(Making Method Calls Simpler)

6. 改善代码设计 —— 处理概括关系(Dealing with Generalization)

1. 改善代码设计 —— 优化函数的构成(Composing Methods)

1. Extract Method (提炼函数)

解释:

如果发现一个函数的代码很长, 很可能的一种情况是这个函数做了很多事情, 找找看函数中有没有注释, 往往注释都是为了解释下面一块代码做的什么事情, 可以考虑将这块代码提炼(Extract)成一个独立的函数.

这样做的好处不言而喻, 是面向对象五大基本原则中的单一职责原则 (Single Responsibility Principle), 比较长的函数被拆分成一个个小函数, 将有利于代码被复用.

冲动前:

public void Print(Employee employee)
{
    //print employee's information
    Console.WriteLine("Name:" + employee.Name);
    Console.WriteLine("Sex:" + employee.Sex);
    Console.WriteLine("Age:" + employee.Age);

    //print employee's salary
    Console.WriteLine("Salary:" + employee.Salary);
    Console.WriteLine("Bonus:" + employee.Bonus);
}

冲动后:

public void Print(Employee employee)
{
    //print employee's information
    PrintInfo(employee);

    //print employee's salary
    PrintSalary(employee);
}

public void PrintInfo(Employee employee)
{
    Console.WriteLine("Name:" + employee.Name);
    Console.WriteLine("Sex:" + employee.Sex);
    Console.WriteLine("Age:" + employee.Age);
}
public void PrintSalary(Employee employee)
{
    Console.WriteLine("Salary:" + employee.Salary);
    Console.WriteLine("Bonus:" + employee.Bonus);
}

2. Inline Method (将函数内联)

解释:

有些函数很短, 只有一两行, 而且代码的意图也非常明显, 这时可以考虑将这个函数干掉, 直接使用函数中的代码.

物件中过多的方法会让人感到不舒服, 干掉完全不必要的函数后代码会更简洁.

冲动前:

public bool IsDeserving(int score)
{
    return IsScoreMoreThanSixty(score);
}

public bool IsScoreMoreThanSixty(int score)
{
    return (score > 60);
}

冲动后:

public bool IsDeserving(int score)
{
    return (score > 60) ;
}

3. Inline Temp (将临时变量内联)

解释:

如果有一个临时变量 (Temp)用来表示某个函数的返回值, 一般来说, 这样的做法挺好的. 但如果这个临时变量实在多余, 将这个临时变量内联之后毫不影响代码的阅读, 甚至这个临时变量妨碍了其它重构工作, 就应该将这个临时变量内联化.

把这个临时变量干掉的好处在于减少了函数的长度, 有时可以让其它重构工作更顺利的进行.

冲动前:

int salary = employee.Salary;
return (salary > 10000);

冲动后:

return (employee.Salary > 10000);

4. Replace Temp With Query (用查询式代替临时变量)

解释:

程序中有一个临时变量(Temp)用来保存某个表达式的计算结果, 将这个计算表达式提炼(Extract)到一个独立的函数(即查询式Query)中, 将这个临时变量所有被调用的地方换成对新函数(Query)的调用, 新函数还可以被其它函数使用.

好处在于减少函数长度, 增加代码复用率, 有利于代码进一步的重构. 并且注意 Replace Temp With Query 往往是 Extract Method 之前必不可少的步骤, 因为局部变量会使代码不太容易被提炼, 所以在进行类似的重构前可以将它们替换成查询式.

下面的这个例子不是很有必要使用Replace Temp With Query, 主要展示如何 Replace Temp With Query. 试想"冲动前"函数中有很多个代码块都使用到 totalPrice, 突然有一天我发现这个函数太长, 我需要将这一块块的代码提炼成单独的函数, 这样就需要将 totalPrice = price * num; 放到每一个提炼出来的函数中. 而如果原来函数中使用的是查询式, 就不存在这个问题. 如果查询式中的计算量很大, 也不建议使用 Replace Temp With Query.

冲动前:

public double FinalPrice(double price, int num)
{
    double totalPrice = price * num;
    if (totalPrice > 100)
        return totalPrice * 0.8;
    else
        return totalPrice * 0.9;
}

冲动后:

public double FinalPrice(double price, int num)
{
    if (TotalPrice(price, num) > 100)
        return TotalPrice(price, num) * 0.8;
    else
        return TotalPrice(price, num) * 0.9;
}
public double TotalPrice(double price, int num)
{
    return price * num;
}

5. Introduce Explaining Variable (引入可以理解的变量)

解释:

很多时候在条件逻辑表达式中, 很多条件令人难以理解它的意义, 为什么要满足这个条件? 不清楚. 可以使用Introduce Explaining Variable将每个条件子句提炼出来, 分别用一个恰当的临时变量名表示条件子句的意义.

好处在于增加了程序的可读性.

冲动前:

if((operateSystem.Contains("Windows"))&&
    (browser.Contatins("IE")))
{
    //do something
}

冲动后:

bool isWindowsOS = operateSystem.Contains("Windows");
bool isIEBrowser = browser.Contatins("IE");
if (isWindowsOS && isIEBrowser)
{
    //do something
}

6. Split Temporary Variable (撇清临时变量)

解释:

例如代码中有个临时变量在函数上面某处表示长方形周长, 在函数下面被赋予面积, 也就是这个临时变量被赋值超过一次, 且表示的不是同一种量. 应该针对每次赋值, 分配一个独立的临时变量.

一个变量只应表示一种量, 否则会令代码阅读者感到迷惑.

冲动前:

double temp = (width + height) * 2;
//do something
temp = width * height;
//do something

冲动后:

double perimeter = (width + height) * 2;
//do something
double area = width * height;
//do something

7. Remove Assignments to Parameters (消除对参数的赋值操作)

解释:

传入参数分"传值"和"传址"两种, 如果是"传址", 在函数中改变参数的值无可厚非, 因为我们就是想改变原来的值. 但如果是"传值", 在代码中为参数赋值, 就会令人产生疑惑. 所以在函数中应该用一个临时变量代替这个参数, 然后对这个临时变量进行其它赋值操作.

冲动前:

public double FinalPrice(double price, int num)
{
    price = price * num;
    //other calculation with price
    return price;
}

冲动后:

public double FinalPrice(double price, int num)
{
    double finalPrice = price * num;
    //other calculation with finalPrice
    return finalPrice;
}

8. Replace Method with Method Object (用函数物件代替函数)

解释:

冲动的写下一行行代码后, 突然发现这个函数变得非常大, 而且由于这个函数包含了很多局部变量, 使得无法使用 Extract Method, 这时 Replace Method with Method Object 就起到了杀手锏的效果. 做法是将这个函数放入一个单独的物件中, 函数中的临时变量就变成了这个物件里的值域 (field).

冲动前:

class Bill
{
    public double FinalPrice()
    {
        double primaryPrice;
        double secondaryPrice;
        double teriaryPrice;
        //long computation
        ...
    }
}

冲动后:

class Bill
{
    public double FinalPrice()
    {
        return new PriceCalculator(this).compute();
    }
}
class PriceCalculator
{
    double primaryPrice;
    double secondaryPrice;
    double teriaryPrice;
    public PriceCalculator(Bill bill)
    {
        //initial
    }
    public double compute()
    {
        //computation
    }
}

9. Substitute Algorithm (替换算法)

解释:

有这么一个笑话:

某跨国日化公司, 肥皂生产线存在包装时可能漏包肥皂的问题, 肯定不能把空的肥皂盒卖给顾客, 于是该公司总裁命令组成了以博士牵头的专家组对这个问题进行攻关, 该研发团队使用了世界上最高精尖的技术 (如红外探测, 激光照射等), 在花费了大量美金和半年的时间后终于完成了肥皂盒检测系统, 探测到空的肥皂盒以后, 机械手会将空盒推出去. 这一办法将肥皂盒空填率有效降低至5%以内, 问题基本解决.

而某乡镇肥皂企业也遇到类似问题, 老板命令初中毕业的流水线工头想办法解决之, 经过半天的思考, 该工头拿了一台电扇到生产线的末端对着传送带猛吹, 那些没有装填肥皂的肥皂盒由于重量轻就都被风吹下去了...

这个笑话可以很好的解释 Substitute Algorithm, 对于函数中复杂的算法, 尽量想办法将这个算法简单化, 从而达到与之前同样甚至更好的效果.

改善代码设计 —— 优化物件之间的特性(Moving Features Between Objects)

1. Move Method (函数搬家)

解释:

如果 ClassA 的某个函数对 ClassB 有过多的依赖, 可以考虑将这个函数搬到 ClassB 中, 在 ClassA 的这个函数中直接调用 ClassB中这个函数的返回值.

这样做的好处是减少物件与物件之间的耦合度, 很多情况下这样做更利于进一步的重构.

冲动前:

class EmployeeSalary
{
    private double baseSalary = 15000.0;
    public double Salary(Employee employee)
    {
        return baseSalary + 10000 / employee.Level;
    }
    // other method with baseSalary
}
class Employee
{
    public int Level { get; set; }
}

冲动后:

class EmployeeSalary
{
    private double baseSalary = 15000.0;
    public double Salary(Employee employee)
    {
        return employee.Salary(baseSalary);
    }
    // other method with baseSalary
}
class Employee
{
    public int Level { get; set; }
    public double Salary(double baseSalary)
    {
        return baseSalary + 10000 / Level;
    }
}

2. Move Field (值域搬家)

解释:

有一天发现公司原来计算员工工资的方法不合适了, 比如不是所有的员工起薪 (baseSalary) 都是一万五, 我想把 baseSalary 搬到 Employee 这个物件中作为员工的一个属性.

这样做可使程序扩展性变得更好, 最明显的是我可以设置不同员工的起薪了.

冲动前:

class EmployeeSalary
{
    private double baseSalary = 15000.0;
    public double Salary()
    {
        double salary = baseSalary;
        //do some compution with salary
        return salary;
    }
}

冲动后:

class EmployeeSalary
{
    public double Salary(Employee employee)
    {
        double salary = employee.BaseSalary;
        //do some compution with salary
        return salary;
    }
}
class Employee
{
    public double BaseSalary { get; set; }
}

3. Extract Class (提炼类)

解释:

当某个物件做的事情过多, 这样的物件往往含有大量的字段, 属性和方法. 应该由两个或更多个物件来分担这些责任, 这时需要使用 Extract Class.

冲动前:

class Employee
{
    public double BaseSalary { get; set; }
    public double Level { get; set; }
 
    public double Salary()
    {
        double salary = BaseSalary;
        //do some complex compution with salary
        return salary;
    }
}

冲动后:

class EmployeeSalary
{
    public double Salary(Employee employee)
    {
        double salary = employee.BaseSalary;
        //do some complex compution with salary
        return salary;
    }
}
class Employee
{
    public double BaseSalary { get; set; }
    public double Level { get; set; }
 
    public double Salary()
    {
        EmployeeSalary salary = new EmployeeSalary();
        return salary.Salary(this);
    }
}

4. Inline Class (将类内联)

解释:

Inline Class 和 Extract Class 正好相反. 当一个物件没有做它应该做的事情, 还专门使用了另一个物件来协助它完成这个职责, 这时可以考虑使用 Inline Class.

如上面所示的例子, 如果我觉得 Employee 这个物件本身就应该实现 Salary 的计算工作, 而不是专门写一个 Salary 的计算物件来帮助它计算, 可以使用 Inline Class 将 Salary 内联到 Employee 中去, 也就是"冲动后"的代码重构成"冲动前"代码的样子.

5. Hide Delegate (隐藏委托关系)

解释:

试想这么一个情况: 有一个 Employee 类, 这个类中含有一个部门 (Department) 属性, 并且 Department 是一种类. 如果我想知道某职工所在部门的经理人是谁的时候, 我需要通过 xxEmployee.Department.Manger 来访问. 但这样做有个缺点是对于其它代码, Department 是 public 的, 其它代码能够访问到 Department 里的其它特性. 可以在 Employee 类中写一个 GetManger() 方法进行封装, 在调用的时候只需要xxEmployee.GetManger() 就行了.

冲动前:

class Department
{
    public string Manger { get; set; }
}
class Employee
{
    public Department Department { get; set; }
}

冲动后:

class Department
{
    public string Manger { get; set; }
}
class Employee
{
    private Department Department;

    public string GetManger()
    {
        return Department.Manger;
    }
}

6. Remove Middle Man (干掉中间人)

解释:

这一条与上一条 Hide Delegate 是相反的. 当我们要访问 Department 的其它很多特性时, 我们用 Hide Delegate 写了一条条简单的委托访问函数, 当这些函数多到几乎访问遍了 Department 里的内容, 可以考虑使用 Remove Middle Man 方法将这些访问函数干掉.

如上面的例子, 就是将"冲动后"的代码重构成"冲动前"代码的样子.

7. Introduce Foreign Method (引入外加函数)

解释:

Introduce Foreign Method 有很深的 C#3.0 中扩展方法的味道, 但扩展方法比 Introduce Foreign Method 好在: 扩展方法就好象是被扩展的那个类型自己的方法一样, 而 Introduce Foreign Method 的函数还需要传递这个类型的参数, 但其实编译器编译扩展方法后还是会把参数传进去的, 扩展方法只是一种语法糖.

它的主要目的是实现被调用的类中没有实现的功能, 注意在进行本项重构时, 如果引入一个外加函数, 这说明这个函数本应该在被调用的类中实现. 下面举一个简单到不能再简单的例子, 这个例子只是说明怎么使用 Introduce Foreign Method, 我并不是说 Int32类型就应该有一个 NextNum 的方法 , 并且实际中多数情况下这种重构用于引用类型

冲动前:

int num = 1;
//I want to get num's next
int nextNum = num + 1; 

冲动后:

int num = 1;
int nextNum = NextNum(num);

private static int NextNum(int arg)
{
    return arg + 1;
}

8. Introduce Local Extension (引入本地扩展)

解释:

如果我不想使用 Introduce Foreign Method, 我觉得它就本来应该实现某某功能, 如果被调用的类不是密封 (sealed) 的话, 可以自定义一个数据类型, 继承这个类, 在自己定义的数据类型中实现我想要它实现的功能, 这就是 Introduce Local Extension.

改善代码设计 —— 组织好你的数据(Composing Data)

1. Self Encapsulate Field (自封装值域)

解释:

大部分类 (class) 中都会有一些值域 (field), 随之还会有一些方法使用到了这些值域. "如果调用这些值域"这个问题分为两种观点: 1. 应该直接调用它们 2. 应该通过访问函数调用它们.

我觉得大部分情况下直接调用比较方便, 过多的访问函数还会造成类中的函数过多, 当然将来如果我觉得直接调用带来了一些问题, 写一个一个的访问函数也并不是很困难.

下面的例子主要说明如何给值域写一个访问函数, 并通过访问函数调用值域的值.

冲动前:

private string _userName, _password;

public bool IsValid()
{
    bool isValid = !(String.IsNullOrEmpty(_userName) &&
        String.IsNullOrEmpty(_password));
    return isValid;
}

冲动后:

private string _userName, _password;

public bool IsValid()
{
    bool isValid = !(String.IsNullOrEmpty(GetUserName()) &&
        String.IsNullOrEmpty(GetPassword()));
    return isValid;
}
private string GetUserName()
{
    return _userName;
}
private string GetPassword()
{
    return _password;
}

2. Replace Data Value with Object (以物件取代数据值)

解释:

如果你发现代码中有很多字段或者值域似乎都在描述某一样东西, 可以考虑将这些字段或者值域封装到一个类中, 用这个物件代替原先代码中繁杂的字段和值域.

冲动前:

class Order
{
    public string CustomerName { get; set; }
    public string CustomerAddress { get; set; }
    public int CreditLevel { get; set; }
    public int CustomerTel { get; set; }
    //...
}

冲动后:

class Order
{
    public Customer Customer { get; set; }
}
class Customer
{
    public string CustomerName { get; set; }
    public string CustomerAddress { get; set; }
    public int CreditLevel { get; set; }
    public int CustomerTel { get; set; }
    //...
}

3. Change Value to Reference (将值对象改为引用对象)

解释:

一个类中有时包含值对象作为它的字段或者属性, 如订单 Order 类中包含了一个 客户 customerName 的字段. 如果同一个客户有好几份订单, 那么每一份订单就都会保存一次客户的姓名. 如果这个客户的姓名改变了, 那么就需要更改每份个订单中的 customerName.

如果将 customerName 提取出去, 提炼一个 Customer 的类, 订单使用的是 Customer 实例的引用, 则只需更改实例中客户姓名的属性, 因为所有的订单都是引用的这个客户实例, 所以它们不需要作其它更改.

冲动前:

class Order
{
    private string _customerName;
    public Order(string customerName)
    {
        this._customerName = customerName;
    }
}

冲动后:

class Order
{
    private Customer _customer;
    public Order(Customer customer)
    {
        this._customer = customer;
    }
}

class Customer
{
    public string CustomerName { get; set; }
}

4. Change Reference to Value (将引用对象改为值对象)

解释:

如果给一个类传递引用类型变得不是你想要的效果, 比如我曾经做过一个类似于 MSN 的软件, 其中将人的头像, 昵称, 个性签名显示在一个自定义控件中, 然后在使用了 ListBox.Add(myControl), 结果我惊奇的发现最后好友列表里居然都是同一个 (最后一个) 人. 原来 ListBox.Add() 传递进去的都是引用的同一个 myControl 实例, 尽管代码貌似在动态的生成每一个人, 但其实都只修改了一个 myControl, 而 ListBox 仅仅了引用这一个对象. 后来我在代码中动态的生成不同对象的实例才解决了这个问题.

除了这样, 还可以在你的类中定义一个值对象, 每一次生成一个类的实例时, 类中值对象的控制权总是属于自己.

5. Replace Array with Object (用物件取代数组)

解释:

一个类中可能会包含很多字段, 你不能因为这些字段都是 string 类型, 就通通把它们放到一个 string[] 里, 在类的方法体中通过数组索引来获取你需要的值, 这样很难让别人记得你 string[0] 表示的是姓名, string[1] 表示的是他的住址...

下面的将展示前后代码可读性的差距.

冲动前:

string[] _person = new string[5];

public string GetName()
{
    return _person[0];
}
public string GetAdd()
{
    return _person[1];
}

冲动后:

private Person _person;

public string GetName()
{
    return _person.Name;
}
public string GetAdd()
{
    return _person.Address;
}

6. Duplicate Observed Data (复制被监视数据)

解释:

一个良好的系统应该事先业务逻辑 (Business Logic) 与用户界面 (User Interface) 分离, 如果没有这样做, 常见的比如在 Program.cs 堆叠了大量的代码, 业务逻辑与用户界面非常紧密的耦合在了一起. 实现业务逻辑与用户界面分离, 最重要的实现是数据的同步机制, Duplicate Observed Data 重构手段用来完成这项工作. 由于例子较长, 可以独立成篇, 在以后介绍观察者模式 (Observer Pattern) 的时候会搬出来讨论.

7. Change Unidirectional Association to Bidirectional (将单向关联改为双向)

解释:

如果两个 Class 都需要使用对方的特性, 但两者之间只有一条单向关联 (如只有调用方才能调用被调用的特性), 如果你想让被调用的那一方知道有哪些物件调用了自身, 这时就需要使用 Change Unidirectional Association to Bidirectional.

通常的做法是在被调用方的代码里定义一个被调用方类型的集合 (Collection), 如 ArrayList<T>, List<T>(推荐使用 List<T>), 在[调用方] 调用 [被调用方] 的时候, 在 [被调用方] 里的集合中也添加一下自己的引用.

8. Change Bidirectional Association to Unidirectional (将双向关联改为单向)

解释:

使用了Change Unidirectional Association to Bidirectional 之后, 如果需求变了, 发现双向关联是没有必要的, 这时就需要将双向关联改为单向, 也就是 Change Bidirectional Association to Unidirectional, 步骤与 Change Unidirectional Association to Bidirectional 相反.

9. Replace Magic Number with Symbolic Constant (用符号常量取代魔法数)

解释:

新手在写程序的时候, 往往不注重命名 (name), 他们对程序的要求也就是能正确运行即可. 在 IDE 中随便拖几个 Button, 也不重新命名, 于是在代码中出现了类似 Button1, Button2, Button3...之类的, 你无法光看代码就能想象得出 Button1 对应 UI 中的哪一个按钮. 同样的坏习惯出现在代码的字段, 临时变量的命名, 到处是 a, aa, aaa...

使用 Replace Magic Number with Symbolic Constant 吧! 如果你的 Button 是用于"发送", 那么你可以给它一个名字 —— btnSend; 如果你的一个 Int32型 变量用于表示一个人的年龄, 请给它一个名字 —— age.

10. Encapsulate Field (封装值域)

解释:

如果你的 class 中存在一个 public 字段, 请将它设置为属性.

面向对象的第一条原则就是封装 (encapsulation), 或者称之为数据隐藏 (Data Hiding), 如果一个字段想被外界访问到, 你应该将它设为属性, 在 C# 中设置属性比在 Java 中要方便许多, 因为 C# 编译器帮你做了一些额外工作.

冲动前:

public string _name;

冲动后:

public string Name { get; set; }

11. Encapsulate Collection (封装集合)

解释:

如果你的 class 中有一个方法需要返回多个值, 在类中有一个集合 (Collection) 暂时保持这些返回值, 有些情况下应当避免让 [调用端] 直接访问这个集合. 如果 [调用端] 修改了你集合的某一项, 而 [被调用端] 不知道自己的集合被修改了, 而另外的一些方法仍然在调用被修改后的集合, 这样可能会造成无法预料的后果.

Encapsulate Collection 建议你在 [被调用端] 将这个集合封装 (至少你将它的访问权限设置为 private) 起来, 有需要的话, 并提供修改这个集合的函数.

12. Replace Record with Data Class (用数据取代记录)

解释:

与第二条 Replace Data Value with Object 和第五条 Replace Array with Object 做法相似, 目的是提取类中的数据到一个描述记录 (Record) 的类中, 方便以后可以对这个类进行扩展.

13. Replace Subclass with Fields (用值域取代子类)

解释:

子类建立在父类之上再增加新的功能或者重载父类可能的变化行为, 有一种变化行为 (variant behavior) 成为常量函数 (constant method), 他们会返回一个硬编码 (hard-coded) 值, 这个值一般有这样的用途: 让不同的父类中的同一个访问函数返回不同的值, 你可以在父类中将访问函数声明为抽象函数, 并在不同的不同的子类中让它返回不同的值. 但如果子类中只有返回常量的函数, 没有其它的作用, 往往子类并没有太大的存在价值.

你可以在父类中设计一些与子类返回值相应的值域, 再对父类做一些其它修改, 从而可以消除这些子类, 好处是避免不必要的子类带来的复杂性, 这就是 Replace Subclass with Fields.

冲动前:

class Person

{

    protected abstract bool isMale();

    protected abstract char Code();

    //...

}

class Male : Person

{

    protected override bool isMale()

    {

        return true;

    }

    protected override char Code()

    {

        return 'M';

    }

}

class Female : Person

{

    protected override bool isMale()

    {

        return false;

    }

    protected override char Code()

    {

        return 'F';

    }

}

冲动后:

class Person

{

    public bool IsMale { get; set; }

    public char Code { get; set; }

 

    public Person(bool isMale, char code)

    {

        this.IsMale = IsMale;

        this.Code = code;

    }

    public Person Male()

    {

        return new Person(true, 'M');

    }

    public Person Female()

    {

        return new Person(false, 'F');

    }

    //...

}

调用的时候这样: Person Create_Chen = Person.Male();

改善代码设计 —— 简化条件表达式(Simplifying Conditional Expressions)

1. Decompose Conditional (分解条件式)

解释:

"复杂的条件逻辑" 是导致复杂性上升最常见的地方, "条件表达式中堆积的计算过程", "条件式表达得不简洁"等等都是造成复杂的原因. Decompose Conditional 用于将这些复杂的元素从条件表达式中分离出去, 仅在条件表达式中调用简洁的函数.

这样做带来的直接好处是减少重复, 而且代码的可读性提高了.

冲动前:

if (date.After(SUMMER_START) && date.Before(SUMMER_END))
    charge = days * _price + _summerServiceTip;
else
    charge = days * _price;

冲动后:

if (date.IsSummer())
    charge = SummerCharge(days);
else
    charge = WinterCharge(days);

2. Consolidate Conditional Expression (合并条件式)

解释:

如果代码中有一连串的 if 检查语句, 检查语句中的条件不相同, 但最终的行为都是一样的. 对于这样的情况, 应该使用 "逻辑与" 和 "逻辑或" 将它们合并成一个条件表达式, 如果嫌这个合并条件后的表达式太罗嗦, 你还可以将这个表达式提取成一个函数.

冲动前:

if (computer.CPU != "T6670")
    return false;
if (computer.RAM != "1.00GB")
    return false;
if (computer.SytemType != "32-bit Operating System")
    return false;
//other compution

冲动后:

if ((computer.CPU != "T6670") || (computer.RAM != "1.00GB") || (computer.SytemType != "32-bit Operating System"))
    return false;
//other compution

你还可以将 if 里长长的条件表达式提取成一个方法, 如 bool IsStandard(Computer computer), 这样在原来的 if 语句中只需要调用这个方法即可

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

解释:

如果条件式的每个分支上都有同样一段代码, 如果这段代码对条件分支在执行这段代码后执行后面的代码没有影响, 请将这段代码移到条件式的外面.

冲动前:

if (date.IsSummer())
{
    charge = days * _price + _summerServiceTip;
    PrintDetail();
}
else
{
    charge = days * _price;
    PrintDetail();
}
//other compution

冲动后:

charge = days * _price;
if (date.IsSummer())
    charge += _summerServiceTip;

PrintDetail();
//other compution

4. Remove Control Flag (移除控制标志)

解释:

很多代码里执行一个 for 或者 while 循环用于寻找一个数组里特点的元素, 很多时候在循环开头就执行控制标志的检查, 满足检查条件就继续执行循环查找元素. 如果这一次查找到了想要的元素, 就更改控制标志的值, 让它下次被检查出不符合条件, 从而循环结束.

这并不是一个很好的做法, 使用诸如 break, continue, return 语句会让你的代码意图更加直接, 更加明显.

冲动前:

for (int i = 0; i < suspects.Length; i++)
{
    if (!found)
    {
        if (suspects[i].Name == guessName)
        {
            sendAlert();
            found = true;
        }
    }
}

冲动后:

for (int i = 0; i < suspects.Length; i++)
{
    if (suspects[i].Name == guessName)
    {
        sendAlert();
        break;
    }
}

5. Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件式)

解释:

许多程序员觉得函数应该只有一个出口 (return), 结果导致函数中的条件逻辑 (Conditional Logic) 本来完全可以终止下面的代码继续执行 (因为没有必要), 结果却只在函数最后 return, 使人难以看清程序的执行路径.

Replace Nested Conditional with Guard Clauses 用来解决这个问题, 它能带给代码可读性的提高, 还有性能上一点点的优化.

冲动前:

double charge;

if (IsSummer(date))
{
    //...
    SummerCharge(charge);
}
else
{
    //...
    WinterCharge(charge);
}
return charge;

冲动后:

double charge;

if (IsSummer(date))
{
    //...
    SummerCharge(charge);
    return charge;
}
else
{
    //...
    WinterCharge(charge);
    return charge;
}

6. Replace Conditional with Polymorphism (以多态取代条件)

解释:

这条重构手法常常用于消除函数中长长的 switch-case 语句. 虽然写一个个的子类比较繁琐, 但随着项目的进行, 好处会体现出来的.

冲动前:

public double Salary(Employee employee)
{
    switch(employee.Type):
    {
        case Employee.Engineer
        {
            //...
        }
        case Employee.Salesman:
        {
            //...
        }
        //...
        default:
        {
            //...
        }
    }
}

冲动后:

    public abstract double Salary(Employee employee);
class Engineer : Employee
{
    public override double Salary(Employee employee)
    {
        //...
    }
}
class Salesman : Employee
{
    public override double Salary(Employee employee)
    {
        //...
    }
}

7. Introduce Null Object (引入 Null 对象)

解释:

如果代码中出现很多判断某值是不是为 null , 诸如 if (XXX != null) {//...} else {//...} 这样的情况, 可以考虑使用 Introduce Null Object 重构手段. 这个手段其实并不难以理解, 可以简单理解成为某一个物件在为空状态下设定默认的值域和行为, 可以建立一个子类, 继承父类中需要对 "为空" 情况下做出响应的虚函数或者值域. 它是 Null Object 设计模式里的最基础最常见的手段.

8. Introduce Assertion (引入断言)

解释:

严格上说, 引入断言并不是为了简化条件表达式, 它主要是为了代替条件表达式上面的注释, 通常这样的注释用来解释下面的条件表达式是基于什么样的假设之上的. 通常经过一系列的测试, 发现所写的断言在任何情况下都是正确的, 在系统发布的时候可以把它们全部删除掉.

在 C# 中引入断言使用 Debug.Assert() 方法, 如果一切假设都是正确的, 则代码会顺利的进行.

冲动前:

//index should between 0 to 10
return (customers[index] == "James") ? true : false;

冲动后:

Debug.Assert((index>=0)&&(index <= 10), "Error", "index should between 0 to 10");
return (customers[index] == "James") ? true : false;

如果断言错误, 在运行的时候会有一个消息框给予错误信息的提示.

改善代码设计 —— 简化函数调用(Making Method Calls Simpler)

1. Parameterize Method (令函数携带参数)

解释:

"令函数携带参数" 并不是简单的让你在函数里加上参数, 如果函数里需要某个参数, 我们谁都会加上它. 你可能发现这样的几个函数: 它们做着类似的事情, 只是因为极少的几个值导致函数的策略不同, 这时可以使用 Parameterize Method 消除函数中那些重复的代码了, 而且可以用这个参数处理其它更多变化的情况.

下面有一个非常简单的例子.

冲动前:

public double FivePercentRaise()
{
    _salary *= 1.05;
    return _salary;
}
public double TenPercentRaise()
{
    _salary *= 1.10;
    return _salary;
}

冲动后:

public double Raise(int percent)
{
    _salary *= (1 + percent / 100);
    return _salary;
}

2. Replace Parameter with Explicit Methods (用明确的函数取代参数)

解释:

Replace Parameter with Explicit Methods 与 Parameterize Method 是相反的操作. 如果函数内根据不同的参数作出了不同的行为, 那么可以考虑使用明确的函数取代参数. 不过这一条很少用到, 很少出现类似下面例子的垃圾代码, 建议使用 C# 里的属性, 即使是"冲动后"的代码, 在实际中也不是很多见.

冲动前:

public void Set(string name, int value)
{
    if(name.Equals("height"))
    {
        _height = value;
        return;
    }
    if(name.Equals("width"))
    {
        _width = value;
        return;
    }
}

冲动后:

public void SetHeight(int height)
{
    _height = height;
}
public void SetWidth(int width)
{
    _width = width;
}

3. Preserve Whole Object (保持对象完整)

解释:

你从某个物件中取出若干值, 将这些值作为参数传递给某个函数, 为何不考虑下把整个物件作为传递进去呢? 当然具体情况具体分析, 有时你确实需要按你原来做的那样做.

冲动前:

int height = rectangle.Height;
int width = rectangle.Width;
int area = CalculateArea(height, width);

冲动后:

int area = CalculateArea(rectangle);

4. Replace Parameter with Methods (以函数取代参数)

解释:

如果函数的某个参数值可以通过方法直接获得, 这种情况下, 很多时候函数不应该通过参数来获取该值, 应该在函数里直接通过方法获取需要的值.

冲动前:

double basePrice = basePrice * num;
double discount = GetDiscount();
double charge = DiscountPrice(basePrice, discount);

冲动后:

double basePrice = basePrice * num;
double charge = DiscountPrice(basePrice);

5. Introduce Parameter Object (引入参数对象)

解释:

你是否发现有几个参数总是同时的出现在一些函数的参数列表里? 这几个参数有一个响亮的臭名 —— 数据泥团(Data Clump), Introduce Parameter Object 让你将数据泥团封装成一个对象, 从而在原先的函数直接传入整个对象即可, 以后还可以对这个对象进行扩展.

我不主张对于所有的数据泥团都这么做, 哪怕这个数据泥团出现过几十次上百次, 如果数据泥团的各个数据直接的联系并不是那么紧密, 它们不能用一个物件笼统的包含它们, 这样的情况下"引入参数对象"可能并不是很适合.

6. Remove Setting Method (移除设值函数)

解释:

这一条没什么好说的, 如果你的类中某个值域只应该在对象初始化时被设值, 那么类中就不应该再有关于这个值域的设值函数, 应该通通把它们删掉.

7. Hide Method (隐藏某个函数)

解释:

这一条也没什么好说的, 如果你的类中某个函数只可能用于类本身的函数调用, 其它类从来没有用到过, 最好将这个函数的访问权限设置为 private. 当然这一步要谨慎, 现在没有被其它类调用过, 不代表以后不会!

8. Replace Constructor with Factory Method (用工厂函数取代构造函数)

解释:

"以工厂函数取代构造函数" 让我想起了 "以多态取代条件", 如果你在创建对象的时候不是仅仅对它做简单的构造, 例如你构造一个 Employee 对象, Employee 可能是工程师, 销售员, 也有可能是设计师, 应该根据不同的类别的职工创建 Employee 对象.

9. Encapsulate Downcast (封装向下转型动作)

解释:

要找到需要进行 Encapsulate Downcast 重构的函数并不困难, 如果你的函数返回的是 object 类型, 在别的地方调用这个函数获取一个 object 之后还要进行一下转型(如将这个 object 转成 int 类型), 你应该在函数里封装一下转型的操作.

冲动前:

public object LastPerson()
{
    return _persons.LastElement();
}

冲动后:

public Person LastPerson()
{
    return (Person)_persons.LastElement();
}

10. Replace Error Code with Exception (用异常取代错误码)

解释:

你的函数返回的是一个 int 型, 你约定返回 1 代表这个函数运行正常, 返回 –1 代表某个地方出错了, 错误码就是指这里的 "1" 和 "-1". 你想想你的函数返回的是错误码, 在调用这个函数的时候极有可能可能还需要判断你返回的是什么错误码, 如果你收到的是 –1 就停止程序的运行, 书中形容这样的做法很有趣: 就好像因为错过一班飞机而自杀一样, 如果真那么做, 哪怕我是猫, 我的九条命 (注: 猫生命力比较强) 也早就赔光了.

别忘了 C# 里的异常处理, 这是一个很重要的概念, 是时候使用它们了.

11. Replace Exception with Test (用测试取代异常)

解释:

"异常处理" 是一个很不错的功能, 但不要滥用, 对于肯定不会出现异常的代码块就不要将它们放入 try...catch 中.

对于在一定条件下代码会抛出异常的某些情况, 建议使用 Replace Exception with Test, 你可以在可能抛出异常的代码前先测试一下运行正常的条件是否满足, 满足之后再运行, 不满足的话则是另一种运行方案, 通过这样可以代替 try...catch 的使用.

冲动前:

try
{
    return _persons[index];
}
catch (IndexOutOfRangeException e)
{
    //...
}
//...

冲动后:

if (index < _persons.Length)
    return _persons[index];
//...

改善代码设计 —— 处理概括关系(Dealing with Generalization)

1. Pull Up Field (提升值域)

解释:

如果发现每个子类都拥有相同的某个值域, 那么使用 Pull Up Field 将这个值域提升到父类中去.

冲动前:

冲动后:

2. Pull Up Method (提升函数)

解释:

如果每个子类都有相同的某个函数, 这个函数做同样的事情, 而且结果也相同, 那么使用 Pull Up Method 将这个函数提升到父类中去.

冲动前:

冲动后:

3. Pull Up Constructor Body (提升构造函数)

解释:

特别要注意每个子类中重复的代码, 如果可能的话尽量将它们提炼成方法并搬到父类中去. 对于子类的构造函数, 我们需要找出相同的部分, 用这些相同的部分组成父类的构造函数.

如下面的例子, 如果不光 Salesman, 还有 Engineer 等等类别的员工在构造他们的时候都需要 name 和 level 属性, 可以考虑使用 Pull Up Constructor Body 将设置这两个属性提升到父类的构造函数中去.

冲动前:

class Employee
{
    public string Name { get; set; }
    public int Level { get; set; }
    //...
}
class Salesman : Employee
{
    public string Hobby { get; set; }

    public Salesman(string name, int level, string hobby)
    {
        this.Name = name;
        this.Level = level;
        this.Hobby = hobby;
    }
    //...
}
//...

冲动后:

class Employee
{
    public string Name { get; set; }
    public int Level { get; set; }
    public Employee(string name, int level)
    {
        this.Name = name;
        this.Level = level;
    }
    //...
}
class Salesman : Employee
{
    public string Hobby { get; set; }
    public Salesman(string name, int level, string hobby):base(name,level)
    {
        this.Hobby = hobby;
    }
    //...
}
//...

4. Push Down Method (降低函数)

解释:

父类里有某个函数只与一部分子类有关, 并不是与所有的子类都有关, 使用 Push Down Method 重构手段将这些函数放到使用它们的子类中去, 而不要放到父类中.

冲动前:

冲动后:

5. Push Down Field (降低值域)

解释:

与 Push Down Method 描述的问题类似, 父类中如果某个值域并不是对于每个子类都有用的, 应该把它放到需要它的子类中去.

6. Extract Subclass (提炼子类)

解释:

我们产生了类的一些实例, 但并不是每个实例都用得到类中所有的特性, 往往这是类中功能设计过多的原因造成的. 尝试从这个类中提炼出一些子类, 子类中的功能应该划分得很明确.

7. Extract Superclass (提炼父类)

解释:

如果你发现有两个类, 他们有很多相同的特性, 尝试找出两个类中相同的特性, 如果你能找到一个合适的理由让这两个类继承自一个父类, 从而你可以提炼出这个父类, 父类中包含那两个类中相同的部分.

8. Extract Interface (提炼接口)

解释:

类与类之间经常会相互调用, 比如 ClassA 的某个函数里需要 ClassB 里的某个值域或者某个函数的返回值, 因此我将整个 ClassB 作为参数传递给 ClassA 的这个函数, 这意味着 ClassA 的这个函数能够调用 ClassB 里所有的功能, 可不可以给 ClassA 的这个函数划定一个特定的职能呢? 让它只能做某些事情, 而避免其它 "越权行为". 有句话常被人说起 —— 使用接口来降低耦合性, 这就是 Extract Interface 的功劳.

这条重构手段经常被使用到, 主要解决类对另一个类的依赖问题, 降低了耦合性.

冲动前:

class Xml
{
    public void Read()
    {
        //...
    }
    public void Translate()
    {
        //...
    }
    //some other methods
}
class WeatherService
{
    public string GetWeather(Xml xml)
    {
        xml.Read();
        xml.Translate();
        //other code, but without xml object
    }
    //some other methods
}

冲动后:

interface IOperation
{
    void Read();
    void Translate();
}
class Xml : IOperation
{
    public void Read()
    {
        //...
    }
    public void Translate()
    {
        //...
    }
    //some other methods
}
class WeatherService
{
    public string GetWeather(IOperation operation)
    {
        operation.Read();
        operation.Translate();
        //other code, but without xml object
    }
    //some other methods
}

注意代码高亮的那行已经变成调用接口, 而不是仅依赖调用 Xml 类的实例, 对于单个类实现这样的接口并不是很有价值, 如果很多类都实现了同样的接口, 这将是很有用的事情.

9. Collapse Hierarchy (去掉不必要的继承关系)

解释:

庞大的继承体系很容易变得复杂, 理清父类与子类它们各自的职能是非常重要的, 你很有可能会发现不必要的子类, 那么使用 Pull Up Field 和 Pull Up Method 将它干掉.

10. Replace Inheritance with Delegation (用委派取代继承)

解释:

如果你想让 ClassA 使用某个类 (如 ClassB) 的某个函数, 就让 ClassA 继承自 ClassB, 这将是一个多么糟糕的设计! 你可以在 ClassA 中包含一个 ClassB 的值域, 通过这个值域调用你需要的函数, 这个值域就是"委派", 而你这时可以去掉 ClassA 和 ClassB 之间不该存在的继承关系了.

11. Replace Delegation with Inheritance (用继承代替委派)

解释:

这一条与 Replace Inheritance with Delegation 正好相反, 如果你需要使用委派中的所有函数, 这时你就应该想想它们之间是不是存在继承关系.




WEB应用程序UI模版代码编写
C# 编码规范和编程好习惯
什么是防御性编程
善于防守-健壮代码的防御性
Visual C++编程命名规则
JavaScript程序编码规范
更多...   


设计模式原理与应用
从需求过渡到设计
软件设计原理与实践
如何编写高质量代码
单元测试、重构及持续集成
软件开发过程指南


某全球知名通信公司 代码整洁
横河电机 如何编写高质量代码
某知名金融软件服务商 代码评审
东软集团 代码重构
某金融软件服务商 技术文档
中达电通 设计模式原理与实践
法国电信 技术文档编写与管理
更多...   
 
 
 
 
 
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号