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

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.