《重构-改善既有代码的设计》读书笔记

重构,第一个案例

1.1 起点

  • 如果发现现有的代码结构使你无法很方便地添加新特性,那就先重构,使特性的添加比较容易进行后,再添加特性;

1.2 重构的第一步

  1. 为即将修改的代码建立可靠的测试环境 – 是人就会犯错,所以需要可靠的测试;
  2. 测试结果能够自我检验 – 成功”OK”,失败列出失败清单并打印行号 (自动化对比测试结果是提高效率的前提);

1.3 分解并重组”巨型”函数

  1. 切分提炼长函数(Extract Method),并移至更合适的类(Move Method) – 代码块越小,越容易管理;
  2. 重构技术 – 以微小的步伐修改程序,如果犯错,很容易便可发现它;
  3. 变量重命名 – 代码应表现自己的目的,而变量名是关键; 唯有写出人类容易理解的代码,才是优秀的程序员;
  4. 去除临时变量 – 临时变量可能成为问题,因为它们只在所属函数有效,从而助长冗长而复杂的函数;
  5. 重构的节奏 – 测试 → 小修改 → 测试 → 小修改 → …… , 正是这种节奏让重构快速而安全地前进;

重构原则

2.1 何谓重构

  1. 调整代码内部结构,在不改变功能的前提下,使其更易理解和修改;
  2. 两顶帽子 – 添加新功能、重构:
    • 添加新功能时 – 不应修改既有代码,只管添加并通过测试;
    • 重构时 – 只管改进程序结构,且只在绝对必要(接口变化)时才修改测试;
    • 开发过程中帽子经常变换 – 譬如增加功能时发现更改结构会更容易; 但无论何时,要清楚自己戴的是哪顶帽子;

2.2 为何重构

  1. 改进软件设计:维持原有设计使其便于阅读理解避免腐败变质; 消除重复代码,方便未来修改;
  2. 使软件更易理解:让代码更好地表达自己的用途 – a.方便自己以后查阅; b.协助自己理解不熟悉的代码;
    • 早期重构 – “擦掉窗户上的污垢,使你看得更远”;
  3. 帮助找到Bug:越理解代码越容易揪出Bug; 重构能更有效的写出健壮的代码;
  4. 提高编程速度:良好的设计是维持开发速度的根本;恶劣的设计会导致更多的调试、阅读理解和寻找重复代码;

2.3 何时重构

  1. 三次法则:第一次做某件事时只管去做;第二次做类似的事会反感但还可以做;第三次再做类似的事就应该重构;
  2. 添加功能时:a.若重构能使我更快地理解; b.若用其他方式来设计,添加功能会简单方便很多;
  3. 修补错误时:如果收到一份错误报告 – 重构代码,因为它没有清晰到你能一眼看出Bug;
  4. 复审代码时:阅读代码 → 一定程度理解 → 提出建议 → 想到点子时考虑是否可通过重构轻松实现 → 重构 → 对代码获得更高层次的认识;
    • 复审者+原作者(结对编程复审) – 复审者提出建议 → 共同判断是否能通过重构轻松实现 → 修改;
    • 大型设计的复审,用UML示意图展现设计并以CRC卡展示软件情节; 和团队进行设计复审,和个人进行代码复审;
  5. 如果发现昨天的决定已不适合今天的决定,放心改变这个决定以完成今天的工作,至于明天,回头看今天觉得幼稚,那时还可以改变你的理解;
    • 希望程序(1)容易阅读; (2)所有逻辑都只在唯一地点指定; (3)新的改动不会危及现有行为; (4)尽可能简单表达条件逻辑;

