Skip to content

《重构》6. 第一组重构

🏷️ 《重构》

第 5 章 是介绍之后几章重构手法的说明,就不单独写一篇博客了。

从第 6 章开始直到最后都是在介绍各种重构手法的。每个重构手法包含 名称速写(Skeetch)动机(motivation)做法(mechanics)范例 (examples) 5 个部分。这里只记录一下 速写(Skeetch) ,速写是用来帮助回忆重构手法的,具体的重构用途和重构的具体步骤这里就不介绍了,还是推荐大家买实体书来看。

第 6 章:第一组重构 中介绍的几种重构方法,可以说是最常见的,所有的开发人员应该都曾经使用到过,只是可能没有这么系统的概念,也没有什么具体的重构步骤。

6.1 提炼函数(Extract Function)

曾用名提炼函数(Extract Method)
反向重构内联函数

重构前

csharp
void PrintOwing(Invoice invoice)
{
    PrintBanner();
    decimal outstanding = CalculateOutstanding();

    // print details
    Console.WriteLine($"name: {invoice.Customer}");
    Console.WriteLine($"amount: {outstanding}");
}

这个重构方法在 Visual Studio 中支持自动化重构,比较常用的几种重构方法在 Visual Studio (我这里使用的是 2019 社区版)中都有较好的支持。自动化重构的代码安全性也比较有保障。

操作步骤:选中需要提炼的代码,右键选择 快速操作和重构提取本地函数 ,之后输入新的方法名,然后回车就可以了。

重构后

csharp
void PrintOwing(Invoice invoice)
{
    PrintBanner();
    decimal outstanding = CalculateOutstanding();
    PrintDetails(invoice, outstanding);

    static void PrintDetails(Invoice invoice, decimal outstanding)
    {
        Console.WriteLine($"name: {invoice.Customer}");
        Console.WriteLine($"amount: {outstanding}");
    }
}

6.2 内联函数(Inline Function)

曾用名内联函数(Inline Method)
反向重构提炼函数

重构前

csharp
int GetRating(Driver driver)
{
    return MoreThanFiveLateDeliveries(driver) ? 2 : 1;
}

private bool MoreThanFiveLateDeliveries(Driver driver)
{
    return driver.NumberOfLateDeliveries > 5;
}

这个是 6.1 提炼函数(Extract Function) 的反向重构,不过 Visual Studio 不支持这种的自动化重构。

重构后

csharp
int GetRating(Driver driver)
{
    return driver.NumberOfLateDeliveries > 5 ? 2 : 1;
}

6.3 提炼变量(Extract Variable)

曾用名引入解释性变量(Introduce Explaining Variable)
反向重构内联变量

重构前

csharp
double GetRating(Order order)
{
    return order.Quantity * order.ItemPrice
        - Math.Max(0, order.Quantity - 500) * order.ItemPrice * 0.05
        + Math.Min(order.Quantity * order.ItemPrice * 0.1, 100);
}

操作步骤:选中需要提炼的代码 order.Quantity * order.ItemPrice,右键选择 快速操作和重构为出现的所有 “...” 引入本地,输入新的变量名后回车。然后依次修改其它需要提炼变量的地方。

重构后

csharp
double GetRating(Order order)
{
    double basePrice = order.Quantity * order.ItemPrice;
    double quantityDiscount = Math.Max(0, order.Quantity - 500) * order.ItemPrice * 0.05;
    double shipping = Math.Min(basePrice * 0.1, 100);
    return basePrice - quantityDiscount + shipping;
}

6.4 内联变量(Inline Variable)

曾用名内联临时变量(Inline Temp)
反向重构提炼变量

重构前

csharp
var basePrice = anOrder.BasePrice;
return basePrice > 1000;

双击选中需要内联的变量(变量声明的地方)后,右键选择 快速操作和重构内联临时变量 就可以了。

重构后

csharp
return anOrder.BasePrice > 1000;

6.5 改变函数声明(Change Function Declaration)

别名函数改名(Rename Function)
曾用名函数改名(Rename Method)
曾用名添加参数(Add Parameter)
曾用名移除参数(Remove Parameter)
别名修改签名(Change Signature)

重构前

csharp
double Cricum(double radius)
{
    return 2 * Math.PI * radius;
}

操作步骤:右键方法名,选择 重命名 ,输入新的方法名按回车。

重构后

csharp
double Cricumference(double radius)
{
    return 2 * Math.PI * radius;
}

另外,本节还讲了通过添加函数重载来安全的添加/移除参数的重构方法。由于 JavaScript 不支持函数重载,所以采用的是新建一个别的名称的方法,最后再统一将函数替换为原来的方法名。

