Skip to content

Latest commit

 

History

History
366 lines (203 loc) · 16.3 KB

第8章 重新组织数据.md

File metadata and controls

366 lines (203 loc) · 16.3 KB

第8章 重新组织数据

8.1 self Encapsulate Field (自封装字段)

你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。

为这个字段建立取值、设置函数,并且只以这些函数来访问字段。

动机

  • 间接访问变量的好处是,子类可以通过覆写一个函数而改变获取数据的途径,他还支持更灵活的数据管理方式,例如延迟初始化。

做法

  • 为待封装字段建立set/get方法

  • 找出该字段的所有引用点,将他们全部改为set/get方法;

    如果引用点要读取字段值,就将它替换为调用取值函数;如果引用点要给字段赋值,就将它替换为调用set函数。

    你可以暂时将该字段改名,让编译器帮助你查找引用点。

  • 将该字段声明为private。

  • 复查,确保找出所有引用点,

  • 编译,测试。

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

你有个数据项,需要与其他数据和行为一起使用才有意义。

将数据项变成对象。

动机

  • 开发初期,简单的数据项表示简单的情况,随着开发进行,这些数据项会涉及相关的处理函数。这是应该将数据值变成对象。

做法

  • 为待替换数值新建一个类,在其中声明一个final字段,其类型和源类中的待替换数值类型一样。然后在新类中加入这个字段的取值函数,再加上一个接受此字段为参数的构造函数。
  • 编译。
  • 将源类中的待替换数值字段的类型改为前面新建的类。
  • 修改源类中该字段的取值函数,令他调用新类的取值函数。
  • 如果源类构造函数中用到这个待替换字段(多半是赋值动作),我们就修改构造函数,令他改用新类的构造函数对字段进行赋值动作。
  • 修改源类中待替换字段的设值函数,令他为新类创建一个实例。
  • 编译、测试
  • 现在,你有可能需要对新类使用change value to reference(179).
8.3 Change Value to Reference (将值对象改为引用对象)

你从一个类衍生出许多彼此相等的实例,希望将他们替换成同一个对象。

将这个值对象变成引用对象

动机

  • 值对象与引用对象,使用==检查对象的同一性,检查两个对象是否相等。检查值对象是否相等,需要覆写equals()以及hashCode()函数。
  • 你可能会希望给对象加入一些可修改数据,并确保对任何一个对象的修改都能影响到所有引用此一对象的地方。这时你就要将这个对象变成一个引用对象。

做法

  • 使用Replace Constructor with Factory Method(304)

  • 编译,测试。

  • 决定由什么对象负责提供访问新对象的途径。

    可能是一个静态字典或一个注册表对象

    你也可以使用多个对象作为新对象的访问点。

  • 决定这些引用对象应该预先创建好,或是应该动态创建。

    如果这些引用对象是预先创建好的,而你必须从内存中将他们读取出来,那么就得确保他们是被需要的时候能够被及时加载。

  • 修改工厂函数,令他返回引用对象。

    如果对象是预先创建好的,你就需要考虑,万一有人索求一个其实并不存在的对象,要如何处理错误?

    你可能希望对工厂函数使用Rename Method(273),使其传达这样的信息:它返回的是一个既存对象。

  • 编译测试。

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

你有一个引用对象,很小且不可变,而且不易管理。将它变成一个值对象。

动机

  • 如果引用对象开始变得难以使用,也许就应该将它改为值对象。
  • 值对象有一个非常重要的特性,他们应该是不可变的。

做法

  • 检查重构目标是否为不可变对象,或是否可修改为不可变对象。

    如果该对象目前还不是不可变的,就使用Remove Setting Method(300),直到它变成不可变的为止。

    如果无法将该对象修改为不可变,就放弃使用本项重构。

  • 建立equals()和hashCode()

  • 编译测试。

  • 考虑是否可以删除工厂函数,并将构造函数声明为public。

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

你有一个数组,其中的元素各自代表不同的东西。以对象替换数组。对于数组中的每个元素,以一个字段来表示。

做法

  • 新建一个类表示数组所拥有的的信息,并在其中以一个public字段保存原先的数组。
  • 修改数组的所有用户,让他们改用新类的实例。
  • 编译,测试。
  • 逐一为数组元素添加取值、设置函数,根据元素的用途,为这些访问函数命名。修改客户端代码,让他们通过访问函数取数组内的元素,每次修改后,编译测试。
  • 当所有对数组的直接访问都转而调用访问函数后,将新类中保存该数组的字段声明为private。
  • 编译
  • 对于数组内的每一个元素,在新类中创建一个类型相当的字段。,修改该元素的访问函数,令他改用上述 的新建字段。
  • 每修改一个元素,编译并测试。
  • 数组的所有元素都有了相应字段之后,删除该数组。