2.5 重构的难题

  1. 数据库:在对象模型和数据库模型之间插入一个分隔层,隔离两个模型各自的变化;
    • 不需要一开始即插入分隔层,在发现对象模型变得不稳定时再产生它;
  2. 修改接口:接口只有被那些“找不到,即使找到也不能修改”的代码使用时,才会成为重构的障碍;
    • 不要过早发布接口 – 修改代码所有权政策,使重构更顺畅; (如果非要更改已发布接口,让旧接口调用新接口,并标记为Deprecate);
  3. 难以通过重构手法完成的设计改动:考虑候选设计方案时,对比重构难度,有限挑选更易重构的设计,即使它不能覆盖所有潜在需求;
  4. 何时不该重构:a.既有代码太混乱或错误过多,应该“重写”,重构前提是代码大部分情况下运行正常;
    • b.项目已近最后期限时应避免重构;

2.6 重构与设计

  1. 预先设计(CRC卡等方式检验各种想法) → 择一可接受方案 → 编码 → 重构;
  2. 仍需思考潜在变化和灵活的解决方案,但不必逐一实现,而是问“简单方案重构成该灵活方案有多大难度”,如果”容易”那实现目前的简单方案即可;
  3. 哪怕你完全了解系统,也请实际度量它的性能,不要臆测,臆测会让你学到一些东西,但十有八九你是错的;

2.7 重构与性能

  1. 首先写出可调的软件,然后调整以获得足够的速度;
  2. 三种追求性能的编写方法:
    • 时间预算法 – 分解设计时就做好预算,给每个组件分配一定资源–包括时间和执行轨迹;每个组件绝对不能超过自己的预算;
    • 持续关注法 – 任何时候做任何事时,都要设法保持系统的高性能; 作用不大,因为过于分散,视角也和狭隘;
    • 关注性能热点 – 90%的时间都花在了10%的代码上; 使用度量工具监控程序的运行,让它指出耗时/耗空间的热点代码,谨慎修改并多多测试以调优代码;

代码的坏味道

3.1 重复代码(Duplicated Code)

  1. 如果在一个以上的地点看到相同的程序结构 – 设法将他们合而为一;
    • 同一个类的两个函数含有相同的表达式; (Extract Method)
    • 两个互为兄弟的子类内含相同表达式; (Extract Method → Pull Up Method(推入超类) → Form Template Method(提炼相似分割差异))

3.2 过长函数(Long Method)

  1. 拥有短函数的对象会活得比较好、比较长 – “间接层”能带来的诸如解释、共享、选择能力都是由小型函数支持的;
  2. 一个好名字 – 让小函数容易理解的真正关键,直观了解而无需进入其中查看;
  3. 更积极地去分解函数:
    • 每当感觉需要注释来说明某块代码时,就把其放入一个独立函数中,并以其用途(而非实现手法)命名;
    • 条件表达式和循环也是提炼的信号 – Decompose Condition处理条件表达式;
  4. 消除大量的参数和临时变量 – Replace Temp With Query消除临时元素; Introduce Parameter Object / Preserve Whole Object将过长参数变得简洁;

3.3 过大的类(Large Class)

  1. 太多成员变量 – 将类中相同前缀或字尾的成员变量提炼到某个组件内,如果该组件适合作为一个子类,可用Extract Subclass;
  2. 太多代码 – 有多个相同代码则提取成函数; 或将部分代码提取成类/子类来瘦身;
  3. GUI大类 – 把数据和行为移到独立的领域对象去,可能需要两边各保留一些重复数据并保持两边同步(Duplicate Observed Data);

3.4 过长参数列(Long Parameter List)

  • 太多参数会造成函数不易使用,且一旦需要更多数据就不得不修改它; 如果传递对象,只需(在函数内)增加一两条请求就能得到更多的数据了;
    • Replace Parameter with Method – 若向已有对象(函数所属类/另一参数)发出请求即可取代一个参数,则激活此重构手法;
    • Preserve Whole Object – 将来自同一对象的一堆数据(参数)收集起来,并以该对象替换它们;
    • Introduce Parameter Object – 若某些数据缺乏合理的对象归属,为它们制造出一个”参数对象”;

