Skip to content

重构 - 7. 封装

🏷️ 《重构》

7.1 封装记录(Encapsulate Record)

曾用名以数据类取代记录(Replace Record with Data Class)

重构前

csharp
(string Name, string Country) organization = ("JiaJia's Blog", "China");

重构后

csharp
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 不同,导致按照书中给定的步骤重构时会一直报错,所以记录一下自己重构的步骤。

  • 创建封装类

    csharp
    class Organization
    {
        public Organization(string name, string country)
        {
            this.Name = name;
            this.Country = country;
        }
    
        public string Name { get; set; }
        public string Country { get; set; }
    }
  • 修改记录为类实例

    csharp
    Organization organization = new Organization("JiaJia's Blog", "China");
  • 右键变量 organization快速操作和重构添加只读修饰符

    csharp
    readonly Organization organization = new Organization("JiaJia's Blog", "China");
  • 右键变量 organization快速操作和重构封装字段:“organization”(并使用属性)

    csharp
    readonly Organization organization = new Organization("JiaJia's Blog", "China");
    
    internal Organization Organization => organization;
  • 右键变量 organization快速操作和重构使用自动属性

    csharp
    internal Organization Organization { get; } = new Organization("JiaJia's Blog", "China");

    个人认为到这一步就可以了,我个人来讲比较倾向于使用属性。如果倾向于使用方法,可以再执行下一步。

  • 右键属性 Organization快速操作和重构将“Organization”替换为方法

    csharp
    private readonly Organization organization = new Organization("JiaJia's Blog", "China");
    
    internal Organization GetOrganization()
    {
        return organization;
    }

    此时调用的地方最终会变成类似如下的形式:

    csharp
    public void Test()
    {
        GetOrganization().Name = "Jiajia's Blog";
        Console.WriteLine($"{GetOrganization().Name}:{GetOrganization().Country}");
    }

7.2 封装集合(Encapsulate Collection)

重构前

csharp
List<Course> courses = new List<Course>();

List<Course> Courses { get => courses; set => courses = value; }

重构后

csharp
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 包。

csharp
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)

重构前

csharp
orders.Where(m => "high" == m.Priority || "rush" == m.Priority).ToList();

class Order
{
    public string Priority { get; set; }
}

上面示例代码中 Priority 是基本类型 string 型,新建一个 Priority 类型以取代简单的字符串。

重构后

csharp
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)

重构前

csharp
public double Price
{
    get
    {
        var basePrice = Quantity * ItemPrice;
        if (basePrice > 1000)
        {
            return basePrice * 0.95;
        }
        else
        {
            return basePrice * 0.98;
        }
    }
}

重构后

csharp
public double Price
{
    get
    {
        if (BasePrice > 1000)
        {
            return BasePrice * 0.95;
        }
        else
        {
            return BasePrice * 0.98;
        }
    }
}

private double BasePrice => Quantity * ItemPrice;

VS 中操作步骤

  • 选中 Quantity * ItemPrice快速操作和重构提取方法

    csharp
    public 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”

    csharp
    public double Price
    {
        get
        {
            var basePrice = BasePrice;
            if (basePrice > 1000)
            {
                return basePrice * 0.95;
            }
            else
            {
                return basePrice * 0.98;
            }
        }
    }
    
    private double BasePrice => Quantity * ItemPrice;
  • 右键变量定义 basePrice快速操作和重构内联临时变量

    csharp
    public double Price
    {
        get
        {
            if (BasePrice > 1000)
            {
                return BasePrice * 0.95;
            }
            else
            {
                return BasePrice * 0.98;
            }
        }
    }
    
    private double BasePrice => Quantity * ItemPrice;
  • 还可以优化为使用三元运算符来替换掉 Price 中的 if 语句,然后再应用 使用属性的表达式主题 快速操作。

    csharp
    public double Price => BasePrice > 1000 ? BasePrice * 0.95 : BasePrice * 0.98;
    
    private double BasePrice => Quantity * ItemPrice;

注意

某些类型的临时变量不能是应该改手法,如对照用途的临时变量。

7.5 提炼类(Extract Class)

反向重构内联类(Inline Class)

重构前

csharp
class Person
{
    public string OfficeAreaCode { get; set; }
    public string OfficeNumber { get; set; }
}

重构后

csharp
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) 是互为反向重构的关系。

重构前

csharp
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; }
}

重构后

csharp
class Person
{
    public string OfficeAreaCode { get; set; }
    public string OfficeNumber { get; set; }
}

7.7 隐藏委托关系(Hide Delegate)

反向重构移除中间人(Remove Middle Man)

重构前

csharp
var manager = aPerson.Department.Manager;

重构后

csharp
var manager = aPerson.Manager;

class Person
{
    public string Manager {
        get
        {
            return Department.Manager;
        }
    }
}

7.8 移除中间人(Remove Middle Man)

反向重构隐藏委托关系(Hide Delegate)

和上节 7.7 隐藏委托关系 是互为反向重构的关系。

重构前

csharp
var manager = aPerson.Manager;

class Person
{
    public string Manager {
        get
        {
            return Department.Manager;
        }
    }
}

重构后

csharp
var manager = aPerson.Department.Manager;

7.9 替换算法

重构前

csharp
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;
}

重构后

csharp
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. 引用

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