《重构》6. 第一组重构
🏷️ 《重构》
第 5 章 是介绍之后几章重构手法的说明,就不单独写一篇博客了。
从第 6 章开始直到最后都是在介绍各种重构手法的。每个重构手法包含 名称、速写(Skeetch)、动机(motivation)、做法(mechanics)、范例 (examples) 5 个部分。这里只记录一下 速写(Skeetch) ,速写是用来帮助回忆重构手法的,具体的重构用途和重构的具体步骤这里就不介绍了,还是推荐大家买实体书来看。
第 6 章:第一组重构 中介绍的几种重构方法,可以说是最常见的,所有的开发人员应该都曾经使用到过,只是可能没有这么系统的概念,也没有什么具体的重构步骤。
6.1 提炼函数(Extract Function)
曾用名:提炼函数(Extract Method)
反向重构:内联函数
重构前:
void PrintOwing(Invoice invoice)
{
PrintBanner();
decimal outstanding = CalculateOutstanding();
// print details
Console.WriteLine($"name: {invoice.Customer}");
Console.WriteLine($"amount: {outstanding}");
}
这个重构方法在 Visual Studio 中支持自动化重构,比较常用的几种重构方法在 Visual Studio (我这里使用的是 2019 社区版)中都有较好的支持。自动化重构的代码安全性也比较有保障。
操作步骤:选中需要提炼的代码,右键选择 快速操作和重构 ⇒ 提取本地函数 ,之后输入新的方法名,然后回车就可以了。
重构后:
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)
反向重构:提炼函数
重构前:
int GetRating(Driver driver)
{
return MoreThanFiveLateDeliveries(driver) ? 2 : 1;
}
private bool MoreThanFiveLateDeliveries(Driver driver)
{
return driver.NumberOfLateDeliveries > 5;
}
这个是 6.1 提炼函数(Extract Function) 的反向重构,不过 Visual Studio 不支持这种的自动化重构。
重构后:
int GetRating(Driver driver)
{
return driver.NumberOfLateDeliveries > 5 ? 2 : 1;
}
6.3 提炼变量(Extract Variable)
曾用名:引入解释性变量(Introduce Explaining Variable)
反向重构:内联变量
重构前:
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
,右键选择 快速操作和重构 ⇒ 为出现的所有 “...” 引入本地,输入新的变量名后回车。然后依次修改其它需要提炼变量的地方。
重构后:
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)
反向重构:提炼变量
重构前:
var basePrice = anOrder.BasePrice;
return basePrice > 1000;
双击选中需要内联的变量(变量声明的地方)后,右键选择 快速操作和重构 ⇒ 内联临时变量 就可以了。
重构后:
return anOrder.BasePrice > 1000;
6.5 改变函数声明(Change Function Declaration)
别名:函数改名(Rename Function)
曾用名:函数改名(Rename Method)
曾用名:添加参数(Add Parameter)
曾用名:移除参数(Remove Parameter)
别名:修改签名(Change Signature)
重构前:
double Cricum(double radius)
{
return 2 * Math.PI * radius;
}
操作步骤:右键方法名,选择 重命名 ,输入新的方法名按回车。
重构后:
double Cricumference(double radius)
{
return 2 * Math.PI * radius;
}
另外,本节还讲了通过添加函数重载来安全的添加/移除参数的重构方法。由于 JavaScript 不支持函数重载,所以采用的是新建一个别的名称的方法,最后再统一将函数替换为原来的方法名。
6.6 封装变量(Encapsulate Variable)
曾用名:自封装字段(Self-Encapsulate Field)
曾用名:封装字段(Encapsulate Field)
重构前:
public (string FirstName, string LastName) DefaultName = (FirstName: "JiaJia", LastName: "Liu");
操作步骤:右键字段名,选择 快速操作和重构 ⇒ 封装字段:“DefaultName”(但仍使用字段) 。
重构后:
private (string FirstName, string LastName) defaultName = (FirstName: "JiaJia", LastName: "Liu");
public (string FirstName, string LastName) DefaultName { get => defaultName; set => defaultName = value; }
上面的代码实际上可以省略如下形式,以去除 defaultName 临时变量:
public (string FirstName, string LastName) DefaultName { get; set; } = (FirstName: "JiaJia", LastName: "Liu");
将变量封装为属性有什么意义呢?我们来看一下如下用法:
obj.DefaultName.FirstName = "Jiajia";
上面的代码其实是赋值语句,重构前的代码上面的方式是可以正常赋值的,而却通过查找 DefaultName 变量的应用时仍然显示为读取,而不是写入。这样不仅会产生一种概念上的混淆,而且很容易发生未知的改动从而导致 bug。在第一章就提到过:可变状态会很快变成烫手的山芋。
重构后的代码再通过上述方式修改属性值时会报如下异常:
CS1612 无法修改“Refactoring_6_6.DefaultName”的返回值,因为它不是变量
此时如果要修改 DefaultName 属性的值,只能通过它的 set 方法。
var newName = obj.DefaultName;
newName.FirstName = "Jiajia";
obj.DefaultName = newName;
这样就可以很容易的查找到 DefaultName 变量所有的写入操作了。
如果需要使用更严厉的约束(如字段不允许修改),可以将 DefaultName 字段的类型修改为只读(readonly)的自定义结构。
internal readonly struct Name
{
public string FirstName { get; }
public string LastName { get; }
public Name(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}
此时如果直接修改字段属性,编译时会报 CS0200 错误。
var newName = obj.DefaultName;
newName.FirstName = "Jiajia";
CS0200 无法为属性或索引器“Name.FirstName”赋值 - 它是只读的
此时若要修改 DefaultName 只能通过如下方式:
obj.DefaultName = new Name("Jiajia", obj.DefaultName.LastName);
上面的两种情况都是值类型,如果 DefaultName 是引用类型,即使修改为属性也无法避免数据通过 get 方法被修改。此时可以通过将 get 方法修改为返回字段值的拷贝来保护数据。
6.7 变量改名(Rename Variable)
重构前:
int a = height * width;
操作步骤和修改方法名一样。
重构后:
int area = height * width;
6.8 引入参数对象(Introduce Parameter Object)
重构前:
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]
特性,将旧的函数标记为已过期。 - 测试
- 调整所有调用者。每修改一处,执行测试。
重构后:
interface Refactoring_6_8
{
decimal AmountInvoiced(DateRange aDateRange);
decimal AmountReceived(DateRange aDateRange);
decimal AmountOverdue(DateRange aDateRange);
}
6.9 函数组合成类(Combine Functions into Class)
重构前:
void Base(Reading aReading) { }
void TaxableCharge(Reading aReading) { }
void CalculateBaseCharge(Reading aReading) { }
重构后:
public class Reading
{
void Base() { }
void TaxableCharge() { }
void CalculateBaseCharge() { }
}
6.10 函数组合成变换(Combine Functions into Transform)
重构前:
decimal Base(Reading aReading) {
decimal result = decimal.Zero;
// do something
return result;
}
decimal TaxableCharge(Reading aReading)
{
decimal result = decimal.Zero;
// do something
return result;
}
重构后:
(decimal BaseCharge, decimal TaxableCharge) EnrichReading(Reading argReading) {
var aReading = DeepClone(argReading);
return (Base(aReading), TaxableCharge(aReading));
}
DeepClone 方法用来实现对象的深拷贝,关 Deep Clone 在 Stack Overflow 有个回复比较多的问题,常用的几种都有提及。我在 这篇博客 里也做过性能比较,还是通过反射的实现性能比较高,实现如下。
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)
重构前:
var orderData = Regex.Split(orderString, @"\s+");
var productPrice = priceList[orderData[0].Split("-")[1]];
var orderPrice = int.Parse(orderData[1]) * productPrice;
重构后:
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 。
csharpvar 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 方法修改为如下形式。
csharpstatic (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 。
csharpvar orderRecord = ParseOrder(orderString); var productPrice = priceList[orderRecord.ProductID]; var orderPrice = orderRecord.Quantity * productPrice;
选择 productPrice 变量应用 内联临时变量 快速重构。
csharpvar orderRecord = ParseOrder(orderString); var orderPrice = orderRecord.Quantity * priceList[orderRecord.ProductID];
选择 orderPrice 变量的赋值部分
orderRecord.Quantity * priceList[orderRecord.ProductID]
,应用 提取本地函数 重构,并将新的方法命名为 Price 。csharpvar 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]; }
最后完整代码如下
csharpvar 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]; }
因为和书上的示例使用的语言和代码不太一样,我也不确定这种步骤是不是标准做法。不过其中只有一部分改动比较大的地方是手动修改的,其它的都是通过自动化重构实现的,比较便捷,安全性也比较高。
引用
- 《重构:改善既有代码的设计》 -- 马丁·福勒(Martin Fowler)