重构原则与核心理念

第1-2章精华提炼:重构不是事后补救,而是开发的日常。

"重构(名词):对软件内部结构的一种调整,目的是在不改变可观察行为的前提下,提高其可理解性,降低其修改成本。"
核心 两顶帽子 (Two Hats)

开发过程中应明确当前状态,频繁切换:

  • 添加功能: 不修改现有结构,只添加新能力。进度由测试通过衡量。
  • 重构: 不添加功能,只重组代码。不添加新测试,只在接口变更时修改测试。
Why 为何重构?
  • 改进设计: 对抗代码腐烂(Entropy),消除重复。
  • 使软件更易理解: 代码主要写给人看,顺便给机器执行。
  • 助你找到Bug: 清晰的结构让Bug无处遁形。
  • 提高编程速度: 设计持久力假说(Design Stamina Hypothesis)——良好的设计能维持长期的开发速度。
When 何时重构?
  • 三次法则: 事不过三,三则重构。
  • 预备性重构: 添加新功能前,让结构更易于接纳新功能("Make the change easy, then make the easy change")。
  • 理解性重构: 理解代码时,将脑中的理解写回代码。
  • 捡垃圾式重构: 看到小问题随手修,大问题记下来稍后修。
  • Code Review时: 甚至通过结对编程进行实时重构。
Performance 重构与性能

多数情况下,重构不会显著影响性能。即使影响,也应该先写出可调优的代码,再调优

优化的三种策略:时间预算(实时系统)、持续关注(通常无效)、90%统计优化(推荐)(即:在良好的结构基础上,利用Profiler找出那10%的性能热点进行优化)。

代码的坏味道 (Bad Smells)

第3章:Kent Beck 的经典隐喻,指引我们何时开始重构。

命名/理解 Mysterious Name (神秘命名)

现象: 函数、变量或类名无法清晰表达其意图。

解法: Change Function Declaration, Rename Variable, Rename Field。

冗余 Duplicated Code (重复代码)

现象: 同样的逻辑结构在多处出现。

解法: Extract Function, Pull Up Method (继承体系中)。

复杂性 Long Function (长函数)

现象: 函数体过长,包含解释性的注释。

解法: Extract Function (99%的情况), Replace Temp with Query, Introduce Parameter Object, Preserve Whole Object, Replace Function with Command (极其复杂时)。

接口 Long Parameter List (长参数列表)

现象: 函数参数超过3-4个。

解法: Replace Parameter with Query, Preserve Whole Object, Introduce Parameter Object, Remove Flag Argument, Combine Functions into Class。

数据 Global Data (全局数据)

现象: 数据可以被代码库的任何角落修改。来自地狱第四层。

解法: Encapsulate Variable (首选防御)。

数据 Mutable Data (可变数据)

现象: 变量在不同地方被更新,导致难以追踪状态。

解法: Encapsulate Variable, Split Variable, Separate Query from Modifier, Remove Setting Method, Replace Derived Variable with Query, Change Reference to Value。

耦合 Divergent Change (发散式变化)

现象: 一个模块因为多种不同的原因(如数据库变更、显示逻辑变更)经常被修改。

解法: Split Phase, Move Function, Extract Class。

耦合 Shotgun Surgery (霰弹式修改)

现象: 每做一次修改,都要在很多不同的类中做小修改。

解法: Move Function/Field, Combine Functions into Class, Combine Functions into Transform, Inline Function/Class。

耦合 Feature Envy (依恋情结)

现象: 函数跟另一个模块的数据交流比跟自己的模块更多。

解法: Move Function, Extract Function (将需要的那部分提炼后移走)。

数据 Data Clumps (数据泥团)

现象: 总是结伴出现的数据项(如 start/end, x/y)。

解法: Extract Class, Introduce Parameter Object, Preserve Whole Object。

数据 Primitive Obsession (基本类型偏执)

现象: 不愿创建小对象(如 Money, TelephoneNumber),只用 string/int。

解法: Replace Primitive with Object, Replace Type Code with Subclasses, Replace Conditional with Polymorphism。