3.5 发散式变化(Divergent Change)

  1. 一旦需要修改,只需跳到系统某处并只在此处修改 – 不行,则应考虑重构;
  2. 若某个类经常因为不同的原因在不同的方向上发生变化 – 找出特定原因造成的变化,Extract Class将它们提炼到另一个类中;

3.6 霰弹式修改(Shotgun Surgery)

  • 若每遇到某种变化,都必须在许多不同的类内做出许多小修改 – 需使”外界变化”与”需要修改的类”趋于一一对应;
    • Move Method和Move Field把需要修改的代码放进同一个类; 若无合适的类安置这些代码,就创造一个(Inline Class);

3.7 依恋情节(Feature Envy)

  1. 函数对某个类的兴趣高过对自己所处类的兴趣 – 移到该去的地方
  2. 函数用到好几个类的功能 – 哪个类拥有最多被此函数使用的数据,就把该函数置于哪(先分解后移动);

3.8 数据泥团(Data Clumps)

  • 那些总是绑在一起出现的数据,当删掉其中某项,其他数据因而失去意义时 – 你应为它们产生一个新对象;
    • 一旦提炼出新对象,即可着手寻找Feature Envy(3.7),分解,提炼,不必太久,所有的类都将充分发挥价值;

3.9 基本类型偏执(Primitive Obsession)

  • 多多运用小对象 – 譬如money类(币值+币种)、range类(起始值+结束值)等等;

3.10 switch惊悚现身(Switch/Case Statements)

  1. 多数情况下,看到switch/case语句,应考虑”多态”来替代;
  2. 若只是在单一函数中有些选择事例,用Replace Parameter with Explicit Methods;

3.11 平行继承体系(Parallel Inheritance Hierarchies)

  • 每当你为某个类增加一个子类,也必须为另一个类相应增加一个子类时 - 重构,让一个继承体系的实例引用另一个继承体系的实例;

3.12 冗赘类(Lazy Class)

  • 如果某些子类没有做足够的工作,Collapse Hierarchy; 对于几乎没用的组件,Inline Class;

3.13 夸夸其谈未来性(Speculative Generality)

  • 用不到,就去掉:a.没有太大用的抽象类; b.不必要的委托; c.多余的参数;

3.14 令人迷惑的暂时字段(Temporary Field)

  • 成员变量未被使用; 成员变量只在某个算法时有效(只是为了减少传参),将变量和算法提炼为新的函数对象;

3.16 中间人(Middle Man)

  • 过度委托,譬如某个类接口有一半的函数都委托给了其他类(Middle Man) - 解决方案:
    • 直接和负责对象通信;
    • 若”不干实事”的函数占少数,Inline Method放进调用端;
    • 若这些Middle Man还有其他行为,将其变为实责对象的子类;

3.17 狎昵关系(Inappropriate Intimacy)

  • 过分狎昵的类必须拆散 – a.移动方法或成员变量; b.双向关联改为单向; c.提炼共同点到新类; d.委托其他类传递相思; e.委托取代继承;

3.18 异曲同工的类(Alternative Classes with Different Interfaces)

  • 若两个函数做同一件事,却有着不同的签名 – 移动,归并,或提炼到超类中;

3.20 纯粹的数据类(Data Class)

  • 除了字段及读写函数,无一长物 – a.封装public字段; b.恰当封装容器类字段; c.移除不应修改的字段的设置函数; d.提炼调用函数以隐藏取值/设值函数;

3.21 被拒绝的遗赠(Refused Bequest)

  • 子类只运用了父类的一部分函数和数据 – 为子类建立一个兄弟类,将所有用不到的字段/函数下移至兄弟类,保证超类的纯粹;