8.6 Duplicated Observed Data(赋值“被监视数据”)

你有一些领域数据置身于GUI空间中,而领域函数需要访问这些数据。

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

动机

  • 一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。
  • Model-View—Controller

做法

  • 修改展现类,使其成为领域类的Observer【GOF】。

    如果尚未有领域类,就建立一个。

    如果没有“从展现类到领域类”的关联,就将领域类保存与展现类的一个字段中。

  • 针对GUI类中的领域数据,使用Self Encapsulate Filed(171)。

  • 编译、测试。

  • 在事件处理函数中调用设置函数,直接更新GUI组件。

  • 编译、测试

  • 在领域类定义数据及相关访问函数。

  • 修改展现类中访问函数,将他们的操作修改对象改为领域函数对象(而非GUI组件)。

  • 修改Observer的update(),使其从相应的领域对象中将所需数据赋值给GUI组件。

  • 编译测试。

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

两个类都 需要使用对方特性,但其间只有一条单向连接。添加一个反向指针,并使修改函数能够同时更新两条连接。

做法

  • 在被引用类中添加一个字段,用以保存反向指针。
  • 决定由哪个类--引用端还是被引用端--控制关联关系。
  • 在被控端建立一个辅助函数,其命名应该清楚指出他的有限用途。
  • 如果既有的修改函数在控制端,让它负责更新反向指针。
  • 如果既有的修改函数在被控端,就在控制端建立一个控制函数,并让既有的修改函数调用这个新建的控制函数。
8.8 Change Bidirectional Association to Unidirectional (将双向关联改为单向关联)

两个类之间有双向关联,但其中一个类如今不再需要另一个类 的特性,去除不必要的关联。

做法

  • 找出保存“你想去除的指针”的字段,检查它的每个用户,判断是否可以去除该指针。

    不但要检查直接访问点,也要检查调用这些直接访问点的函数。

    考虑有无可能不通过指针取得被引用对象,如果有可能,你就可能对取值函数Substitute Algorithm(139),从而让客户在没有指针的情况下,也可使用该取值函数。

    对于使用该字段的所有函数,考虑将被引用悐作为参数传进去。

  • 如果客户使用了取值函数,先运用Self Encapsulate Field(171)将待删除字段自我封装起来,然后使用Substitute Algorithm对付取值函数,令他不在使用该字段。然后编译测试。

  • 如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后移除该字段。

  • 编译,测试、

8.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)

你有一个字面数值,带有特别含义。创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。

动机

  • 在计算科学中,魔法数是历史悠久的不良现象之一。
  • 常量不会造成任何性能开销,却可以大大提高代码可读性。

做法

  • 声明一个常量,令其值为原本的魔法数值。
  • 找出这个魔法数的所有引用点。
  • 检查是否可使用这个新声明的常量来替换该魔法数。如果可以,便以此常量替换纸。
  • 编译。
  • 所有魔法数都被替换完毕后,编译并测试。此时整个程序应该运转如常,就像没做任何修改一样。
8.10 Encapsulate Field (封装字段)

类中存在一个public字段,将它声明为private,并提供相应的访问函数。

动机

  • 面向对象的首要原则之一就是封装,或是“数据隐藏”。据此原则,不应该将数据声明为public。这样还会降低程序的模块化程度。数据和使用改数据的行为如果集中在一起,代码修改就比较简单。

做法

  • 为public字段提供取值、设值函数

  • 找到这个类以外使用该子弹的所有地点。如果客户只是读取该字段,就把引用替代为对取值函数的调用;如果客户修改了该字段值,就将此引用点替换为对设值函数的调用。

    如果这个字段是个对象,而客户只不过是调用该对象的某个函数,那么无论该函数是否改变对象状态,都是能算是读取该字段,只有当客户为该字段赋值时,才能将其替换为设置函数。

  • 每次修改之后,编译并测试。

  • 将字段的所有用户修改完毕后,把字段声明,为private。

  • 编译,测试。

8.11 Encapsulate Collection(封装集合)

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

