• 第5章 重构列表
  • 第6章 重新组织函数
  • 第8章 重新组织数据
  • 第9章 简化条件表达式 237
  • 第10章 简化函数调用
  • 第11章 处理概括关系 319

      5.1 重构的记录格式103

      这个格式可以作为自己未来记录重构手法的标准,方便查阅自己常用的和总结的重构方式:

      • 名称:重构词汇表
      • 概要:适用的情景,以及所做的事情
      • 动机:说明“为什么需要”和“什么情况不做”
      • 做法:重构的步骤(看似简单,其实很重要,因为重构不建议跳跃完成,最好保证每次一小步都是正确的)
      • 范例:通过例子正确的理解重构手法的操作方式

      第6章 重新组织函数
      • Extract Method是本章的重点,用于解决Long Methods问题;
      • Extract Method的困难是处理局部变量,特别是局部变量中的临时变量,为此需要本章中的其他方法作为补充。

      6.1 (P110)Extract Method(提炼函数)

      动机:Long Methods问题或者代码需要Comments问题;
      方法:(可以使用IDE提供的重构工具,下面的具体操作的原理)

      • 创造一个新函数,根据这个函数的意图来对它命名;
      • 将提炼的代码拷贝到新建函数中;
      • 检查提炼的代码是否引用了“作用域限于原函数”的变量(局部变量、原函数参数)
      • 检查提炼的代码是否包含了“仅用于被提炼代码段”的临时变量
        • 如果有,在目标函数中将之声明为局部变量
      • 检查被提炼代码段,看看是否有任何局部变量的值被他改变,
        • 如果临时变量的值被修改了,尝试将提炼的代码变为一个查询,将结果返回给相关变量;
        • 如果不好操作,或者被修改的变量多于一个,可以尝试(分解临时变量/以查询替换变量)等手段了。
      • 将被提炼代码段中需要被读取的局部变量,当参数传给目标函数
      • 处理完所有局部变量后,编译,检查
      • 在源函数中,将被提炼的代码替换为对目标函数的调用
      • 编译,检查,测试

      7.1 Move Method(搬移函数)142

      动机:类中某个函数与其他类交互过多
      方法:将该函数搬移到交互最多的类里面,将旧函数变成委托函数或者删除。
      具体方法:

      • 检查源类中被源函数使用的一切特性,如果特性被其他函数使用,考虑这些函数一起搬移
      • 检查源类的子类和超类,看看是否有该函数的声明,如果出现,很可能不能搬移。
      • 目标类需要使用源类的特性:
        1. 将该特性转移到目标类;
        2. 建立目标类到源类之间引用。
        3. 将源类作为参数传给目标类
        4. 将该特性作为参数传给目标类
      • 如果源函数包含异常处理,需要考虑是在目标类还是源函数处理

      7.2 Move Field(搬移字段)146

      动机:类中某个字段被其他类频繁使用(包括:传参数、调用取值函数、调用设值函数)
      方法:将该字段搬移到目标类
      具体方法:

      • 先封装这个字段;
      • 在目标类建立这个字段,并且封装;
      • 设定目标对象;
      • 替换对源字段的引用为目标类的取值函数

      7.3 Extract Class(提炼类)149

      动机:一个类做了两个类的事
      方法:

      • 建立新类,将相应的字段和函数放到新类
      • 使用Move Field重构;
      • 使用Move Method重构;
      • 判断是否需要公开新类。

      7.8 Introduce Local Extension(引入本地扩展)164

      动机:使用的类无法提供多个功能,但是又不能修改该类
      方法:建立新的类,在新类中建立需要的功能函数,可以作为服务类的子类实现新的类,也可以包装服务类实现新的类。
      具体情况:

      • 首选子类,工作量最小
        • 但是必须在对象创建期实施,如果不行就只能选择包装类;
        • 子类的对象不能修改父类的数据,否则建议选择包装类,因为会导致父类对象与子类对象的数据可能不一致
      • 包装类需要实现被包装对象的所有接口,工作量很大。

      8.1 Self Encapsulate Field(自封装字段)171

      动机:直接访问一个字段,但是字段之间的耦合关系逐渐变得笨拙。
      方法:自封装就是在对于类内部的字段也封装一个设值取值的函数。
      争论:字段访问方式是直接访问还是间接访问一致争论不断
      间接访问的好处:

      • 子类可以通过覆盖一个函数来改变获取数据的途径;
      • 支持更灵活的数据管理,如延迟加载(需要用到才加载)等。直接访问的好处:代码容易读懂,理解不需要转换为取值函数。

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

      动机:一个类有许多相等的实例,希望把这些相等的实例统一为一个对象,方便统一修改或者进行相等性比较
      方法:将值对象变成引用对象
      “引用对象”与“值对象”的区别:

      • 每个引用对象代表着现实中一个对象,使用对象的一致性用来检测两个对象是否相等,即(==)
      • 值对象完全由其自身的值来相互区分,需要重写一些方法用来检测两个对象是否相等。(重写equals()和hashcode()方法)具体方法:
      • 需要使用工厂模式来创建对象
      • 需要另一个对象(或者是自身)作为访问点来访问定义的引用对象,对象用Dictionary或者HashTable来保存对象
      • 决定对象是预先创建还是动态创建

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

      动机:引用对象,很小且不可变,而且不易管理

      • 很小:创建许多也不会消耗太多内存
      • 不可变:不需要复杂的管理代码,也不需要考虑同步问题,还会造成别名问题具体方法:
      • 检查重构目标是否是不可变对象或者可修改成不可变对象
        • 使用Remove Setting Method变成不可变对象
        • 如果无法修改成不可变对象,就放弃重构
      • 重写hashCode和equals()方法
      • 取消使用的工厂模式,并将对象的构造函数设为public

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

      动机:两个类相互之间都需要对方的数据,但是相互之间只有一条单向的连接
      这个重构需要添加测试,因为“反向指针”很容易造成混乱。
      具体方法:

      • 在被引用类添加字段,保存引用类的指针;
      • 判断由哪个类来控制关联关系;
        • 如果两者都是引用对象,且关联关系为“一对多”的关系,那么就由“拥有单一引用”的对象作为控制者;
        • 如果A对象是B对象的部件,则由B对象负责控制关系;
        • 如果两者都是引用对象,且关联关系为“多对多”的关系,那么随意确定一个对象作为控制者。
      • 在被控端建立辅助函数,命名清晰地描述其用途;
        • 如果修改函数在控制端,则由其负责更新反向指针;
        • 如果修改函数在被控制端,则在控制端建立一个修改反射指针的函数,由修改函数调用其修改反向指针。
        • 两者是一对多关系,有单一引用承担控制关联关系责任

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

      动机:两个类有双向关联,但是一个类不再需要另一个类的特性
      原因:

      • 双向关联可能造成僵尸对象,不能被清除释放内存。
      • 使两个类存在耦合关系,一个类的变化会导致另一类的变化。方法:去除双向关联
        困难:检查可行性

      8.11 Encapsulate Collection(封装集合)208

      动机:类中使用集合,但是集合不能提供给用户直接操作,而是提供函数操作集合,降低用户与集合之间的耦合度
      方法:提供函数返回集合的只读副本,并提供增加和删除集合元素的函数
      具体方法:

      • Java2:封装Set
      • Java1.1:封装Vector
      • 封装数组

      9.1 Decompose Conditional(分解条件表达式)238

      动机:if-then-else语句,不同分支做不同事动机成大型函数,本身就难以阅读,尤其在带有复杂条件的逻辑中。方法:

      • 将if语句提炼为函数
      • 将then和else段落提炼为函数
      • 对于存在嵌套的条件逻辑,先判断是否可以用Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)消除。不行再分解每个条件

      9.2 Consolidate Conditional Expression(合并条件表达式)240

      动机:有一系列条件判断都服务于共同的目标
      方法:将这些条件判断合并为同一个表达式,再将这个表达式提炼为独立函数
      原因:

      • 只是一次条件检查,只是存在多个并列条件需要检查而已
      • 为Extract Method(提炼函数)做准备,通过函数名告知“为什么这么做”

      9.4 Remove Control Flag(移除控制标记)245

      动机:在循环执行的程序段中,某个变量定义为判断条件中的控制标记(control flag),增加了代码理解的复杂度
      方法:

      • 以break或者continue代替;
      • 也可以通过函数调用和return语句来实现。

      第10章 简化函数调用

      使接口变得更加简洁易用的重构方法。

      • 修改函数名称,使之容易理解;
      • 缩短参数列表;
      • 不同的功能分离到不同的函数中;
      • 隐藏函数,提升接口的质量。

      10.2 Add Parameter(添加参数)275

      动机:被调用的函数需要从调用函数中得到更多的信息
      方法:为被调用的函数添加参数
      抉择:

      • 现有参数是否提供足够的信息?
      • 这个函数是否应该移动到拥有该信息的对象中?
      • 加入新参数是否合适?
      • 如果需要的参数过多,是否需要使用Introduce Parameter Object(引入参数对象)?

      10.3 Remove Parameter(移除参数)277

      动机:函数不需要某个参数(不需要了就放弃,保留也需要付出代价)
      方法:

      • 如果是独立的函数,直接将该参数移除
      • 如果是多态函数,不能移除,就增加一个新的没有这个参数的函数,使调用者的工作得到简化

      10.4 Separate Query from Modifier(将查询函数和修改函数分离)279

      动机:某个函数既修改对象状态,又返回对象状态值。(使调用者担心误操作修改了不应该修改的数据,增加调用者的操作负担)
      本质:函数功能简洁、明确,如果一个函数具备多个功能,就把它们分离成多个函数。
      方法:建立两个不同的函数,其中一个负责查询,另一个负责修改。
      原则:

      • 任何一个有返回值的函数都不应该有看得到的副作用。
      • 编码中主要考虑的不是代码的效率,而是代码的易读性,效率可以在未来上线的时候再根据实际需要调整。多线程:将修改和查询函数封装在一个同步函数中分开调用。

      10.9 Introduce Parameter Object(引入参数对象)295

      动机:有些参数总是自然地同时出现
      方法:用一个对象把这些参数包装起来进行传递
      目的:

      • 缩短参数列表长度;
      • 函数具有一致性,降低理解和修改代码的难度

      10.10 Remove Setting Method(移除设值函数)300

      动机:类的某个字段应该对象创建的时候被设置,然后不再改变
      方法:去掉该字段的设置函数

      • 如果对参数的运算很简单,而且只有一个构造函数,就可以直接在构造函数中初始化。
      • 如果修改复杂,或者有多个函数试图改变这个字段,那么就需要提供一个独立函数,并给予独立函数一个清楚表达用途的名字
      • 如果是子类希望修改超类的字段
        • 那么最好是使用超类的构造器实现改变;
        • 或者通过拥有能够清楚表达用途的名字的函数来实现。
      • 如果修改集合字段,请使用Encapsulate Collection(208)实现。

      10.12 Replace Constructor with Factory Method(以工厂函数取代构造函数)304

      动机:创建对象时不仅仅是做简单的构建动作方法:将构造函数替换为工厂模式范例:

      • 根据整数(实际是类型码)创建对象;
      • 根据字符串创建子类对象;
      • 以函数创建子类;

      10.14 Replace Error Code with Exception(以异常取代错误码)310

      动机:某个函数返回一个特定的代码,表示某个错误的情况
      方法:取消那个代码判断,改用抛出异常
      范例:

      • 非受控异常:使用守卫语句检查这个异常情况;
      • 受控异常:需要修改的调用者函数和被调用者函数,步骤太大,容易出错。可以先创建一个临时的中间函数,保留原函数,使所有的调用都改为新函数后,删除原函数,再修改新函数名称,即可。

      11.1 Pull Up Field(字段上移)320

      动机:两个子类拥有相同的字段
      方法:

      • 将该字段移动到超类,去除重复数据声明;
      • 将使用该字段的行为搬移到超类,去除关于这个字段的重复行为。
      • 考虑对超类的该字段使用Self Encapsulate Field(171)

      11.2 Pull Up Method(函数上移)322

      动机:有些函数,在各个子类产生相同的结果。
      方法:

      • 将该函数移动到超类
      • 如果被提升的函数引用了子类中的函数
        • 如果可以将引用函数提升,就一起提升
        • 如果不可以将引用函数提升,可以在超类里面那个抽象函数

      11.3 Pull Up Constructor Body(构造函数本体上移)325

      动机:你在各个子类拥有一些构造函数,它们的本地几乎完全一致
      方法:在超类新建一个构造函数,并在子类构造函数中调用它。
      具体方法:

      • 将共同代码放在子类构造函数起始处,然后再复制到超类构造函数中。
      • 将子类构造函数中共同代码删除,改用调用新建的超类构造函数。

      11.6 Extract Subclass(提炼子类)330

      动机:类中的某些特性只被部分实例用到。
      方法:新建一个子类,将上面所说的那一部分特性移到子类中。
      具体情况:

      • 并不是出现类型码就表示需要用到子类,可以在委托和继承之间做选择。
      • 为子类新建构造函数,
        • 子类构造函数与超类构造函数拥有相同的参数列表,并且直接调用超类构造函数
        • 如果需要隐藏子类,可使用Replace Constructor with Factory Method(以工厂函数取代构造函数)
      • 找出超类调用点
        • 如果超类构造函数与子类不同,通过rename method方法可以解决。
        • 如果不需要超类实例,可以将超类声明为抽象类。
      • 逐一使用函数下移和字段下移将源类的特性移动到子类。

      11.12 Replace Delegation with Inherited(以继承取代委托)352

      动机:在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数,方法:让委托类继承受托类。注意:

      • 如果并没有使用受托类的所有函数,那么就不要使用这个方法。因为子类应该总是遵循超类的接口,如果委托过多可以通过Remove Middle Man(160)方法让客户端调用受托函数,或者Extract Superclass(336)让两个类的接口提炼到超类中;还可以使用Extract Interface(341)方法。
      • 如果受托对象被不止一个其他对象共享,而且受托对象是可变的时候,那么这种情况下,不能将委托关系替换为继承关系,因为这样就无法共享数据了。数据共享是委托关系的一种重要功能。
    • 相关文章:

      • 2022-01-16
      • 2022-01-10
      • 2021-10-06
      • 2021-12-21
      • 2021-09-16
      • 2021-07-22
      猜你喜欢
      • 2021-08-11
      • 2022-01-02
      • 2022-12-23
      • 2021-08-14
      • 2021-08-16
      • 2021-09-11
      • 2021-11-29
      相关资源
      相似解决方案