3.22 过多的注释(Comments)

  • 注释之所以存在是因为代码很糟糕 – 当需要撰写注释时先尝试重构,试着让注释都变得多余;
    • 若需要注释解释一块代码做了什么 – 提炼函数(Extract Method);
    • 若函数已提炼,仍需解释其行为 – 函数更名(Rename Method);
    • 如果需要注释说明某些系统的需求规格 – 引入断言(Introduce Assertion);

构筑测试体系

  • 重构的前提 – 拥有可靠的测试环境; 并且,编写优良的测试程序,可极大提高编程速度.

4.1 自测试代码的价值

  • 编程时间消耗:a.编写代码(20%); b.决定下一步干什么(10%); c.设计(20%); d.调试(50%) – 修复很快,找出错误则像是噩梦一场;
  • 确保所有测试都完全自动化,让它们检查自己的测试结果;
  • 一套测试就是一个强大的bug侦测器,能大大缩减查找bug所需要的时间;
  • 频繁地测试 – 极限编程方法之一:
    • 写好一点功能,就立即添加测试,分量越小,越能轻松找到错误的源头;
    • 每次编译请把测试也考虑进去 – 每天至少执行每个测试一次;
    • 重构时,只需运行当下正在开发或整理的这部分代码测试;
  • 编写测试代码时,一开始先让它们失败,确保测试机制可运行;
  • 每当收到功能测试的bug报告,请先写一个单元测试来暴露这个bug;

4.3 添加更多测试

  • 观察类该做的所有事情,然后针对任何一项功能的任何一种可能失败情况,进行测试;
    • 风险驱动 – 测试的目的是找出可能出现的错误,因此没必要去测试那些Public下的简单读写字段;
    • 避免完美 – 将时间耗费在为担心出错的部分写测试;
    • 集中火力 – 考虑可能出错的边界条件,把测试火力集中在那儿;
  • 程序公敌:积极思考如何破坏代码的正确运行; 当事情被认为应该会出错时,别忘了检查是否抛出了预期的异常;
  • 不要因为测试无法捕捉所有bug就不写测试 – 因为测试的确可以捕捉大多数bug;
  • 继承和多态会加大测试困难 – 之类多所以组合多,但尽量的测试每个类,会大大减少各种组合所造成的风险;

重新组织函数

6.1 Extract Method(提炼函数)

  • 动机:1.过长的函数(粒度越细越易被复用/复写); 2.注释才能理解用途的代码(命名清晰);
  • 做法:
    • 新建函数并以其意图(“做什么”)命名 – 只要新命名能更好地昭示代码意图,即使代码简单也可提炼,反之,就别动;
    • 提炼局部变量 – a.无变更使用Replace Temp with Query; b.一次变更可作为参数; c.多次变更传参+返回值;
    • 将代码段替换为目标函数 → 编译,测试;

6.2 Inline Method(内联函数)

  • 在函数调用点插入函数本体代码,然后删除函数;
  • 动机:1.内部代码和函数名同样清晰易读; 2.组织不甚合理的函数,内联到大型函数后再提炼出合理的小函数;
  • 做法:确定该函数不具有多态性 → 内联替换 → 编译测试 → 删除函数定义;

6.3 Inline Temp(内联临时变量)

  • 将所有对该变量的引用动作,替换为对它赋值的那个表达式自身; 无危害,除非妨碍了重构;

6.4 Replace Temp with Query(以查询取代临时变量)

  • 将表达式提炼到新的独立函数中,并替换所有引用点;此后,该函数就可被其他函数调用;
  • 动机:临时变量驱使函数”变胖”,而函数会使代码更清晰明了;

6.5 Introduce Explaining Variable(引入解释性变量)

  • 将复杂表达式(或其中一部分)的结果放进一个临时变量,以变量名称来解释表达式的用途;
  • 动机:表达式复杂而难以阅读,尤其是”条件逻辑”; 但尽量用Extract Method来解释一段代码的意义;

6.6 Split Temporary Variable(分解临时变量)

  • 针对每次赋值,创造一个独立、对应的临时变量;
  • 动机:除了”循环变量”和”结果收集变量”,同一临时变量承担了两种及以上不同责任;