逻辑 Repeated Switches (重复的Switch)

现象: 同样的条件逻辑(switch/if-else)在多处重复。

解法: Replace Conditional with Polymorphism。

逻辑 Loops (循环语句)

现象: 传统的 for/while 循环,特别是包含复杂逻辑时。

解法: Replace Loop with Pipeline (filter/map/reduce)。

冗余 Lazy Element (冗赘的元素)

现象: 没什么用的类或函数(可能是为了未来扩展但从未实现)。

解法: Inline Function, Inline Class, Collapse Hierarchy。

冗余 Speculative Generality (夸夸其谈未来性)

现象: "以此应对未来可能的变化",添加了未使用的钩子或特殊情况。

解法: Collapse Hierarchy, Inline Function/Class, Remove Dead Code, Change Function Declaration (删除参数)。

中间人 Temporary Field (临时字段)

现象: 类中的某个字段仅在特定情况下(特定函数中)有效。

解法: Extract Class, Move Function, Introduce Special Case。

耦合 Message Chains (消息链)

现象: `a.getB().getC().doSomething()`,客户端与导航结构紧耦合。

解法: Hide Delegate, Extract Function, Move Function。

中间人 Middle Man (中间人)

现象: 一个类有一半的函数都在单纯委托给另一个类。

解法: Remove Middle Man, Inline Function。

耦合 Insider Trading (内幕交易)

现象: 两个模块之间过于亲密,私下频繁交换数据。

解法: Move Function/Field, Hide Delegate, Replace Subclass/Superclass with Delegate。

复杂性 Large Class (过大的类)

现象: 实例变量太多,代码太多。

解法: Extract Class, Extract Superclass, Replace Type Code with Subclasses。

继承 Refused Bequest (被拒绝的遗赠)

现象: 子类继承了父类的方法和数据,但只用了一小部分,甚至不支持父类接口。

解法: Push Down Method/Field (如果只是没用到), Replace Subclass/Superclass with Delegate (如果接口都不想要)。

注释 Comments (过多的注释)

现象: 注释被用来解释糟糕的代码("注释是除臭剂")。

解法: Extract Function (用名字解释意图), Change Function Declaration, Introduce Assertion。

构建测试体系 (Chapter 4)

没有测试的重构是在裸奔。

核心 自测试代码 (Self-testing Code)

只有当我们拥有一套能够快速发现错误的测试套件时,我们才能放心地进行重构。测试是Bug探测器。

  • 自动化: 测试必须能自动运行并检查结果。
  • 频繁运行: 至少每天运行,重构时每几分钟运行一次。
  • TDD: 先写失败的测试,再写代码。
技巧 测试策略
  • Fixture: 使用 beforeEach 创建干净的标准夹具,避免测试间污染。
  • 关注风险: 不要为了测试而测试(如简单的getter/setter),关注可能出错的逻辑。
  • 边界探测: 0、负数、空列表、null、非法输入。
  • 复现Bug: 收到Bug报告时,先写一个测试暴露它。

第一组重构 (Chapter 6)

最常用、最基础的工具箱。

常用 Extract Function (提炼函数)

动机: 分离意图与实现。如果你需要花时间浏览一段代码才能弄清它在干什么,就应该将其提炼。

做法: 创造新函数,以“做什么”命名;复制代码;处理变量作用域(参数/返回值)。

常用 Inline Function (内联函数)

动机: 函数体本身比函数名更清晰,或者为了消除过多的间接层。

做法: 检查多态;找到所有调用点替换为函数体;删除定义。

变量 Extract Variable (提炼变量)

动机: 表达式难以理解。引入解释性变量来分解复杂逻辑。

变量 Inline Variable (内联变量)

动机: 变量名并不比表达式本身更具描述性。

接口 Change Function Declaration (改变函数声明)

别名: Rename Function, Add/Remove Parameter。

动机: 名字是最好的文档;参数决定了函数的上下文。

做法: 简单式(直接改)或 迁移式(提炼新函数->旧函数调用新函数->逐步迁移调用者)。

