佳佳的博客
Menu
首页
《重构》 6. 第一组重构
Posted by
佳佳
on 2020-05-08
IT
《重构》
读书笔记
<!-- # 《重构》 6. 第一组重构 --> <!-- refactoring-06-the-first-set-of-refactorings --> 第 5 章 是介绍之后几章重构手法的说明,就不单独写一篇博客了。 从第 6 章开始直到最后都是在介绍各种重构手法的。每个重构手法包含 *名称*、*速写(Skeetch)*、*动机(motivation)*、*做法(mechanics)*、*范例(examples)* 5 个部分。这里只记录一下 *速写(Skeetch)* ,速写是用来帮助回忆重构手法的,具体的重构用途和重构的具体步骤这里就不介绍了,还是推荐大家买[实体书][1]来看。 第 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 Clone* 在 [Stack Overflow][3] 有个回复比较多的问题,常用的几种都有提及。我在 [这篇博客][4] 里也做过性能比较,还是通过反射的实现性能比较高,实现如下。 ```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. [《重构:改善既有代码的设计》][1] -- 马丁·福勒(*Martin Fowler*) --- [1]: https://union-click.jd.com/jdc?e=&p=AyIGZRhaEwAQBFUZXBIyEgRREl4QChs3EUQDS10iXhBeGlcJDBkNXg9JHU4YDk5ER1xOGRNLGEEcVV8BXURFUFdfC0RVU1JRUy1OVxUBFg5QHlMcMloDXR8JHXtmYiNlWEBFeV1UUjxnS0QLWStaJQITBlYbXB0LFQJlK1sSMkBpja3tzaejG4Gx1MCKhTdUK1sRCxQBVxtdFAcTB1crXBULIloNXwZBXUReEStrJQEiN2UbaxYyUGlUE1xGBhFQBR5bFVUXVAdOU0dWGwFdTwtAAkZTVE9fRTIQBlQfUg%3D%3D (《重构:改善既有代码的设计》) [2]: https://docs.microsoft.com/zh-cn/dotnet/core/testing/unit-testing-with-dotnet-test (使用 dotnet test 和 xUnit 在 .NET Core 中进行 C# 单元测试) [3]: https://stackoverflow.com/questions/78536/deep-cloning-objects (Deep cloning objects) [4]: https://www.liujiajia.me/2019/11/15/compare-performance-of-two-deep-copy-methods-in-csharp (C# 两种深拷贝方法效率比较)
版权声明:原创文章,未经允许不得转载。
https://www.liujiajia.me/2020/5/8/refactoring-06-the-first-set-of-refactorings
“Buy me a nongfu spring”
« 《重构》 7. 封装
Bootstrap下拉菜单第一次点击无反应 »
昵称
*
电子邮箱
*
回复内容
*
(回复审核后才会显示)
提交
目录
AUTHOR
刘佳佳
江苏 - 苏州
软件工程师