6.7 Remove Assignments to Parameters(移除对参数的赋值)

  • 以临时变量替换被赋值的参数; “出参数”的函数例外,但应尽量少用,多用返回值进行返回;

6.8 Replace Method with Method Object(以函数对象取代函数)

  • 因局部变量无法Extract Method,将其放入类中,局部变量作为字段 – 即可在同一个类中将大型函数分解为多个小函数;

6.9 Substitute Algorithm(替换算法)

  • 将函数本体替换为另一个算法(更清晰/更简单/效率更高);

在对象之间搬移特性

7.1 Move Method(搬移函数)

  • 在该函数最常引用/交流的类中新建一个类似函数; 将旧函数变成一个单纯的委托函数,或完全移除;
  • 动机:1.一个类具有太多行为; 2.一个类与另一个类有太多合作而高度耦合;
  • 做法:定位强关联特性 → 分析该特性相关的字段/函数 → 整体搬移;

7.2 Move Field(搬移字段)

  • 字段被另一个类更多地用到,将其移到目标类并封装,然后令源字段的所有用户使用新字段;

7.3 Extract Class(提炼类)

  • 某个类做了应该由两个类做的事,建立一个新类,将相关字段和函数搬移到新类;旧类责任与名称不符时更名;
  • 一个类应该是一个清楚的抽象,处理一些明确的责任; 包含大量函数和数据的类,往往太大而不易理解,此时考虑哪些部分可分离出去;

7.4 Inline Class(将类内联化)

  • 某个类没做太多事情,将该类的所有特性搬移到另一个类中,然后移除原类;

7.5 Hide Delegate(隐藏”委托关系”)

  • 客户通过委托类调用另一对象,在服务类上建立客户所需的函数,用以隐藏委托关系;
  • 动机:客户通过服务对象的字段得到另一对象然后调用后者的函数,那客户必须知晓这一层委托关系;用委托函数隐藏委托关系,能使变化不会波及客户;

7.6 Remove Middle Man(移除中间人)

  • 某个类做了过多的简单委托动作,让客户直接调用委托类; “合适的隐藏程度”;

7.7 Introduce Foreign Method(引入外加函数)

  • 在客户类中建立一个函数,并以第一参数形式传入一个服务类实例;
  • 动机:服务类无法直接提供需要的服务且无法更改服务类,需多次使用该服务时,不要重复复制代码,而是提炼成函数;

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

  • 建立一个新类,使它包含这些额外函数 – 让扩展成为源类的子类(subclassing)或包装类(wrapping);
  • 需要为服务类提供额外函数,但无法修改这个类; 本地扩展 – “函数和数据应该被统一封装”;

重新组织数据

8.1 Self Encapsulate Field(自封装字段)

  • 为字段建立取值/设值函数,并且只以这些函数来访问字段;
  • 动机:1.访问超类字段,又想在子类中将对这个字段的访问改为一个计算后的值; 2.延迟初始化(只在需要用到时才初始化);
  • 在有”属性”语义的语言中,建议都用”属性”,比如Delphi – 除了上诉优点外,同时兼顾了易于阅读的特性[直接访问变量的优点];

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

  • 某个数据需要与其他数据和行为一起使用才有意义 – 将数据项变成对象;

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

  • 从一个类衍生出许多彼此相等的实例,希望将它们替换为同一对象 – 将这个值对象变成引用对象;
  • 动机:希望给某个对象加入一些可修改数据,并确保对任何一个对象的修改都能影响到所有引用此对象的地方;
  • 做法:
    • 1.Replace Constructor with Factory Method → 编译测试;
    • 2.决定访问新对象的途径 – a.一个静态字典或注册表对象; b.也可多个对象作为访问点;
    • 3.决定这些对象的创建方式 – a.预先创建(从内存中读取,得确保在被需要的时候能被及时加载); b.动态创建;
    • 4.修改工厂函数使其返回引用对象 – a.预先创建,需考虑万一所求一个不存在的对象应如何处理; b.命名要体现出返回的是既存对象;

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

  • 有一个引用对象,很小且不可变也不易管理 – 将它编程一个值对象;
  • 动机:并发系统中,”不可变”的值对象特别有用,无需考虑同步问题;
    • 不可变 – 比如薪资用Money类(币种+金额)表示,若需更改薪资,应用另一Money对象取代,而非在现有Money对象上修改; 薪资和Money对象间的关系可改变,但Money对象自身不能改变;

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

  • 有一个数组中的元素各自代表不同的东西 – 以对象替换数组;对于数组中的某个元素,以一个字段来表示;

