摘要:对象应该直接访问其中的数据,还是通过访问函数来访问?
- 常选择“直接访问”方式,任何时候进行这项重构(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将代码移到宿主对象去
- 修改现有取值函数的名字,添加一个新的取值函数,使其返回一个枚举。找出旧取值函数的所有被使用点,改用新取值函数
- 问题:有一个不可变的类型码,会影响类的行为
- 解决:以子类取代这个类型码
- 动机:
- 面对的类型码不会影响宿主类的行为,使用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语句是可以接受的
- 使用self encapsulate field将类型码自我封装起来
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将子类构造函数内联到超类的工厂函数中
- 删除子类