1. 处理数据的重构手法
1.1 Self Encapsulate Field(自封装字段)
你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙,为这个字段建立取值/设值函数,并且只以这些函数来访问字段。
动机
此种方式一般看个人意愿和实际情况,建议使用自封装,因为比较灵活。
做法
- 为待封装字段建立取值/设值函数。
- 找出该字段的所有引用点,将它们全部改为调用取值/设值函数。
- 将该字段声明为private。
- 复查,确保找出所有引用点。
- 编译,测试。
范例:略。
1.2 Replace Data Value with Object(以对象取代数据值)
你有一个数据项,需要与其他数据和行为一起使用才有意义,将数据项变成对象。
动机
数据项,需要与其他数据和行为一起使用才有意义。
做法
- 为待替换数值新建一个类,在其中声明一个final字段,其类型和源类中的替换数值类型一样。然后在新类中加入这个字段的取值函数,再加上一个接受此字段为参数的构造函数。
- 编译。
- 将源类中的待替换数值字段的类型改为前面新建的类。
- 修改源类中该字段的取值函数,令它调用新类的取值函数。
- 如果源类构造函数中用到这个待替换字段(多半是赋值动作),我们就修改构造函数,令它改用新类的构造函数来对字段进行赋值动作。
- 修改源类中待替换字段的设值函数,令它为新类创建一个实例。
- 编译,测试。
- 现在,你有可能需要对新类使用Change Value to Reference。
范例:略。
1.3 Change Value to Reference(将值对象改为引用对象)
你从一个类衍生出许多彼此相等的实例,希望将他们替换成同一个对象,将这个值对象变成引用对象。
动机
你希望对任何一个对象的修改都能影响到所有引用此对象的地方。你就需要将这个对象变成一个引用对象。
做法
- 使用Replace Constructor with Factory Method。
- 编译,测试。
- 决定由什么对象负责提供访问新对象的途径。
- 决定这些引用对象应该预先创建好,或者是应该动态创建。
- 修改工厂函数,令它返回引用对象。
- 编译,测试。
范例:略。
1.4 Change Reference to Value(将引用对象改为值对象)
你从一个引用对象,很小且不可变,而且不易管理,将它变成一个值对象。
动机
值对象应该是不可变的。
关于不可变:
如果你以Money类表示钱的概念,其中有币种和金额两条信息,那么Money对象通常是不可变的。这并不是一位着你的薪资不可变,只是如果要改变你的薪资,就需要使用另一个Money对象来取代现有的Money对象,而不是在现有的Money对象上修改。
做法
- 检查重构目标是否为不可变对象,或是否可以修改为不可变对象。
- 建立equals()和hashCode()。
- 编译,测试。
- 考虑是否可以删除工厂函数,并将构造函数声明为public。
范例:略。
1.5 Replace Array with Object(以对象取代数组)
你有一个数组,其中元素各自代表不同的东西,以对象替换数组。对于数组中的每个元素,以一个字段来表示。
动机
数组应该是以某种顺序容纳一组相似对象。如果一个数组容纳了多种不同对象,这时你应该考虑重构
做法
- 新建一个类表示数组所拥有的信息,并在其中一个public字段表示原先的数组。
- 修改数组的所有用户,让它们改用新类的实例。
- 编译,测试。
- 逐一为数组元素添加取值/设值函数。根据元素的用途,为这些访问函数命名。修改客户端代码,让它们通过访问函数取用数组内的元素。每次修改后,编译并测试。
- 当所有对数组的直接访问都转而调用访问函数后,将新类中保存该数组的字段声明为private。
- 编译。
- 对于数组内的每一个元素,在新类中创建一个类型相当的字段。修改该元素的访问函数,令它改用上述的新建字段。
- 每修改一个元素,编译并测试。
- 数组的所有元素都有了响应字段之后,删除该数组。
范例:略。
1.6 Duplicate Observed Data(复制“被监视数据”)
你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
动机
如果你遇到的代码是以两层方式开发,业务逻辑被内嵌于用户界面之中,你就有必要将行为分离出来。其中的主要工作就是函数的分解和搬移。但数据就不同了,你不能仅仅只是移动数据,必须将它复制到新的对象中,并提供相应的同步机制。
做法
- 修改展现类,使其成为领域类的Observer。
- 针对GUI类中的领域数据,使用Self Encapsulate Field。
- 编译,测试。
- 在时间处理函数中调用设值函数,直接更新GUI组件。
- 编译,测试。
- 在领域类中定于数据及其相关访问函数。
- 修改展现类中的访问函数,将它们的操作对象改为领域对象。
- 修改Observer的update(),使其从相应的领域对象中将所需数据复制给GUI组件。
- 编译,测试。
范例:略。
1.7 Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
两个类都需要使用对方特性,但其间只有一条单向连接。添加一个反向指针,并使修改函数能够同时更新两条连接。
动机
你需要在被引用端操作引用端。
做法
- 在被引用类中增加一个字段,用以保存反向指针。
- 决定由哪个类-引用端还是被引用端-控制关联关系。
- 在被控制端建立一个辅助函数,其命名应该清楚指出它的有限用途。
- 如果既有的修改函数在控制端,让它负责更新反向指针。
- 如果既有的修改函数在被控端,就在控制端建立一个控制函数,并让既有的修改函数调用这个新建的控制函数。
范例:略。
1.8 Change Bidirectional Association to Unidirectional (将双向关联改为单向关联)
两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。
动机
你发现双向关联不再有存在价值。
做法
- 找出保存你想去除的指针的字段,检查它的每一个用户,判断是否可以去除该指针。
- 如果客户使用了取值函数,先运用Self Encapsulate Field将待删除字段自我封装起来,然后使用Substitute Algorithm对付取值函数,令它不再使用该字段。然后编译、测试。
- 如果客户并未使用取值函数,那就直接修改待删除字段的所有被引用点,改为以其他途径获得该字段所保存的对象。每次修改后,编译并测试。
- 如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后删除该字段。
- 编译,测试。
范例:略。
1.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)
你有一个字面常量,带有特别含义。创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。
动机
字面常量增加了程序的阅读难度和修改难度。
做法
- 声明一个常量,令其值为原本的魔法数值。
- 找出这个魔法数的所有引用点。
- 检查是否可以使用这个新声明的常量来替换魔法数。如果可以,便以此常量替换之。
- 编译。
- 所有魔法数都被替换完毕后,编译并测试。此时整个程序应该运转如常,就像没有做过任何修改一样。
范例:略。
1.10 Encapsulate Field(封装字段)
你的类中存在一个public字段。将它声明为private,并提供相应的访问函数。
动机
封装数据。
做法
- 为public字段提供取值/设值函数。
- 找到这个类以外使用该字段的所有地点。如果客户指示读取该字段,就把引用替换为对取值函数的调用;如果客户修改了该字段值,就将此引用点替换为对设值函数的调用。
- 每次修改之后,编译并测试。
- 将字段的所有用户修改完毕后,将字段声明为private。
- 编译,测试。
范例:略。
1.11 Encapsulate Collection(封装集合)
有个函数返回一个集合。让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。
动机
集合的取值函数不该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者却一无所悉。这也会对用户暴露过多对象内部数据结构的信息。如果一个取值函数确实需要返回多个值,它应该避免用户直接操作对象内所保存的集合,并隐藏对象内与用户无关的数据结构。并且,不应该为整个集合提供一个设值函数,但应该提供用以为集合添加/移除元素的函数。
做法
- 加入为集合添加/移除元素的函数。
- 将保存集合的字段初始化为一个空集合。
- 编译。
- 找出集合设值函数的所有调用者。你可以修改那个设值函数,让它使用上述新建立的“添加/移除元素”函数;也可以直接修改调用端。改让它们调用上述新建立的“添加/移除元素”函数。
- 编译,测试。
- 找出所有通过取值函数获得集合并修改其内容的函数。逐一修改这些函数,让它们改用添加/移除函数。每次修改后,编译并测试。
- 修改完上述所有通过取值函数获得集合并修改集合内容的函数后,修改取值函数本身,使它返回该集合的一个只读副本。
- 编译,测试。
- 找出取值函数的所有用户,从其中找出应该存在于集合所属对象内的代码。运用Extract Method和Move Method将这些代码移到宿主对象去。
- 修改现有取值函数的名字,然后添加一个新取值函数,使其返回一个枚举。找出旧取值函数的所有被使用点,将它们都改为使用新取值函数。
- 如果这一步跨度太大,你可以先使用Rename Method修改原取值函数的名称;再建立一个新取值函数用以返回枚举;最后再修改所有调用者,使其调用新取值函数。
- 编译,测试。
范例:略。
1.12 Replace Record with Data Class(以数据类取代记录)
你需要面对系统编程环境中的记录结构。为该记录创建一个“哑”数据对象。
动机
记录型结构。
做法
- 新建一个类,表示这个记录。
- 对于记录中的每一项数据,在新建的类中建立对应的一个private字段,并提供相应的取值/设值函数。
范例:略。
1.13 Replace Type Code with Class(以类取代类型码)
类之中有一个数值类型码,但它不影响类的行为。以一个新的类替换该数值类型码。
动机
程序中使用了类型码。
做法
- 为类型码建立一个类。
- 修改源类实现,让它使用上述新建的类。
- 编译,测试。
- 对于源类中每一个使用类型码的函数,相应建立一个函数,让新函数使用新建的类。
- 逐一修改源类用户,让它们使用新接口。
- 每修改一个用户,编译并测试。
- 删除使用类型码的旧接口,并删除保存旧类型码的静态变量。
- 编译,测试。
范例:略。
1.14 Replace Type Code with Subclasses(以子类取代类型码)
有一个不可变的类型码,它会影响类的行为。以子类取代这个类型码。
动机
类型码影响了宿主类的行为。但以下两种情况不能那么做:类型码值在对象创建之后发生了改变;由于某些原因宿主类已经有了子类。如果遇到这两种情况,需要使用Replace Type Code with State/Strategy。
做法
- 使用Self Encapsulate Field将类型码自我封装起来。
- 为类型码的每一个数值建立一个相应的子类。在每一个子类中覆写类型码的取值函数,使其返回相应的类型码值。
- 每建立一个新的子类,编译并测试。
- 从超类中删掉保存类型码的字段。将类型码访问函数声明为抽象函数。
- 编译,测试。
范例:略。
1.15 Replace Type Code with State/Strategy(以State/Strategy取代类型码)
有一个类型码,它会影响类的行为,但你无法通过继承手法消除它。以状态对象取代类型码。
动机
无法使用Class和Subclasses替换类型码。
做法
- 使用Self Encapsulate Field将类型码自我封装起来。
- 新建一个类,根据类型码的用途为它命名。这就是一个状态对象。
- 为这个新类添加子类,每个子类对应一种状态码。
- 在超类中建立一个抽象的查询函数,用以返回类型码。在每个子类中覆写该函数,返回确切的类型码。
- 编译。
- 在源类中建立一个字段,用以保存新建的状态对象。
- 调整源类中负责查询类型码的函数,将查询动作转发给状态对象。
- 调整源类中为类型码设值的函数,将一个恰当的状态对象子类赋值给保存对象的那个字段。
- 编译,测试。
范例:略。
1.16 Replace Subclass with Fields(以字段取代子类)
你的各个子类的唯一差别只在“返回常量数据”的函数身上。修改这些函数,使它们返回超类中的某个(新增)字段,然后销毁子类。
动机
建立子类的目的,是为了增加新特性或者变化其行为。有一种变为行为被称为“常量函数”,它们会返回一个硬编码的值。但如果子类中只有常量函数,实在没有足够的存在价值。
做法
- 对所有子类使用Replace Constructor with Factory Method。
- 如果有任何代码直接引用子类,令它改而引用超类。
- 针对每个常量函数,在超类中声明一个final字段。
- 为超类声明一个protected构造函数,用以初始化这些新增字段。
- 新建或修改子类构造函数,使它调用超类的新增构造函数。
- 编译,测试。
- 在超类中实现所有常量函数,令它们返回相应字段值,然后将该函数从子类中删掉。
- 每删掉一个常量函数,编译并测试。
- 子类中所有的常量函数都被删除后,使用Inline Method将子类构造函数内联到超类的工厂函数中。
- 编译,测试。
- 将子类删掉。
- 编译,测试。
- 重复内联构造函数、删除子类过程,直到所有子类都被删除。
范例:略。
本文引用自图灵程序设计丛书:重构 改善改善既有的代码设计。