05.重构-重新组织数据

1. 问题及方案

  • 表示方式:Replace Value with Object、Change Value to Reference;
  • 数组的行为方式很像一种数据结构:Replace Array with Object;
  • 魔法数:Replace Magic Number with Symbolic Constant;
  • 对象关联方向:Change Unidirectional Association to Bidirectional(单向->双向)、Change Bidirectional Association to Unidirectional(双向->单向);
  • 非公开数据:Encapsulate Field、Encapsulate Collection;
  • 记录暴露在外:Replace Record with Data Class;
  • 类型码(type code):Replace Type Code with Class、Replace Type Code with Subclasses、Replace Type Code with State/Strategy;

2. Self Encapsulate Field(自封装字段)

为这个字段设置取值、设值函数,并只能通过该函数访问。

3. Replace Data Value with Object(以对象取代数据值)

将数据项变成对象。

4. Change Value to Reference(将值对象改为引用对象)

问题:你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象。
将这个值对象改为引用对象。

4.1 动机

  • 实例共享,防止多份拷贝;
  • 解决方案:如单例模式、静态函数等;

5. Change Reference to Value(将引用对象改为值对象)

问题:你有一个引用对象,很小且不可变,而且不易管理。
将这个引用对象改为值对象。

5.1 动机

  • 值对象特性:不可变;
  • 提供 equals() 和 hashCode() 等实现;

6. Replace Array with Object(以对象取代数组)

问题:你有一个数组,其中的元素每个都代表不同的东西。
ps:Martin Flower 写《重构》一书时,基于 Java1.0 -> Java1.1 版本,那时还没有泛型一说。现如今应该不会出现这种数组存储完全不同类型的问题,否则就是需求分析建模出错。
以对象取代数组,对于数组中的每个元素,都使用一个字段表示。

6.1 动机

  • 数组存储不同类型数据;
  • 个人理解另一个问题:数组数据涉及到相关操作,可考虑将属性修改为直接继承已有列表类型的对象,而后扩展相应操作。

7. Duplicate Observed Data(复制“被监视数据”)

将数据复制到一个领域对象中。建立一个 Observer 模式,用以同步领域对象和 GUI 对象内的重复数据。

7.1 动机

  • UI 和业务逻辑代码分离;

8. Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)

添加一个反向指针,并使修改函数能够同时更新两条连接。

8.1 动机

  • 被引用类需要其引用者以便进行某些处理。

9. Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)

去除不必要的关联。

9.1 动机

  • 双向关联维护复杂,引用垃圾回收不了;
  • 依赖关系太强,导致紧耦合;

10. Replace Magic Number with Symbolic Constant(使用字面常量替换魔法数)

创造一个变量,根据其意义为它命名,并将上述的字面数值替换为这个常量。
ps

  • 常量不会造成任何性能开销,却可以大大提高代码可读性。
  • 若魔法数是类型码,考虑使用 Replace Type Code with Class;

11. Encapsulate Field(封装字段)

将字段声明为 private,并提供 get/set 方法。
ps:面向对象的原则之一就是封装,或称为“数据隐藏”。故数据不应该声明为 public,而通过(函数)行为暴露。

12. Encapsulate Collection(封装集合)

让这个函数返回该集合的一个只读副本,并在这个类中提供添加/删除集合元素的函数。

12.1 动机

  • 集合用来保存一组实例,并提供取值/设值函数;
  • 取值函数不应该返回集合本身,因为这会使得用户修改集合内容而集合拥有者却不知晓,过多暴露对象内部数据结构的信息;
  • 不应该为集合提供一个设置函数,而应该提供添加/删除元素的函数;
  • 封装数组,直接将数组替换为其他集合;

13. Replace Record with Data Class(以数据类取代记录)

为该记录创建一个“哑”数据对象。
ps:其实就是单独建立一个实体(Model/Entity),只有取值/设值函数。

14. Replace Type Code with Class(以类取代状态码)

以一个新的类替换该数值类型码。
ps

  • 现如今有枚举类型,强类型检查,可优先考虑。
  • 或者单独建立一个类,提供相应的状态码的静态字段,类似于 C# 中 Encoding 类的实现。

15. Replace Type Code with Subclasses(以子类取代类型码)

以子类取代这个类型码。
ps

  • 主要作用是为 Replace Conditional with Polymorphism 搭建平台。
  • 若宿主类中无条件表达式,推荐使用 Replace Type Code with Class。

15.1 动机

  • 以类型码的宿主类作为基类,针对每种类型码建立相应子类;
  • 把“对不同行为的了解”从类用户转移到类自身;
  • 不适用情况,需使用 Replace Type Code with State/Strategy:
    • 类型码在对象创建后发生改变;
    • 由于某种原因,类型码宿主已有子类;

16. Replace Type Code with State/Strategy(使用 State/Strategy 模式取代类型码)

以状态对象取代类型码。

  • 状态:State;
  • 算法:Strategy;

17. Replace Subclass with Fields(以字段取代子类)

问题:各个子类的唯一差别在于“返回常量数据”的函数上。
修改这些函数,使它们返回超类中的某个(新增)字段,然后销毁子类。

17.1 动机

建立子类的目的是为了增加新特性或变化行为。其中,有一类行为变化被称为“常量函数”,它们会返回一个硬编码的值,使得不同子类的同一个函数返回不同的值。
若子类只有常量函数,则没存在的价值。可直接在超类中使用字段表示类型(如 C# 中的 Encoding 类)

参考

  • 《重构-改善既有代码的设计》(第8章)
坚持原创技术分享,您的支持将鼓励我继续创作!