封装 Encapsulate Variable (封装变量)

动机: 数据比函数更难重构。通过函数(getter/setter)访问数据,以便监控和修改数据访问方式。

变量 Rename Variable (变量改名)

动机: 好的名字能解释代码意图。对于持久字段尤为重要。

结构 Introduce Parameter Object (引入参数对象)

动机: 一组数据总是一起传递(如 start/end)。将其组合成结构或类,可以进一步吸引行为。

结构 Combine Functions into Class (函数组合成类)

动机: 一组函数紧密操作同一块数据。类能提供共享环境,简化函数调用。

结构 Combine Functions into Transform (函数组合成变换)

动机: 类似组合成类,但适用于将源数据计算派生数据并返回新记录的场景(特别是在只读上下文中)。

结构 Split Phase (拆分阶段)

动机: 代码在同时处理两件不同的事(如:解析数据 + 渲染)。

做法: 将第二阶段提炼为函数,引入中转数据结构,将第一阶段转为转换器。

封装 (Chapter 7)

隐藏信息,减少耦合。

数据 Encapsulate Record (封装记录)

动机: 对象优于简单的记录/哈希表,因为对象可以隐藏结构变化,提供有意义的操作方法。

数据 Encapsulate Collection (封装集合)

动机: 不要直接返回集合的引用,防止外部绕过类直接修改集合。应提供 add/remove 方法,getter 返回只读副本。

数据 Replace Primitive with Object (以对象取代基本类型)

动机: 简单数据(如电话字符串)逐渐需要行为(格式化)。创建值对象(Value Object)来取代它。

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

动机: 临时变量只在函数内可见。将其提炼为函数,可被复用,且有助于缩短函数长度。

Extract Class (提炼类)

动机: 类太大,责任太多。将一部分数据和函数分离出去。

Inline Class (内联类)

动机: 一个类不再承担足够责任,将其合并回使用它的类。

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

动机: 客户端必须知道对象的内部结构(`a.getB().getC()`)。在A上建立方法委托给C,减少耦合。

委托 Remove Middle Man (移除中间人)

动机: 过度封装导致某个类全是简单的委托方法。让客户端直接调用受托对象。

逻辑 Substitute Algorithm (替换算法)

动机: 用更清晰的算法替换复杂的旧算法。

搬移特性 (Chapter 8)

在上下文之间重新分配责任。

常用 Move Function (搬移函数)

动机: 函数与另一个上下文的交互更多。这是模块化的核心操作。

常用 Move Field (搬移字段)

动机: 数据结构是根基。如果数据所在的结构不合理,行为也会变乱。

语句 Move Statements into Function (搬移语句到函数内)

动机: 如果某些重复代码总是紧随某函数出现,将其并入该函数。

语句 Move Statements to Callers (搬移语句到调用者)

动机: 函数内部的某些行为在不同调用点需要变化。将这部分移出函数。

语句 Replace Inline Code with Function Call (以函数调用取代内联代码)

动机: 消除重复,提高抽象层级。

语句 Slide Statements (移动语句)

动机: 将相关代码移动到一起,通常是为提炼函数做准备。

循环 Split Loop (拆分循环)

动机: 一个循环做了两件事。拆分后更容易理解和重构。不用过早担心性能。

循环 Replace Loop with Pipeline (以管道取代循环)

动机: 使用 map, filter, reduce 等集合操作,逻辑更清晰,像水流一样。

清理 Remove Dead Code (移除死代码)

动机: 无用的代码是理解的负担。版本控制系统会帮你记住它们。

组织数据 (Chapter 9)

处理数据结构、作用域和引用。

变量 Split Variable (拆分变量)

动机: 一个变量承担了多个责任(被多次赋值用于不同目的)。每个变量应只负责一件事(const)。

变量 Rename Field (字段改名)

动机: 数据结构的命名对于理解程序至关重要。

变量 Replace Derived Variable with Query (以查询取代派生变量)

动机: 尽量减少可变数据的作用域。如果一个值可以计算出来,就不要存储。

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

