0%

008-重新组织数据

摘要:对象应该直接访问其中的数据,还是通过访问函数来访问?

  • 常选择“直接访问”方式,任何时候进行这项重构(self encapsulate field)都是很简单的
  • eplace value with object:将“哑”数据变成善表达的对象
  • hange value to reference:有太多地方需要这一类对象,可用此方法将其变为引用对象
  • eplace array with object:看到一个数组的行为方式很像一个数据结构。将数组变成对象,从而使这个数据结构更清晰地显露出来。move method,为这个对象加入相应行为,真正的好处才得以体现
  • eplace magic number with symbolic constant:处理魔法数(带有特殊含义的数字)
  • hange unidirectional association to bidirectional:将对象之间的单向关联变成双向关联
  • hange bidirectional association to undirectional:将双向关联变成单向关联
  • uplicate Observed data:GUI类处理不该其处理的业务:将处理业务逻辑的行为移到合适的领域类去,需要在领域类中保存这些逻辑的相关数据。一般,不喜欢重复数据,但这是一个意外,这里的重复数据不可避免
  • ncapsulate field:封装类中公开的数据
  • ncapsulate collection:封装类中公开的集合。集合有其特殊协议
  • eplace recorde with data class:如果一整条记录都被裸露在外
  • ype code:类型码,特殊数值。“与实例所属之类型相关的某些东西”。通常以枚举形式出现。通常以static final 整数实现。
  • eplace type code with class:应用于:用来表现某种信息,且不会改变所属类型的行为。可以提供更好的类型检查和更好的平台(在未来方便地将相关的行为添加进去)
  • eplace type code with subclasses :如果当前类型的行为受到类型码的影响
  • eplace type code with state/strategy:更复杂的情况

self encapsulate field:自封装字段

  • 问题:直接访问一个字段,但是字段之间的耦合关系逐渐变得笨拙
  • 解决:为这个字段建立取值、设值函数,并且只以这些函数来访问字段
  • 动机:
    • “间接访问变量”。好处:子类可以通过覆写一个函数,改变获取数据的途径;支持更灵活的数据管理方式(延迟初始化)
    • “直接访问变量”。好处:代码比较容易阅读。
    • 通常,先使用直接访问方式,直到这种方式会带来麻烦,接着转而使用间接访问方式。重构给了改变主意的自由
    • 使用 self encapsulate field:
      • 想访问超类中的一个字段,却又想在子类中将这个变量的访问改为一个计算后的值
      • “字段的自我封装” 只是第一步。完成自我封装之后,可以在子类中根据自己的需要随意覆写set/get函数
  • 做法:
    • 可以暂时将字段改名,让编译器帮助查找引用点
    • 将字段声明为private

replace data value with object:以对象取代数据值

  • 问题:有一个数据项,需要与其他数据、行为一起使用才有意义
  • 解决:将数据项变成对象
  • 动机:
    • 开发初期,往往决定以简单的数据项表示简单的情况。随着开发的进行,发现这些简单数据项不再简单。散发duplicate code/feature envy的代码坏味道,则需要将数据值变成对象
    • 例:开始,用字符串表示“电话号码”。随后,电话号码需要“格式化”、“抽取区号”之类的特殊行为。
  • 做法:
    • 为待替换数值新建一个类,在其中声明一个final字段,类型和源类中的待替换数据类型一样;在新类中加入这个字段的取值函数;新类中加上一个接受此字段为参数的构造函数
    • 将源类中的待替换数值字段的类型改为新建的类
    • 修改源类的该字段的取值函数,令其调用新类的取值函数
    • 如果源类构造函数中用到这个待替换字段(多半是赋值动作),就修改构造函数,令其改用新类的构造函数来对字段进行赋值
    • 修改源类中的待替换字段的设值函数,令其为新类创建一个实例(值对象应该是不可修改的内容。可以避免一些讨厌的别名问题)
    • 可能需要对新类使用change value to reference

change value to reference:将值对象改为引用对象

  • 问题:从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象
  • 解决:将这个值对象变为引用对象
  • 动机:
    • 引用对象:可以直接==,来验证对象同一性
    • 值对象:覆写equal()、hashCode()
    • 应用:确保对任何一个对象的修改,都能影响到所有引用此对象的地方。需要将此对象变成一个引用对象
  • 做法:
    • 使用replace constructor with factory method
    • 决定由什么对象负责提供访问新对象的途径
      • 可能是一个静态字典,或一个注册表对象
      • 可以使用多个对象作为新对象的访问点
    • 决定这些引用对象应该预先创建好,还是应该动态创建
      • 如果引用对象是预先创建好的,且必须从内存中将其读取出来。就需确保它们在被需要的时候能够被及时加载
    • 修改工厂函数,令它返回引用对象
      • 如果对象是预先创建好的,需要考虑:万一有人索取一个其实并不存在的对象,要如何处理错误
      • 可能希望对工厂函数使用rename method,使其传达:它返回的是一个既存对象

