重构的思路

UML 类图关系

UML 类图中大致有 6 种关系: 继承和实现; 依赖; 聚合和组合; 关联;

继承和实现

类继承类, 接口继承接口, 类实现接口

继承和实现都是使用 空心三角箭头 来表示, 不同的是继承为实线, 实现为虚线

依赖

当一个对象需要调用另外一个对象的方法去完成某些工作时, 就构成了依赖关系

依赖关系使用 虚线箭头 来表示

聚合和组合

聚合和组合描述的都是包含关系

聚合描述的主体和部分是可以分离的, 也可以用 has a 表示, 比如部门和雇工, 即使没有雇工, 部门也依然存在

组合描述的主体和部分是不可分离的, 也可以用 is a 表示, 比如墙体和砖头, 如果没有了砖头, 墙体也就不存在了

聚合和组合都是使用 扁平菱形箭头 表示的, 不同的是聚合是空心, 组合是实心

关联

对象之间既不是依赖, 也不是聚合, 组合的关系, 那就是关联的关系, 比如老师和学生, 他们既不是依赖, 也不是包含

关联关系直接使用 直线 表示

移动方法

当某个类的方法实现的功能更多地适用于另外一个类, 且符合它的语义时, 应将该方法移动到另外一个类.

提取方法

如果一个方法包含多个逻辑, 我们应将每个逻辑提取出来, 并确保每个方法只做一件事情.

方法以及字段的升级和降级

升级方法:当子类的方法描述了相同的行为时, 应将这样的方法提升到基类.
降级方法:在基类中的行为仅和个别子类相关时, 应将这样的行为降低到子类.
升级字段:当子类中的字段描述着相同的信息时, 应将这样的字段提升到基类.
降级字段:当基类中的字段仅仅用于个别子类时, 应将这样的字段降低到子类.

方法升级时的注意点

  1. 基类中定义的行为实现细节, 应该是所有子类共有的
  2. 子类应该具有重写基类行为的能力, 重写时应该是对行为细节的附加, 而不应当随意篡改基类的行为细节

使用 [Flags] 优化多 bool 参数的情况以及实现枚举多选

将参数整合为结构或类来减少参数的数量

区分继承和委托

  1. 父子关系用继承
  2. 利用关系用委托

提取接口

假设 A 类调用 B 类的 C 方法, 而 C 方法随着程序的复杂出现了多个, 完全相同的方法签名但是其中的逻辑不同,

此时就可以将 B 类中的 C 方法提取为接口, B 类实现此接口, 进而通过不同的 B 类来实现不同的 C 方法

分解依赖 (解除依赖)

假设 A 类依赖 B 类中的 C 方法

想要解除掉 A 对 B 的依赖, 可以将 C 方法使用接口进行包装

A 仅依赖于这个包装, 而 B 是此包装的其中一个具体实现, 这样 A 就不会直接依赖于 B 了

SRP 原则 (单一职责原则)

每个类, 接口都只定义单一的职责, 定义要清晰明确

提取基类

当不止有一个类具有相同功能的时候, 应该提取出一个基类, 将相同的功能放在基类中

这个相同的功能必须每一个子类都具有, 但不要求每个子类此功能的逻辑完全相同, 即赋予子类重写该功能的权限

提取子类

当基类中的某个功能或者属性不是所有子类所共有的, 那么需要将这些方法或属性下放到子类中

移除上帝类 (全能类)

为了提升代码的易读性以及降低修改代码的成本, 需要移除掉臃肿的上帝类

移除中间类 (过度使用设计模式)

过度使用设计模式的时候, 可能会出现中间设计了一些过度层的情况, 这些层级中的代码几乎没有任何作用, 只是重复了一些底层的逻辑而已, 这部分类就可以移除掉

多态代替条件判断 (将变化下放到子类中)

提取管理器中的 变化 到基类或者接口中, 让管理器中的变化消失, 以子类中对基类或接口的不同实现来体现 变化

重构前的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Customer { }
public class Employee : Customer { }
public class NonEmployee : Customer { }
public class OrderProcessor
{
public decimal ProcessOrder(Customer customer, IEnumerable<Product> products)
{
var orderTotal = products.Sum(p => p.Price);
var customerType = customer.GetType();
if (customerType == typeof(Employee))
{
orderTotal -= orderTotal * 0.15m;
}
else if (customerType == typeof(NonEmployee))
{
orderTotal -= orderTotal * 0.05m;
}
return orderTotal;
}
}

重构后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
public interface Customer { decimal DiscountPercentage { get; } }
public class Employee : Customer { public decimal DiscountPercentage => 0.15m; }
public class NonEmployee : Customer { public decimal DiscountPercentage => 0.05m; }
public class OrderProcessor
{
public decimal ProcessOrder(Customer customer, IEnumerable<Product> products)
{
var orderTotal = products.Sum(p => p.Price);
orderTotal -= orderTotal * customer.DiscountPercentage;
return orderTotal;
}
}

契约设计

在逻辑开始之前使用断言对参数的合法性进行判断

在逻辑结束之前使用断言对返回值的合法性进行判断

IEnumerable 替换 IList

IListIEnumerable 都可以遍历集合的元素

IList 拥有集合的所有操作方法, 包括集合元素的增加, 修改和删除

而IEnumerable则只有一个GetEnumerator方法(扩展方法除外),它返回一个可用于循环访问集合的IEnumerator对象。

Job System

Job System 如何避免资源竞争

Job System 通过给每一个需要操作数据的 Job 一份数据拷贝而不是主线程中的数据引用来避免这个问题, 拷贝和原本的数据独立, 从而排除了资源竞争

Job System 拷贝数据的方式决定了一个 Job 只能访问可位块传输的数据类型(blitable data types), 这种数据类型在托管代码和原生代码之间进行传递的时候不需要类型转换

安全性系统中拷贝数据的缺点是单个 Job 的计算结果是与外部隔离的, 为了突破这个限制需要把结果放在共享内存 NativeContainer 中

NativeContainer 是一种托管的数据类型, 包括一个指向非托管分配内存的指针, NativeContainer 使得一个 Job 可以访问和主线程共享的数据, 而不是在一份拷贝