做法

  • 加入为集合添加、移除元素的函数。

  • 将保存集合的字段初始化为一个空集合。

  • 编译。

  • 找出集合设值函数的所有调用者。你可以修改那个设置函数。让它使用上述新建的“添加、移除元素”函数;也可以直接修改调用端,改让他们调用上述新建立的“添加、移除元素”函数。

    两种情况下需要用到集合设置函数:(1)集合为空时;(2)准备将原有几何替换为另一个集合时。

    你或许会想运用Rename Method(273)为集合设值函数改名:从setXXX()改为initializeXXx()或replaceXxx();

  • 编译测试。

  • 找出所有“通过取值函数获得集合并修改其内容”的函数。注意修改这些函数,让他们改用添加、移除函数。每次修改后,编译并测试。

  • 修改完上述所有“通过取值函数获得集合并修改集合内容”的函数后,修改取值函数自身,使它返回该集合的一个只读副本。

  • 编译测试。

  • 找出取值函数的所有用户,从中找出应该存在于集合所属对象内的代码。运用Extract Method(110)和Move Method(142)将谢谢代码移到宿主对象去。

  • 修改现有取值函数的名字,然后添加一个新取值函数,使其返回一个枚举。找出旧取值函数的所有被使用点,将他们都改为使用新取值函数。

  • 如果这一步跨度太大,你可以先使用Rename Method(273)修改原取值函数的名称;在建立一个新取值函数用以返回枚举;最后在修改所有调用者,使其调用新取值函数。

  • 编译测试。

8.12 Replace Record With Data Class (以数据类取代记录)

你需要面对传统编程环境中的记录结构,为该记录创建一个“哑”数据对象。

做法

  • 新建一个类,表示这个记录。
  • 对于记录中的每一项数据,在新建的类汇总建立对象的一个private子弹,并提供相应的取值、设值函数。
8.13 Replace Type Code With Class(以类取代类型码)

类之中有一个数值类型码,但它并不影响类的行为。以一个新的类替换该数值类型码。

动机

  • 任何switch语句都应该运用Replace Conditional With Polymorphism(255)去掉。

做法

  • 为类型码建立一个类。

    这个类需要一个用以记录类型码的字段,字类型应该和类型码相同,并应该有对应的取值函数,此外,还应该用一组静态变量保存允许被创建的实例,并以一个静态函数根据原本的类型码返回合适的实例。

  • 修改源类实现,让它使用上述新建的类。

    维持原先以类型码为基础的函数接口,但改变静态字段,以新建的类产生代码,然后,修改类型码相关函数,让他们也从新建的类中获取类型码。

  • 编译,测试。

    此时,新建的类可以对类型码进行运行期检查。

  • 对于源类中每一个使用类型码的函数,相应建立一个函数,让新函数使用新建的类。

  • 逐一修改源类用户,让他们使用新接口。

  • 每修改一个用户,编译并测试。

  • 删除使用类型码的旧接口,并删除保存旧类型码的静态变量。

  • 编译测试。

8.14 Replace Type Code with Subclass(以子类取代类型码)

你有一个不可变的类型码,它会影响类的行为,以子类取代这个类型码

做法

  • 使用self Encapsulate Field(171)将类型码自我封装起来。

    如果类型码被传递给构造函数,就需要将构造函数换成工厂函数。

  • 为类型码的每一数值建立一个相应的 子类,在每个子类中覆写类型码的取值函数,使其返回相应的类型码值。

  • 每建立一个新的子类,编译并测试。

  • 从超类中删除保存类型码的字段。将类型码访问函数声明为抽象函数。

  • 编译测试。

8.15 Replace Type Code with State /Strategy(以State/Strategy取代类型码)

你有一个类型码,她会影响类的行为,但你无法通过继承手法消除它。以状态对象取代类型码。

做法

  • 使用Self Encapsulate Field(171)将类型码自我封装起来。
  • 新建一个类,根据类型码的用途为它命名。这是一个状态对象。
  • 为这个新类添加子类,每个子类对应一种类型码、
  • 在超类中建立一个抽象的查询函数,用以返回类型吗。在每个子类中覆写该函数,返回确切的类型码。
  • 编译。
  • 在源类中建立一个字段,用以保存新建的状态对象。
  • 调整源类中负责查询类型码的函数,将查询动作转发给状态对象。
  • 调整源类中为类型码设值的函数,将一个恰当的状态对象子类赋值给“保存编译对象”的那个字段。
  • 编译,测试。
8.16 Replace Subclass With Field(以字段取代子类)

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

动机

  • 建立子类的目标是为了增加新特性或变化其行为。有一种变化行为称为“常量函数”,他们会返回一个硬编码的值。可以让不同的子类中的同一个访问函数返回不同的值。在超类中将访问函数声明为抽象函数,并在不同的子类中让它返回不同的值。

做法

  • 对所有子类使用replace constructor with Factory method(304)
  • 如果有任何代码直接引用子类,令他改为引用超类;
  • 针对每个常量函数,在超类中声明为一个final字段。
  • 为超类声明一个protected构造函数,用以初始化这些新增字段。
  • 新建或修改子类构造函数,使他调用超类的新增构造函数;
  • 编译、测试
  • 在超类中实现所有常量函数,令他们返回相应的字段值,然后将该函数从子类中删除。
  • 每删除一个常量函数,编译并测试。
  • 将子类删除。
  • 重复内联构造函数,删除子类过程,直到所有子类都被删除。