8.6 Duplicate Observed Data(复制”被监视数据”)

  • 将数据复制到一个领域对象中;建立一个Observe模式,用以同步领域对象和GUI对象内的重复数据;
  • 动机:一些领域数据置身与GUI控件中,而领域函数需要访问这些数据;
    • 用户界面和业务逻辑分离 – MVC(Model-View-Controller 模型-视图-控制器),多层系统;

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

  • 两个类都需要使用对方特性 – 添加一个反向指针,并使修改函数能够同时更新两条连接;

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

  • 两个类有双向关联,但如今某条关联不再有价值 – 去除不必要的关联;

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

  • 有一个带有特别含义的字面数值 – 创造一个常量,根据其意义命名,替换掉字面数值;

8.10 Encapsulate Field(封装字段)

  • 类中存在一个public字段 – 将它声明为private,并提供相应的访问函数;
  • 数据隐藏 – 若其他对象修改字段,而持有对象毫无察觉,造成数据和行为分离(坏事情!!)

8.11 Encapsulate Collection(封装集合)

  • 某函数返回一个集合 – 让其返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数;
  • 类中常用集合(array/list/set/vector)来保存一组实例,同时提供取值/设值函数; 取值函数不应返回集合自身,预防不可预知的修改;

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

  • 为记录创建一个”哑”数据类,以便日后将某些字段和函数搬移到该类中;

8.13 Replace Type Code with Class(以类取代类型码)

  • 类中有一个数值类型码且不影响类的行为 – 以一个新的类替换该类型码;
  • 不能进行类型检测,从而大概率引起bug;

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

  • 类中有不可变的类型码且会影响类的行为 – 以子类取代这个类型码;
  • 动机:宿主类中出现了”只与具备特定类型码之对象相关”的特性;
    • 例外 – a.类型码的值在对象生命期中发生了改变; b.某些原因使宿主类不能被继承,使用Replace Type Code with State/Strategy;

8.15 Replace Type Code with State/Strategy(状态/策略模式取代类型码)

  • 做法:
    • 1.Self Encapsulate Field类型码;
    • 2.新建一个超类,以类型码用途命名(状态对象) → 为每种类型码添加一个子类,从超类继承;
    • 3.超类中建立一个抽象的查询函数,用以返回类型码 → 子类复写并返回确切的类型码;
    • 4.源类新建一个字段来保存状态对象 → 将查询动作转发给状态对象 → 调整设值函数,将恰当的状态子对象赋值给”保存字段”;
    • 5.用多态取代类型码相关的条件表达式;

8.16 Replace Subclass with Fields(以字段取代子类)

  • 各子类的唯一差别只在”返回常量数据”的函数身上 – 修改这些函数,使其返回超类中的某个(新增)字段,然后销毁子类;
  • 动机:建立子类的目的,是为了增加新特性或变化其行为; 若子类只有常量函数(返回硬编码值),则应去除子类;

简化条件表达式

9.1 Decompose Condition(分解条件表达式)

  • 有一复杂的条件语句 – 从if、then、else三个段落中分别提炼出独立函数;
  • 动机:分支条件和逻辑代码自身与代码意图有不小差距 – 提炼,更好的命名,会看上去如注释版清晰明了;