change reference to value:将引用对象改为值对象

  • 问题:一个引用对象,很小,且不可变,且不易管理
  • 解决:将其变成一个值对象
  • 动机:
    • 引用对象开始变得难以使用。
      • 引用对象必须被某种方式控制,总是必须向其控制者请求适当的引用对象
      • 可能造成内存区域之间错综复杂的关联
      • 在分步系统、并发系统中,不可变的值对象特别有用,无需考虑它们的同步问题
    • 值对象的一个非常重要的特性:不可变
      • 保证了,可以放心地以多个对象表示同一事物
      • 不代表不能改变。如果要改变,需要用新的对象来取代现有对象,而不是在现有对象上修改。值对象自身不能修改,但是可以修改其他对象与值对象之间的关系
  • 做法:
    • 检查重构目标是否为不可变对象,或是否可修改为不可变对象
      • 如果改对象目前还不是不可变的,使用remove setting method,直到其成为不可变的为止
      • 如果无法将对象修改为不可变的,就放弃使用本项重构
    • 建立equal()、hashcode()。这两个函数的修改必须同时进行,负责依赖hash的任何集合对象(hashtable、hashset、hashmap……)都可能产生意外行为
    • 考虑是否可以删除工厂函数,并将构造函数声明为public
  • 注意:要把一个引用对象变成值对象,关键动作:检查是否不可变。
    • 如果不是,就不能使用本项重构。
    • 可变的值对象会造成烦人的别名问题

replace array with object:以对象取代数值

  • 问题:由一个数值,各元素各自代表不同的东西、
  • 解决:以对象替换数组。对于数组中的每个元素,以一个字段来表示

change unidirectional association to bidirectional将单向关联改为双向关联

  • 场合:两个类·····都需要使用双方特性,但其间只有一条单向连接
  • 方式:添加一个反向指针,并使修改函数能够同时更新两条连接
  • 动机:
    • 开发初期,可能会在两个类之间建立一条单向连接,使其中一个类可以引用另一个类。随着时间推移,引用类需要得到其引用者,以便进行某些处理。即需要一个反向指针
    • 指针是一种单向连接,不可能反向操作。可以绕道而行,耗费一些计算时间,成本还算合理,然后在被引用类中建立一个函数专门负责此行为
    • 有时候绕过这个问题并不容易,此时需要建立双向引用关系,即反向指针
    • 如果使用不当,反向指针很容易造成混乱;但只要习惯了这种手法,其实并不复杂
    • 反向指针,不好实现。在熟练运用前,应该有相应的测试。
    • 通常不会花心思测试访问函数,因为普通访问函数的风险没有高到需要测试的地步。但本重构要求测试访问函数,是极少数需要添加测试的重构手法之一
  • 做法:
    • 在被引用类中增加一个字段,用以保存反向指针
    • 决定由哪个类:引用端?被引用端? 控制关联关系
      • 如果两者都是引用对象,而其间的关联是“一对多”关系。那么就由“拥有单一引用”的那一方(N)承担“控制者”角色。
      • 如果某个对象是组成另一对象的不见,那么由后者负责控制关联关系
      • 如果两者都是引用对象,而关联关系是“多对多”关系,那么随便其中一个对象来控制关联关系都无所谓
    • 在被控端建立一个辅助函数,其命名应该清楚指出它的有限用途。
      • 尽量降低可见程度:最小范围内可见
    • 如果既有的修改函数在控制端,让它负责更新反向指针
    • 如果既有的修改函数在被控端,就在控制端及那里一个控制函数,并让既有的修改函数调用这个新建的控制函数
      • 基本形式:先让对方删除指向你的指针,再将你的指针指向一个新对象,最后让那个新对象把它的指针指向你