6.6 封装变量(Encapsulate Variable)

曾用名自封装字段(Self-Encapsulate Field)
曾用名封装字段(Encapsulate Field)

重构前

csharp
public (string FirstName, string LastName) DefaultName = (FirstName: "JiaJia", LastName: "Liu");

操作步骤:右键字段名,选择 快速操作和重构封装字段:“DefaultName”(但仍使用字段)

重构后

csharp
private (string FirstName, string LastName) defaultName = (FirstName: "JiaJia", LastName: "Liu");

public (string FirstName, string LastName) DefaultName { get => defaultName; set => defaultName = value; }

上面的代码实际上可以省略如下形式,以去除 defaultName 临时变量:

csharp
public (string FirstName, string LastName) DefaultName { get; set; } = (FirstName: "JiaJia", LastName: "Liu");

将变量封装为属性有什么意义呢?我们来看一下如下用法:

csharp
obj.DefaultName.FirstName = "Jiajia";

上面的代码其实是赋值语句,重构前的代码上面的方式是可以正常赋值的,而却通过查找 DefaultName 变量的应用时仍然显示为读取,而不是写入。这样不仅会产生一种概念上的混淆,而且很容易发生未知的改动从而导致 bug。在第一章就提到过:可变状态会很快变成烫手的山芋

重构后的代码再通过上述方式修改属性值时会报如下异常:

CS1612 无法修改“Refactoring_6_6.DefaultName”的返回值,因为它不是变量

此时如果要修改 DefaultName 属性的值,只能通过它的 set 方法。

csharp
var newName = obj.DefaultName;
newName.FirstName = "Jiajia";
obj.DefaultName = newName;

这样就可以很容易的查找到 DefaultName 变量所有的写入操作了。

如果需要使用更严厉的约束(如字段不允许修改),可以将 DefaultName 字段的类型修改为只读(readonly)的自定义结构。