9.2 Consolidate Condition Expression(合并表达式)

  • 将多个条件合并为一个条件表达式,并将其提炼成一个独立函数;
  • 动机:有一串各不相同的检查条件但最终行为是一致的,且各条件无其他副作用 – 目的是a.使检查用意清晰; b.为提炼函数做准备;

9.3 Consolidate Duplicate Condition Fragments(合并重复的条件代码)

  • 在一组条件表达式的所有分支上都执行了某段相同的代码 – 将这段重复代码搬移到条件表达式之外(起始处/尾端);

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

  • 在一系列布尔表达式中,某变脸带有”控制标记”的作用 – 以break/return语句取代控制标记;

9.5 Replace Nested Condition with Guard Clauses(以卫语句取代嵌套表达式)

  • 函数中的条件逻辑使人难以看清正常的执行路径 – 使用卫语句(单独的检查)表现所有特殊情况;
  • 动机:1.所有分支都是正常行为(if…else…); 2.分支中只有一种是正常行为,其他则非常罕见(单独if…语句即卫语句/条件反转);

9.6 Replace Condition with Polymorphism(以多态取代条件表达式)

  • 条件表达式根据对象类型的不同而选择不同的行为 – 将原始函数声明为抽象函数而后将每个分支放进子类复写函数中;

9.7 Introduce Null Object(引入Null对象)

  • 需要再三检查某对象是否Null – 将Null值替换为Null对象;
  • 做法:1.行为都一致的Null行为 – Singleton(单例模式); 2.有着特殊行为 – Special Case(特例类);

9.8 Introduce Assertion(引入断言)

  • 某段代码需要对程序状态做出某种假设 – 以断言(交流与调试的辅助)明确表现这种假设;
  • 程序不犯错,断言就不会造成任何影响 – 不要滥用断言,请只使用它来检查”一定必须为真”的条件;

简化函数调用

10.1 Rename Method(函数改名)

  • 函数的名称未能揭示其用途 – 修改函数名称;
  • 将就可用的命名是恶魔的召唤,是通向混乱之路,千万不要!!!

10.2 Add Parameter(添加参数)

  • 函数需要从调用端得到更多信息 – 添加参数让其带着所需信息;

10.3 Remove Parameter(移除参数)

  • 函数本地不再需要某个参数 – 去除参数;

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

  • 函数既返回对象状态值,又修改对象值 – 建立两个不同函数,一个负责查询,一个负责修改;
  • 并发时,建立第三个函数(查询-修改),声明为synchronized,里面调用各自独立的查询和修改函数;

10.5 Parameterize Method(令函数携带参数)

  • 若干类似函数只因本体中少数几个值而致使行为略有不同 – 建立单一函数,以参数表达那些不同的值;

10.6 Replace Parameter with Explicit Methods(以明确函数取代参数)

  • 某个参数有多种可能值,而函数内又用表达式检查以采取不同行为 – 针对该参数的每一个可能值,建立一个独立函数;

10.7 Preserve Whole Object(保持对象完整)

  • 从某个对象中取出若干值作为某一次函数调用时的参数 – 改为传递整个对象;

10.8 Replace Parameter with Methods(以函数取代参数)

  • 对象调用某个函数,并将所得结果作为参数,传递给另一函数 – 去除参数,直接调用前一个函数;

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

  • 某些参数总是很自然的同时出现 – 以一个对象取代这系列参数;

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

  • 某个字段在对象创建时被设值就不再改变 – 去掉该字段的所有设值函数;

10.11 Hide Method(隐藏函数)

  • 类中某个函数,从未被其他任何类用到 – 将函数降为private;

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

  • 希望在创建对象时不仅仅是做简单的构建动作 – 将构造函数替换为工厂函数;

