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

重构,第一个案例

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章 重构,第一个案例 1 1.1 起点 1 1.2 重构的第一步 7 1.3 分解并重组statement() 8 1.4 运用多态取代与价格相关的条件逻辑 34 1.5 结语 52 第2章 重构原则 53 2.1 何谓重构 53 2.2 为何重构 55 2.3 何时重构 57 2.4 怎么对经理说 60 2.5 重构的难题 62 2.6 重构设计 66 2.7 重构与性能 69 2.8 重构起源何处 71 第3章 代码的坏味道 75 3.1 Duplicated Code(重复代码) 76 3.2 Long Method(过长函数) 76 3.3 Large Class(过大的类) 78 3.4 Long Parameter List(过长参数列) 78 3.5 Divergent Change(发散式变化) 79 3.6 Shotgun Surgery(霰弹式修改) 80 3.7 Feature Envy(依恋情结) 80 3.8 Data Clumps(数据泥团) 81 3.9 Primitive Obsession(基本类型偏执) 81 3.10 Switch Statements(switch惊悚现身) 82 3.11 Parallel InheritanceHierarchies(平行继承体系) 83 3.12 Lazy Class(冗赘类) 83 3.13 Speculative Generality(夸夸其谈未来性) 83 3.14 Temporary Field(令人迷惑的暂时字段) 84 3.15 Message Chains(过度耦合的消息链) 84 3.16 Middle Man(中间人) 85 3.17 Inappropriate Intimacy(狎昵关系) 85 3.18 Alternative Classes with Different Interfaces(异曲同工的类) 85 3.19 Incomplete Library Class(不完美的库类) 86 3.20 Data Class(纯稚的数据类) 86 3.21 Refused Bequest(被拒绝的遗赠) 87 3.22 Comments(过多的注释) 87 第4章 构筑测试体系 89 4.1 自测试代码的价值 89 4.2 JUnit测试框架 91 4.3 添加更多测试 97 第5章 重构列表 103 5.1 重构的记录格式 103 5.2 寻找引用点 105 5.3 这些重构手法有多成熟 106 第6章 重新组织函数 109 6.1 Extract Method(提炼函数) 110 6.2 Inline Method(内联函数) 117 6.3 Inline Temp(内联临时变量) 119 6.4 Replace Temp with Query(以查询取代临时变量) 120 6.5 Introduce Explaining Variable(引入解释性变量) 124 6.6 Split Temporary Variable(分解临时变量) 128 6.7 Remove Assignments to Parameters(移除对参数的赋值) 131 6.8 Replace Method with Method Object(以函数对象取代函数) 135 6.9 Substitute Algorithm(替换算法) 139 第7章 在对象之间搬移特性 141 7.1 Move Method(搬移函数) 142 7.2 Move Field(搬移字段) 146 7.3 Extract Class(提炼类) 149 7.4 Inline Class(将类内联化) 154 7.5 Hide Delegate(隐藏“委托关系”) 157 7.6 Remove Middle Man(移除中间人) 160 7.7 Introduce Foreign Method(引入外加函数) 162 7.8 Introduce Local Extension(引入本地扩展) 164 第8章 重新组织数据 169 8.1 Self Encapsulate Field(自封装字段) 171 8.2 Replace Data Value with Object(以对象取代数据值) 175 8.3 Change Value to Reference(将值对象改为引用对象) 179 8.4 Change Reference to Value(将引用对象改为值对象) 183 8.5 Replace Array with Object(以对象取代数组) 186 8.6 Duplicate Observed Data(复制“被监视数据”) 189 8.7 Change Unidirectional Association to Bidirectional(将单向关联改为双向关联) 197 8.8 Change Bidirectional Association to Unidirectional(将双向关联改为单向关联) 200 8.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法数) 204 8.10 Encapsulate Field(封装字段) 206 8.11 Encapsulate Collection(封装集合) 208 8.12 Replace Record with Data Class(以数据类取代记录) 217 8.13 Replace Type Code with Class(以类取代类型码) 218 8.14 Replace Type Code with Subclasses(以子类取代类型码) 223 8.15 Replace Type Code with State/Strategy(以State/Strategy取代类型码) 227 8.16 Replace Subclass with Fields(以字段取代子类) 232 第9章 简化条件表达式 237 9.1 Decompose Conditional(分解条件表达式) 238 9.2 Consolidate Conditional Expression(合并条件表达式) 240 9.3 Consolidate Duplicate Conditional Fragments(合并重复的条件片段) 243 9.4 Remove Control Flag(移除控制标记) 245 9.5 Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式) 250 9.6 Replace Conditional with Polymorphism(以多态取代条件表达式) 255 9.7 Introduce Null Object(引入Null对象) 260 9.8 Introduce Assertion(引入断言) 267 第10章 简化函数调用 271 10.1 Rename Method(函数改名) 273 10.2 Add Parameter(添加参数) 275 10.3 Remove Parameter(移除参数) 277 10.4 Separate Query from Modifier(将查询函数和修改函数分离) 279 10.5 Parameterize Method(令函数携带参数) 283 10.6 Replace Parameter with Explicit Methods(以明确函数取代参数) 285 10.7 Preserve Whole Object(保持对象完整) 288 10.8 Replace Parameter with Methods(以函数取代参数) 292 10.9 Introduce Parameter Object(引入参数对象) 295 10.10 Remove Setting Method(移除设值函数) 300 10.11 Hide Method(隐藏函数) 303 10.12 Replace Constructor with Factory Method(以工厂函数取代构造函数) 304 10.13 Encapsulate Downcast(封装向下转型) 308 10.14 Replace Error Code with Exception(以异常取代错误码) 310 10.15 Replace Exception with Test(以测试取代异常) 315 第11章 处理概括关系 319 11.1 Pull Up Field(字段上移) 320 11.2 Pull Up Method(函数上移) 322 11.3 Pull Up Constructor Body(构造函数本体上移) 325 11.4 Push Down Method(函数下移) 328 11.5 Push Down Field(字段下移) 329 11.6 Extract Subclass(提炼子类) 330 11.7 Extract Superclass(提炼超类) 336 11.8 Extract Interface(提炼接口) 341 11.9 Collapse Hierarchy(折叠继承体系) 344 11.10 Form Tem Plate Method(塑造模板函数) 345 11.11 Replace Inheritance with Delegation(以委托取代继承) 352 11.12 Replace Delegation with Inheritance(以继承取代委托) 355 第12章 大型重构 359 12.1 Tease Apart Inheritance(梳理并分解继承体系) 362 12.2 Convert Procedural Design to Objects(将过程化设计转化为对象设计) 368 12.3 Separate Domain from Presentation(将领域和表述/显示分离) 370 12.4 Extract Hierarchy(提炼继承体系) 375 第13章 重构,复用与现实 379 13.1 现实的检验 380 13.2 为什么开发者不愿意重构他们的程序 381 13.3 再论现实的检验 394 13.4 重构的资源和参考资料 394 13.5 从重构联想到软件复用和技术传播 395 13.6 小结 397 13.7 参考文献 397 第14章 重构工具 401 14.1 使用工具进行重构 401 14.2 重构工具的技术标准 403 14.3 重构工具的实用标准 405 14.4 小结 407 第15章 总结 409
一直很喜欢重构这本书,但是由于自己记性不太好,书看过之后其中的方法总是记不住,于是想如果有电子版的重构书就好了,工作中遇到重构的问题可以随时打开查阅。在网上搜索了许久,发现重构这本书有英文chm版本的,而中文版的电子书只有扫描的PDF版本,用起来非常不方便。于是萌生想做一本重构工具书的想法,本来打算自己重新将重构书的内容再整理归类一下,后来发现原书的目录编排就很适合做工具书,包括坏味道分类,重构手法归类等,都有了一个比较系统的整理。因此,我利用空余时间制作了这样的一本中文的chm版重构,希望对大家有所帮助,也算对中国软件业做出一点小小的贡献。 本书基本上是取自”重构”中文版一书的内容,但格式上参照的是chm英文版的格式,还有一些格式小修改,比如第一章的重构前后代码对比。因为时间匆促,个人能力有限,本书难免存在一些缺漏,如果大家发现有问题,随时可以给我发邮件,我会尽快更新错误的内容。 最后再次感谢几位大师 Martin Fowler、Kent Beck等,还有翻译的侯捷和熊节先生,为我们带来这么精彩的一本书。谢谢。 免责声明:本书仅供个人学习研究之用,不得用于任何商业目的,不得以任何方式修改本作品,基于此产生的法律责任本人不承担任何连带责任。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值