csharp
internal readonly struct Name
{
    public string FirstName { get; }
    public string LastName { get; }

    public Name(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

此时如果直接修改字段属性,编译时会报 CS0200 错误。

csharp
var newName = obj.DefaultName;
newName.FirstName = "Jiajia";

CS0200 无法为属性或索引器“Name.FirstName”赋值 - 它是只读的

此时若要修改 DefaultName 只能通过如下方式:

csharp
obj.DefaultName = new Name("Jiajia", obj.DefaultName.LastName);

上面的两种情况都是值类型,如果 DefaultName 是引用类型,即使修改为属性也无法避免数据通过 get 方法被修改。此时可以通过将 get 方法修改为返回字段值的拷贝来保护数据。

6.7 变量改名(Rename Variable)

重构前

csharp
int a = height * width;

操作步骤和修改方法名一样。

重构后

csharp
int area = height * width;

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

重构前

csharp
interface Refactoring_6_8
{
    decimal AmountInvoiced(DateTime startDate, DateTime endDate);

    decimal AmountReceived(DateTime startDate, DateTime endDate);

    decimal AmountOverdue(DateTime startDate, DateTime endDate);
}

这个 VS 不支持自动化重构,不过重构的步骤也比较简单。

  • 如果没有合适的数据结构,就创建一个。
  • 测试
  • 使用 改名函数声明 添加一个参数重载。
    6.5 改名函数声明 中添加参数其实采用的类似重载的方法,但本节中却采用的增加参数的方式。
    C# 支持重载,个人觉得采用新建一个重载方法的方式可能更方便些。另外还可以通过添加 [Obsolete] 特性,将旧的函数标记为已过期。
  • 测试
  • 调整所有调用者。每修改一处,执行测试。

重构后

csharp
interface Refactoring_6_8
{
    decimal AmountInvoiced(DateRange aDateRange);

    decimal AmountReceived(DateRange aDateRange);

    decimal AmountOverdue(DateRange aDateRange);
}

6.9 函数组合成类(Combine Functions into Class)

重构前

csharp
void Base(Reading aReading) { }

void TaxableCharge(Reading aReading) { }

void CalculateBaseCharge(Reading aReading) { }

重构后

csharp
public class Reading
{
    void Base() { }

    void TaxableCharge() { }

    void CalculateBaseCharge() { }
}

6.10 函数组合成变换(Combine Functions into Transform)

重构前

csharp
decimal Base(Reading aReading) {
    decimal result = decimal.Zero;
    // do something
    return result;
}

decimal TaxableCharge(Reading aReading)
{
    decimal result = decimal.Zero;
    // do something
    return result;
}

重构后

csharp
(decimal BaseCharge, decimal TaxableCharge) EnrichReading(Reading argReading) {
    var aReading = DeepClone(argReading);
    return (Base(aReading), TaxableCharge(aReading));
}

DeepClone 方法用来实现对象的深拷贝,关 Deep CloneStack Overflow 有个回复比较多的问题,常用的几种都有提及。我在 这篇博客 里也做过性能比较,还是通过反射的实现性能比较高,实现如下。

csharp
public static T DeepClone<T>(T obj)
{
    if (obj is string || obj.GetType().IsValueType) return obj;
    object result = Activator.CreateInstance(obj.GetType());
    var fields = obj.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
    foreach (var field in fields)
    {
        try { field.SetValue(result, DeepClone(field.GetValue(obj))); }
        catch { }
    }
    return (T)result;
}

函数组合成变换6.9 函数组合成类 比较类似,两种重构手法都很有用,一个重要的区别在于如果代码中对源数据做了修改,那么使用类要好的多。

6.11 拆分阶段(Split Phase)

重构前

csharp
var orderData = Regex.Split(orderString, @"\s+");
var productPrice = priceList[orderData[0].Split("-")[1]];
var orderPrice = int.Parse(orderData[1]) * productPrice;

重构后

csharp
var orderRecord = ParseOrder(orderString);
var orderPrice = Price(priceList, orderRecord);

static (string ProductID, int Quantity) ParseOrder(string orderString)
{
    string[] values = Regex.Split(orderString, @"\s+");
    return (values[0].Split("-")[1], int.Parse(values[1]));
}

static decimal Price(Dictionary<string, decimal> priceList, (string ProductID, int Quantity) orderRecord)
{
    return orderRecord.Quantity * priceList[orderRecord.ProductID];
}

这里体验了一把通过 Visual Studio 自动化重构的便捷。

  • 选中 orderData 变量的赋值副本,使用 提取本地函数 快速重构,将行的本地函数命名为 ParseOrder

    csharp
    var orderData = ParseOrder(orderString);
    var productPrice = priceList[orderData[0].Split("-")[1]];
    var orderPrice = int.Parse(orderData[1]) * productPrice;
    return orderPrice;
    
    static string[] ParseOrder(string orderString)
    {
        return Regex.Split(orderString, @"\s+");
    }
  • ParseOrder 方法修改为如下形式。

    csharp
    static (string ProductID, int Quantity) ParseOrder(string orderString)
    {
        string[] values = Regex.Split(orderString, @"\s+");
        return (values[0].Split("-")[1], int.Parse(values[1]));
    }
  • 修改 ParseOrder 方法后,调用 orderData 变量的地方会报错,修改为使用返回值中的对应属性,并将 orderData 变量名使用 重命名 修改为 orderRecord

    csharp
    var orderRecord = ParseOrder(orderString);
    var productPrice = priceList[orderRecord.ProductID];
    var orderPrice = orderRecord.Quantity * productPrice;
  • 选择 productPrice 变量应用 内联临时变量 快速重构。

    csharp
    var orderRecord = ParseOrder(orderString);
    var orderPrice = orderRecord.Quantity * priceList[orderRecord.ProductID];
  • 选择 orderPrice 变量的赋值部分 orderRecord.Quantity * priceList[orderRecord.ProductID] ,应用 提取本地函数 重构,并将新的方法命名为 Price

    csharp
    var orderRecord = ParseOrder(orderString);
    var orderPrice = Price(priceList, orderRecord);
    
    static decimal Price(Dictionary<string, decimal> priceList, (string ProductID, int Quantity) orderRecord)
    {
        return orderRecord.Quantity * priceList[orderRecord.ProductID];
    }
  • 最后完整代码如下

    csharp
    var orderRecord = ParseOrder(orderString);
    var orderPrice = Price(priceList, orderRecord);
    
    static (string ProductID, int Quantity) ParseOrder(string orderString)
    {
        string[] values = Regex.Split(orderString, @"\s+");
        return (values[0].Split("-")[1], int.Parse(values[1]));
    }
    
    static decimal Price(Dictionary<string, decimal> priceList, (string ProductID, int Quantity) orderRecord)
    {
        return orderRecord.Quantity * priceList[orderRecord.ProductID];
    }

因为和书上的示例使用的语言和代码不太一样,我也不确定这种步骤是不是标准做法。不过其中只有一部分改动比较大的地方是手动修改的,其它的都是通过自动化重构实现的,比较便捷,安全性也比较高。

引用

  1. 《重构:改善既有代码的设计》 -- 马丁·福勒(Martin Fowler