10.13 Encapsulate Downcast(封装向下转型)

  • 某个函数返回的对象,需要由函数调用者执行向下转型 – 将向下转型移到函数中;

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

  • 某个函数返回一个特定的代码,用以表述某种错误情况 – 改用异常(只应该用于异常的、罕见的行为);
  • 非受控异常 – 需调用者检查,编程错误; 受控异常 – 被调用函数进行检查,抛出异常,上层调用者catch并处理;

10.15 Replace Exception with Test(以测试取代异常)

  • 面对一个调用者可预先检查的条件,你抛出了一个异常 – 修改调用者,使其在调用函数之前先做检查;

处理概括关系

11.1 Pull Up Field(字段上移)

  • 两个子类拥有相同的字段 – 将该字段移至超类;

11.2 Pull Up Method(函数上移)

  • 有些函数在各子类中产生完全相同的结果 – 将该函数移至超类;

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

  • 各个子类中有一些构造函数本体几乎完全一致 – 在超类中新建一个构造函数,并在子类构造函数中调用它;

11.4 Push Down Method(函数下移)

  • 超类中的某个函数只与部分(而非全部)子类有关 – 将函数移到相关的子类中;

11.5 Push Down Field(字段下移)

  • 超类中的某个字段只被部分(而非全部)子类用到 – 将字段移到需要它的子类中;

11.6 Extract Subclass(提炼子类)

  • 类中的某些特性只被某些(而非全部)实例用到 – 新建一个子类,将上述部分的特性移到子类中;

11.7 Extract Superclass(提炼超类)

  • 两个类有相似特性 – 为这两个类建立一个超类,将相同特性移至超类;

11.8 Extract Interface(提炼接口)

  • 若干客户使用类接口中的同一子集,或两个类的接口有部分相同 – 将相同的子集提炼到一个独立接口中;

11.9 Collapse Hierarchy(折叠继承体系)

  • 超类和子类之间无太大区别 – 将它们合为一体;

11.10 Form Template Method(塑造模板函数)

  • 子类中某些函数以相同顺序执行类似操作,但各操作细节略有不同 – 将操作放进独立函数(保持签名相同),然后将它们移至超类;

11.11 Replace Inheritance with Delegation(以委托取代继承)

  • 某个子类只使用超类接口中的一部分或根本不需要继承而来的数据 – 子类新建字段保存超类,调整子类函数为委托超类,取消继承关系;

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

  • 在两个类中使用委托关系,并经常为整个接口编写许多极简单的委托函数 – 让委托类继承受托类;
  • 告诫:1.若未使用委托类的所有函数,则不应继承; 2.受托对象被不止一个其他对象共享,且受托对象是可变的,不应继承;

大型重构

12.1 Tease Apart Inheritance(梳理并分解继承体系)

  • 某个继承体系同时承担两项责任 – 建立两个继承体系,并通过委托关系让其中一个可调用另一个;

12.2 Convert Procedure Design to Objects(将过程化设计转化为对象设计)

  • 手上有一些传统过程化风格的代码 – 将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中;

12.3 Separate Domain from Presentation(将领域和表述/显示分离)

  • [MVC]某些GUI类之中包含了领域逻辑 – 将领域逻辑分离出来,为它们建立独立的领域类;

Extract Hierarchy(提炼继承体系)

  • 某个类做了太多工作,其中部分工作是以大量条件表达式完成的 – 建立继承体系,以一个子类表示一种特殊情况;

总结

  1. 随时挑选一个目标:某个地方代码发臭了,就将问题解决掉,达成目标停止 – 不是去探索真善美,而是防止程序散乱;
  2. 没把握就停下来:无法保证做出的更改能保持程序原本的语义 – 有改善就发布,没有就撤销修改;
  3. 学习原路返回:重构后运行测试,若失败,退回原点重新开始 – 调试时间可能很短也可能很长,但再次重复重构很快;
  4. 二重奏:结对编程,也利于重构;
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值