动机: 值对象(不可变)更易于理解和处理。如果对象不需要共享更新,尽量作为值对象。

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

动机: 如果需要共享数据的更新(例如:一个客户对象被多个订单引用),则需要将其转为引用对象(通常使用 Repository)。

简化条件逻辑 (Chapter 10)

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

动机: 复杂的条件检查掩盖了意图。提炼条件判断、then分支、else分支为独立的函数。

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

动机: 一系列检查都导致相同的结果。使用 AND/OR 合并它们,并提炼函数(如 isDisabilityEligible())。

条件 Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)

动机: if-else 暗示两个分支同等重要。如果某个分支是异常情况,检查后直接 return,让主流程保持平铺。

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

动机: 多个函数中有同样的 switch/if 逻辑基于类型分发。使用类和多态使逻辑更清晰。

特例 Introduce Special Case (引入特例)

别名: Introduce Null Object。

动机: 代码中充满了对特殊值(如 null, "unknown")的检查。创建一个特例对象来封装通用行为。

断言 Introduce Assertion (引入断言)

动机: 明确代码对状态的假设。断言是程序员间的沟通工具,且有助于调试。

重构 API (Chapter 11)

优化模块间的连接点。

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

原则: 命令查询分离(CQS)。任何有返回值的函数都不应该有副作用(Observable Side Effects)。

接口 Parameterize Function (函数参数化)

动机: 两个函数逻辑非常相似,只有字面量不同。合并它们,将不同之处作为参数。

接口 Remove Flag Argument (移除标记参数)

动机: 布尔标记参数(如 `render(true)`)让调用代码晦涩难懂。应拆分为明确命名的函数。

接口 Preserve Whole Object (保持对象完整)

动机: 不要解构对象传多个参数,直接传整个对象。这能应对未来变化,减少参数列表。

接口 Replace Parameter with Query (以查询取代参数)

动机: 如果函数可以通过其他参数自己获取数据,就不要让调用者麻烦去计算并传参。减少参数列表。

接口 Replace Query with Parameter (以参数取代查询)

动机: 为了解耦(去除函数内部对他物的依赖),或者为了纯函数化(引用透明性),将内部依赖改为参数传入。

接口 Remove Setting Method (移除设值函数)

动机: 如果字段在创建后不应改变,移除 setter,表明其不可变性。

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

动机: 构造函数有限制(名字固定、必须返回实例)。工厂函数更灵活。

命令 Replace Function with Command (以命令取代函数)

动机: 函数太复杂。封装成命令对象(Command Object),将参数转为字段,便于分解复杂逻辑。

命令 Replace Command with Function (以函数取代命令)

动机: 命令对象如果不复杂,就只是增加了复杂度。变回普通函数。

处理继承 (Chapter 12)

继承很强大,但也容易被误用。

继承 Pull Up Method / Field (上移函数/字段)

动机: 子类中有重复代码。移到超类中。

继承 Push Down Method / Field (下移函数/字段)

动机: 超类中的某些行为只与部分子类有关。

继承 Pull Up Constructor Body (上移构造函数主体)

动机: 子类构造函数中有重复代码。调用 super()

继承 Remove Subclass (移除子类)

动机: 子类做的事情太少,不值得存在。用超类中的字段(Type Field)代替。

继承 Extract Superclass (提炼超类)

动机: 两个类有相似的特性。创建一个共同的超类。

继承 Collapse Hierarchy (折叠继承体系)

动机: 超类和子类太相似,没必要分开。

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

动机: 需要基于类型码表现多态行为,或特定类型有特定字段。

委托 Replace Subclass with Delegate (以委托取代子类)

动机: 组合优于继承。当继承导致类关系过紧,或想沿不同轴向变化(继承只能一次)时,使用委托(策略/状态模式)。

委托 Replace Superclass with Delegate (以委托取代超类)

动机: 误用继承(如 Stack 继承 List),导致子类暴露了不该暴露的接口。改用委托持有旧超类对象。

原文

源链接