重构 - 7. 封装
🏷️ 《重构》
7.1 封装记录(Encapsulate Record)
曾用名:以数据类取代记录(Replace Record with Data Class)
重构前:
(string Name, string Country) organization = ("JiaJia's Blog", "China");
重构后:
public Organization Organization { get; } = new Organization(("JiaJia's Blog", "China"));
class Organization
{
public Organization((string Name, string Country) data)
{
Name = data.Name;
Country = data.Country;
}
public string Name { get; set; }
public string Country { get; set; }
}
操作步骤:
由于 C# 中的记录型结构是通过值元组提供的,很多特性和 JavaScript 不同,导致按照书中给定的步骤重构时会一直报错,所以记录一下自己重构的步骤。
创建封装类
csharpclass Organization { public Organization(string name, string country) { this.Name = name; this.Country = country; } public string Name { get; set; } public string Country { get; set; } }
修改记录为类实例
csharpOrganization organization = new Organization("JiaJia's Blog", "China");
右键变量 organization ⇒ 快速操作和重构 ⇒ 添加只读修饰符
csharpreadonly Organization organization = new Organization("JiaJia's Blog", "China");
右键变量 organization ⇒ 快速操作和重构 ⇒ 封装字段:“organization”(并使用属性)
csharpreadonly Organization organization = new Organization("JiaJia's Blog", "China"); internal Organization Organization => organization;
右键变量 organization ⇒ 快速操作和重构 ⇒ 使用自动属性
csharpinternal Organization Organization { get; } = new Organization("JiaJia's Blog", "China");
个人认为到这一步就可以了,我个人来讲比较倾向于使用属性。如果倾向于使用方法,可以再执行下一步。
右键属性 Organization ⇒ 快速操作和重构 ⇒ 将“Organization”替换为方法
csharpprivate readonly Organization organization = new Organization("JiaJia's Blog", "China"); internal Organization GetOrganization() { return organization; }
此时调用的地方最终会变成类似如下的形式:
csharppublic void Test() { GetOrganization().Name = "Jiajia's Blog"; Console.WriteLine($"{GetOrganization().Name}:{GetOrganization().Country}"); }
7.2 封装集合(Encapsulate Collection)
重构前:
List<Course> courses = new List<Course>();
List<Course> Courses { get => courses; set => courses = value; }
重构后:
List<Course> courses = new List<Course>();
public List<Course> Courses { get => courses.ToList(); }
public void AddCourse(Course course)
{
courses.Add(course);
}
public void RemoveCourse(Course aCourse)
{
courses.Remove(aCourse);
}
这里使用了集合的 ToList 扩展方法来返回一个集合的浅拷贝,以避免集合意外的被修改,也方便找到集合的修改点。也可以调用 AsReadOnly 方法返回一个只读的集合。
不过无论是 ToList 还是 AsReadOnly 都只能保护集合的新增和删除,无法避免集合项的属性被修改。
如果一旦添加到集合后属性就不会再被修改,则可以考虑返回集合的深拷贝。此时需要注意调用 RemoveCourse 时必须传递调用 AddCourse 方法时传递的引用。如果传递的是通过 Courses 属性获取的引用,虽然不会报错,但数据其实并没有没删除掉。
关于 C# 中深拷贝的方法和性能可以参考这篇博客,比较推荐使用 JSON 序列化/反序列化的方式。
下面是示例代码,需要安装 Newtonsoft.Json 包。
public static class CloneExtensions
{
public static T DeepClone<T>(this T source)
{
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source));
}
}
7.3 以对象取代基本类型(Replace Primitive with Object)
曾用名:以对象取代数据值(Replace Data Value with Object)
曾用名:以类取代类型码(Replace Type Code with Class)
重构前:
orders.Where(m => "high" == m.Priority || "rush" == m.Priority).ToList();
class Order
{
public string Priority { get; set; }
}
上面示例代码中 Priority 是基本类型 string
型,新建一个 Priority 类型以取代简单的字符串。
重构后:
orders.Where(m => m.Priority.HigherThan(new Priority("normal"))).ToList();
class Order
{
public Priority Priority { get; set; }
}
class Priority
{
string _value;
static List<string> LegalValues { get; } = new List<string> { "low", "normal", "high", "rush" };
public int Index {
get {
return LegalValues.IndexOf(_value);
}
}
public Priority(string value)
{
if (LegalValues.Contains(value))
{
_value = value;
}
else
{
throw new Exception($"{value} 是一个无效的 Priority。");
}
}
public bool Equals(Priority other)
{
return this.Index == other.Index;
}
public bool HigherThan(Priority other)
{
return this.Index > other.Index;
}
public bool LowerThan(Priority other)
{
return this.Index < other.Index;
}
public override string ToString()
{
return _value;
}
}
7.4 以查询取代临时变量(Replace Temp with Query)
重构前:
public double Price
{
get
{
var basePrice = Quantity * ItemPrice;
if (basePrice > 1000)
{
return basePrice * 0.95;
}
else
{
return basePrice * 0.98;
}
}
}
重构后:
public double Price
{
get
{
if (BasePrice > 1000)
{
return BasePrice * 0.95;
}
else
{
return BasePrice * 0.98;
}
}
}
private double BasePrice => Quantity * ItemPrice;
VS 中操作步骤
选中
Quantity * ItemPrice
⇒ 快速操作和重构 ⇒ 提取方法csharppublic double Price { get { var basePrice = GetBasePrice(); if (basePrice > 1000) { return basePrice * 0.95; } else { return basePrice * 0.98; } } } private double GetBasePrice() { return Quantity * ItemPrice; }
右键 GetBasePrice 方法定义 ⇒ 快速操作和重构 ⇒ 使用属性替代:“GetBasePrice”
csharppublic double Price { get { var basePrice = BasePrice; if (basePrice > 1000) { return basePrice * 0.95; } else { return basePrice * 0.98; } } } private double BasePrice => Quantity * ItemPrice;
右键变量定义 basePrice ⇒ 快速操作和重构 ⇒ 内联临时变量
csharppublic double Price { get { if (BasePrice > 1000) { return BasePrice * 0.95; } else { return BasePrice * 0.98; } } } private double BasePrice => Quantity * ItemPrice;
还可以优化为使用三元运算符来替换掉 Price 中的 if 语句,然后再应用 使用属性的表达式主题 快速操作。
csharppublic double Price => BasePrice > 1000 ? BasePrice * 0.95 : BasePrice * 0.98; private double BasePrice => Quantity * ItemPrice;
注意:
某些类型的临时变量不能是应该改手法,如对照用途的临时变量。
7.5 提炼类(Extract Class)
反向重构:内联类(Inline Class)
重构前:
class Person
{
public string OfficeAreaCode { get; set; }
public string OfficeNumber { get; set; }
}
重构后:
class Person
{
private TelephoneNumber _telephoneNumber = new TelephoneNumber();
public string OfficeAreaCode
{
get
{
return _telephoneNumber.AreaCode;
}
set
{
_telephoneNumber.AreaCode = value;
}
}
public string OfficeNumber
{
get
{
return _telephoneNumber.Number;
}
set
{
_telephoneNumber.Number = value;
}
}
}
class TelephoneNumber
{
public string AreaCode { get; set; }
public string Number { get; set; }
}
7.6 内联类(Inline Class)
反向重构:提炼类(Extract Class)
和上节 7.5 提炼类(Extract Class) 是互为反向重构的关系。
重构前:
class Person
{
private TelephoneNumber _telephoneNumber = new TelephoneNumber();
public string OfficeAreaCode
{
get
{
return _telephoneNumber.AreaCode;
}
set
{
_telephoneNumber.AreaCode = value;
}
}
public string OfficeNumber
{
get
{
return _telephoneNumber.Number;
}
set
{
_telephoneNumber.Number = value;
}
}
}
class TelephoneNumber
{
public string AreaCode { get; set; }
public string Number { get; set; }
}
重构后:
class Person
{
public string OfficeAreaCode { get; set; }
public string OfficeNumber { get; set; }
}
7.7 隐藏委托关系(Hide Delegate)
反向重构:移除中间人(Remove Middle Man)
重构前:
var manager = aPerson.Department.Manager;
重构后:
var manager = aPerson.Manager;
class Person
{
public string Manager {
get
{
return Department.Manager;
}
}
}
7.8 移除中间人(Remove Middle Man)
反向重构:隐藏委托关系(Hide Delegate)
和上节 7.7 隐藏委托关系 是互为反向重构的关系。
重构前:
var manager = aPerson.Manager;
class Person
{
public string Manager {
get
{
return Department.Manager;
}
}
}
重构后:
var manager = aPerson.Department.Manager;
7.9 替换算法
重构前:
string FoundPerson(string[] people)
{
for (int i = 0; i < people.Length; i++)
{
if (people[i] == "Don")
{
return "Don";
}
if (people[i] == "John")
{
return "John";
}
if (people[i] == "Kent")
{
return "Kent";
}
}
return string.Empty;
}
重构后:
static readonly string[] candidates = { "Don", "John", "Kent" };
string FoundPerson(string[] people)
{
return people.FirstOrDefault(p => candidates.Contains(p)) ?? string.Empty;
}
对于引用类型来说,static readonly 的效果类似于 JavaScript 和 Java 中的 const,不过 C# 中不支持在方法体中定义 static readonly 修饰符的变量。
附 1. 引用
- 《重构:改善既有代码的设计》 -- 马丁·福勒(Martin Fowler)