change bidirectional association to unidirectional将双向关联改为单向关联

  • 场合:两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性
  • 方式:去除不必要的关联
  • 动机:
    • 双向关联很有用,但是必须为其付出代价。维护双向关联;确保对象被正确创建、删除而增加的复杂度;很多程序员并不习惯使用双向关联,往往成为错误之源
    • 大量的双向连接也很容易造成“僵尸对象”:某个对象本来应该已经死亡,却仍然保留在系统中,因为对其的引用还没有完全清除
    • 双向关联迫使两个类直接有了依赖:对其中任一个类的任何修改,都可能引发另一个类的变化。如果两个类位于不同的包,这种依赖就是包与包之间的相依。过多的跨包依赖会造成紧耦合系统,使得任何一点小小改动都可能造成许多无法预知的后果
    • 只有真正需要双向关联的时候,才应该使用它,如果发现双向关联不再存在价值,就应该去掉其中不必要的一条关联
  • 做法:
    • 找出“想去除的指针”的字段,检查它的每一个用户,判断时候可以去除改指针
      • 不但要检查直接访问点,也要检查调用这些直接访问点的函数
      • 考虑有无可能不通过指针取得被引用的对象。如果有可能,就可以对取值函数使用substitute algorithm,从而让客户在没有指针的情况先也可以使用改取值函数
      • 对于使用该字段的所有函数,考虑将被引用对象作为参数传进去
    • 如果客户使用了取值函数,先运用self encapsulate field将待删除字段自我封装起来,然后使用substitute algorithm对付取值函数,令它不再使用该字段
    • 如果客户并未使用取值函数,直接修改待删除字段的所有被引用点,改以其他途径获得该字段所保存的对象
    • 如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后移除该字段
      • 如果有许多地方对次字段赋值,先运用self encapsulate field使这些地点改用同一赋值函数。而后将这个设值函数的本体清空。接着就可将此字段、设值函数、及其所有调用全部移除
  • 注意:
    • 最困呐的地方:检查可行性。如果知道本重构是安全的,则重构手法自身十分简单。

replace magic number with symbolic constant:以字面常量取代魔法值

encapsualte collection 封装集合

  • 场合:有一个函数返回一个集合
  • 方式:让这个函数返回该集合的一个只读副本,并在这个类中提供添加、移除集合元素的函数
  • 动机:
    • 集合的处理方式应该和其他种类的数据略有不同。
    • 取值函数不该返回集合本身:这将会让用户得以修改集合内容,而集合拥有者却一无所知;对用户暴露过多对象内部的数据接口信息
    • 如果一个取值函数确实需要返回多个值,应该避免用户直接操作对象内所保存的集合,并隐藏对象内与用户无关的数据结构。
    • 不应该为整个集合提供一个设值函数,但应该为集合加上添加、移除元素的函数。集合拥有者可以控制集合元素的添加、移除
    • 降低集合拥有者、用户间的耦合度
      做法:
  • 为集合加上添加、移除元素的函数
  • 将保存集合的字段初始化为一个空集合
  • 找出集合设值函数的所有调用者。修改那个设值函数,使其使用上述新建立的“添加、移除元素”的函数;也可以直接修改调用端,让其使用上述新建立的“添加、移除元素”的函数
    • 两种情况下,需要用到集合设值函数:1、集合为空;2、准备将原有集合替换为另一个集合时
    • 也能用rename method,为集合设值函数改名,从set()改为initialize()或replace***()
  • 找出所有“通过取值函数,获得集合,并修改其内容的函数”,逐一修改这些函数,改用述新建立的“添加、移除元素”的函数
  • 修改取值函数本身,使其返回该集合的一个只读副本
  • 找出取值函数的所有用户,找出应该存在与集合所属对象内的代码。用extract method、remove method将代码移到宿主对象去
  • 修改现有取值函数的名字,添加一个新的取值函数,使其返回一个枚举。找出旧取值函数的所有被使用点,改用新取值函数
    • 跨步过大,可使用rename method修改原有取值函数的名称;再建立一个新取值函数,用以返回枚举;最后再修改所有调用者,使其调用新取值函数。

      replace type code with subclasses:以子类取代类型码

  • 问题:有一个不可变的类型码,会影响类的行为
  • 解决:以子类取代这个类型码
  • 动机:
    • 面对的类型码不会影响宿主类的行为,使用replace type code with class来处理。
    • 如果类型码会影响宿主类的行为,最好的办法是借助多态来处理变化行为
      • 一般,这种情况的标志就是向switch这样的表达式、if-then-else结构……检查类型码的值,根据不同的值执行不同的动作。应该以replace conditional with polymorphism进行重构。
      • 为了能够顺利进行那样的重构,首先应该将类型码替换为可拥有多态行为的继承体系。这样的一个继承体系应该以类型码的宿主类为基类,并针对每一种类型码各建立一个子类
    • 为了建立这样的一个继承体系,最简单的方法:replace type code with subclasses。以类型码的宿主类为基类,针对每种类型码建立相应的子类
    • 以下情况不能这么做:1.类型码值在对象创建之后发生了改变;2.由于某些原因,类型码宿主类已经有了子类。
      • 使用replace type code with state/stragegy
    • replace type code with subclasses:主要作用是搭建一个舞台,让replace conditional with polymorphism得以一展身手。
    • 如果宿主类中并美哟出现条件表达式,replace type code with class 更合适,风险更低
    • 使用replace type code with subclasses另一个原因:宿主类中出现了“只与具备特定类型码之对象有关”的特性。可以使用push down method、push down field将这些特性推到合适的子类去,以彰显只与特定情况相关这一事实
    • replace type code with subclasses好处:把“对不同行为的了解”从类用户那儿转移到了类自身。如果需要再加入新的行为变化,只需添加一个子类就行了。如果没有多态机制,就必须找到所有条件表达式,并逐一修改它们=》如果未来还有可能加入新行为,这项重构将特别有价值
  • 做法:
    • 使用self encapsulate field将类型码自我封装起来
      • 如果类型码被传递给构造函数,就需要将构造函数换成工厂函数
    • 为类型码的每一个数值建立一个相应的子类。在每个子类中覆写类型码的取值函数,使其返回相应的类型码值
      • 这个值被硬编码于return中(例如:return 1).看起来很肮脏,但只是权益之计,当所有case子句都被替换后,问题就解决了
    • 每建立一个新的子类,编译并测试
    • 从超类中删掉保存类型码的字段,将类型码访问函数声明为抽象函数
    • 注意:避免使用switch语句。这里只有一处用到switch语句,并且只用于决定创建何种对象,这样的switch语句是可以接受的

replace type code with state/strategy

  • 问题:有一个类型码,会影响类的行为,但无法通过继承手法消除它
  • 解决:以状态对象取代类型码
  • 动机:
    • 与replace type code with subclasses很相似。但如果“类型码值在对象生命周期中发生变化”或“其他原因使得宿主类不能被继承”,可以使用本重构(使用state模式、strategy模式)
    • state模式、strategy模式非常相似,无论选择其中哪一个,重构过程都是相同的。“选择哪一个模式”并非问题关键所在,只需要选择更合适特定情境的模式就行了
      • strategy模式适合:在本项重构之后再以replace conditional with polymorphism简化一个算法
      • state模式适合:搬移与状态相关的数据,而且把新建对象视为一种变迁状态
  • 做法:
    • selfe encapsulate field将类型码自我封装起来
    • 新建一个类,根据类型码的用途为它命名,就是一个状态对象
    • 为这个新类添加子类,每个子类对应一种类型码
      • 比起逐一添加,一次性加入所有必要的子类可能更简单写
    • 在超类中建议一个抽象的查询函数,用以返回类型码。在每个子类中覆写该函数,返回确切的类型码
    • 在源类中建立一个字段,用以保存新建的状态对象
    • 调整源类中负责铲鲟类型码的函数,将查询动作状态转发给状态对象
    • 调整源类中为类型码设值的函数,将一个恰当的状态对象子类赋值给“保存状态对象”的那个字段

replace subclass with field:以字段取代子类

  • 问题:各个子类的唯一差别,只在“返回常量数据”的函数身上
  • 解决:修改这些函数,使其返回超类中的某个(新增)字段,然后销毁子类
  • 动机:
    • 建立子类的目的:为了增加新特性、变化其行为。
    • 有一种变化行为被称为“常量函数”constant method,会返回一个硬编码的值。
      • 有其用途,可以让不同的子类中的同一个访问函数返回不同的值。可以在超类中将访问函数声明为抽象函数,并在不同的子类中让它返回不同的值
    • 尽管常量函数有其用途,但若子类中只有常量函数,实在没有足够的存在价值。可以在超类中设计一个与常量函数返回值相应的字段,从而完全去除这样的子类=》避免因继承而带来的额外的复杂性
  • 做法:
    • 对所有子类使用replace constructor with factory method
    • 如果有任何代码直接引用子类,令它改而引用超类
    • 针对每个常量函数,在超类中声明一个final 字段
    • 为超类声明一个protected构造函数,用以初始化这些新增字段
    • 新建、修改子类构造函数,使其调用超类的新增构造函数
    • 在超类中实现所有常量函数,令其返回相应字段值,然后将函数从子类中删掉
    • inline method将子类构造函数内联到超类的工厂函数中
    • 删除子类
一分也是爱,两分情更浓【还没有人赞赏,支持一下呗】