重构原则与核心理念
第1-2章精华提炼:重构不是事后补救,而是开发的日常。
开发过程中应明确当前状态,频繁切换:
- 添加功能: 不修改现有结构,只添加新能力。进度由测试通过衡量。
- 重构: 不添加功能,只重组代码。不添加新测试,只在接口变更时修改测试。
- 改进设计: 对抗代码腐烂(Entropy),消除重复。
- 使软件更易理解: 代码主要写给人看,顺便给机器执行。
- 助你找到Bug: 清晰的结构让Bug无处遁形。
- 提高编程速度: 设计持久力假说(Design Stamina Hypothesis)——良好的设计能维持长期的开发速度。
- 三次法则: 事不过三,三则重构。
- 预备性重构: 添加新功能前,让结构更易于接纳新功能("Make the change easy, then make the easy change")。
- 理解性重构: 理解代码时,将脑中的理解写回代码。
- 捡垃圾式重构: 看到小问题随手修,大问题记下来稍后修。
- Code Review时: 甚至通过结对编程进行实时重构。
多数情况下,重构不会显著影响性能。即使影响,也应该先写出可调优的代码,再调优。
优化的三种策略:时间预算(实时系统)、持续关注(通常无效)、90%统计优化(推荐)(即:在良好的结构基础上,利用Profiler找出那10%的性能热点进行优化)。
代码的坏味道 (Bad Smells)
第3章:Kent Beck 的经典隐喻,指引我们何时开始重构。
现象: 函数、变量或类名无法清晰表达其意图。
解法: Change Function Declaration, Rename Variable, Rename Field。
现象: 同样的逻辑结构在多处出现。
解法: Extract Function, Pull Up Method (继承体系中)。
现象: 函数体过长,包含解释性的注释。
解法: Extract Function (99%的情况), Replace Temp with Query, Introduce Parameter Object, Preserve Whole Object, Replace Function with Command (极其复杂时)。
现象: 函数参数超过3-4个。
解法: Replace Parameter with Query, Preserve Whole Object, Introduce Parameter Object, Remove Flag Argument, Combine Functions into Class。
现象: 数据可以被代码库的任何角落修改。来自地狱第四层。
解法: Encapsulate Variable (首选防御)。
现象: 变量在不同地方被更新,导致难以追踪状态。
解法: Encapsulate Variable, Split Variable, Separate Query from Modifier, Remove Setting Method, Replace Derived Variable with Query, Change Reference to Value。
现象: 一个模块因为多种不同的原因(如数据库变更、显示逻辑变更)经常被修改。
解法: Split Phase, Move Function, Extract Class。
现象: 每做一次修改,都要在很多不同的类中做小修改。
解法: Move Function/Field, Combine Functions into Class, Combine Functions into Transform, Inline Function/Class。
现象: 函数跟另一个模块的数据交流比跟自己的模块更多。
解法: Move Function, Extract Function (将需要的那部分提炼后移走)。
现象: 总是结伴出现的数据项(如 start/end, x/y)。
解法: Extract Class, Introduce Parameter Object, Preserve Whole Object。
现象: 不愿创建小对象(如 Money, TelephoneNumber),只用 string/int。
解法: Replace Primitive with Object, Replace Type Code with Subclasses, Replace Conditional with Polymorphism。
现象: 同样的条件逻辑(switch/if-else)在多处重复。
解法: Replace Conditional with Polymorphism。
现象: 传统的 for/while 循环,特别是包含复杂逻辑时。
解法: Replace Loop with Pipeline (filter/map/reduce)。
现象: 没什么用的类或函数(可能是为了未来扩展但从未实现)。
解法: Inline Function, Inline Class, Collapse Hierarchy。
现象: "以此应对未来可能的变化",添加了未使用的钩子或特殊情况。
解法: Collapse Hierarchy, Inline Function/Class, Remove Dead Code, Change Function Declaration (删除参数)。
现象: 类中的某个字段仅在特定情况下(特定函数中)有效。
解法: Extract Class, Move Function, Introduce Special Case。
现象: `a.getB().getC().doSomething()`,客户端与导航结构紧耦合。
解法: Hide Delegate, Extract Function, Move Function。
现象: 一个类有一半的函数都在单纯委托给另一个类。
解法: Remove Middle Man, Inline Function。
现象: 两个模块之间过于亲密,私下频繁交换数据。
解法: Move Function/Field, Hide Delegate, Replace Subclass/Superclass with Delegate。
现象: 实例变量太多,代码太多。
解法: Extract Class, Extract Superclass, Replace Type Code with Subclasses。
现象: 子类继承了父类的方法和数据,但只用了一小部分,甚至不支持父类接口。
解法: Push Down Method/Field (如果只是没用到), Replace Subclass/Superclass with Delegate (如果接口都不想要)。
现象: 注释被用来解释糟糕的代码("注释是除臭剂")。
解法: Extract Function (用名字解释意图), Change Function Declaration, Introduce Assertion。
构建测试体系 (Chapter 4)
没有测试的重构是在裸奔。
只有当我们拥有一套能够快速发现错误的测试套件时,我们才能放心地进行重构。测试是Bug探测器。
- 自动化: 测试必须能自动运行并检查结果。
- 频繁运行: 至少每天运行,重构时每几分钟运行一次。
- TDD: 先写失败的测试,再写代码。
- Fixture: 使用
beforeEach创建干净的标准夹具,避免测试间污染。 - 关注风险: 不要为了测试而测试(如简单的getter/setter),关注可能出错的逻辑。
- 边界探测: 0、负数、空列表、null、非法输入。
- 复现Bug: 收到Bug报告时,先写一个测试暴露它。
第一组重构 (Chapter 6)
最常用、最基础的工具箱。
动机: 分离意图与实现。如果你需要花时间浏览一段代码才能弄清它在干什么,就应该将其提炼。
做法: 创造新函数,以“做什么”命名;复制代码;处理变量作用域(参数/返回值)。
动机: 函数体本身比函数名更清晰,或者为了消除过多的间接层。
做法: 检查多态;找到所有调用点替换为函数体;删除定义。
动机: 表达式难以理解。引入解释性变量来分解复杂逻辑。
动机: 变量名并不比表达式本身更具描述性。
别名: Rename Function, Add/Remove Parameter。
动机: 名字是最好的文档;参数决定了函数的上下文。
做法: 简单式(直接改)或 迁移式(提炼新函数->旧函数调用新函数->逐步迁移调用者)。
动机: 数据比函数更难重构。通过函数(getter/setter)访问数据,以便监控和修改数据访问方式。
动机: 好的名字能解释代码意图。对于持久字段尤为重要。
动机: 一组数据总是一起传递(如 start/end)。将其组合成结构或类,可以进一步吸引行为。
动机: 一组函数紧密操作同一块数据。类能提供共享环境,简化函数调用。
动机: 类似组合成类,但适用于将源数据计算派生数据并返回新记录的场景(特别是在只读上下文中)。
动机: 代码在同时处理两件不同的事(如:解析数据 + 渲染)。
做法: 将第二阶段提炼为函数,引入中转数据结构,将第一阶段转为转换器。
封装 (Chapter 7)
隐藏信息,减少耦合。
动机: 对象优于简单的记录/哈希表,因为对象可以隐藏结构变化,提供有意义的操作方法。
动机: 不要直接返回集合的引用,防止外部绕过类直接修改集合。应提供 add/remove 方法,getter 返回只读副本。
动机: 简单数据(如电话字符串)逐渐需要行为(格式化)。创建值对象(Value Object)来取代它。
动机: 临时变量只在函数内可见。将其提炼为函数,可被复用,且有助于缩短函数长度。
动机: 类太大,责任太多。将一部分数据和函数分离出去。
动机: 一个类不再承担足够责任,将其合并回使用它的类。
动机: 客户端必须知道对象的内部结构(`a.getB().getC()`)。在A上建立方法委托给C,减少耦合。
动机: 过度封装导致某个类全是简单的委托方法。让客户端直接调用受托对象。
动机: 用更清晰的算法替换复杂的旧算法。
搬移特性 (Chapter 8)
在上下文之间重新分配责任。
动机: 函数与另一个上下文的交互更多。这是模块化的核心操作。
动机: 数据结构是根基。如果数据所在的结构不合理,行为也会变乱。
动机: 如果某些重复代码总是紧随某函数出现,将其并入该函数。
动机: 函数内部的某些行为在不同调用点需要变化。将这部分移出函数。
动机: 消除重复,提高抽象层级。
动机: 将相关代码移动到一起,通常是为提炼函数做准备。
动机: 一个循环做了两件事。拆分后更容易理解和重构。不用过早担心性能。
动机: 使用 map, filter, reduce 等集合操作,逻辑更清晰,像水流一样。
动机: 无用的代码是理解的负担。版本控制系统会帮你记住它们。
组织数据 (Chapter 9)
处理数据结构、作用域和引用。
动机: 一个变量承担了多个责任(被多次赋值用于不同目的)。每个变量应只负责一件事(const)。
动机: 数据结构的命名对于理解程序至关重要。
动机: 尽量减少可变数据的作用域。如果一个值可以计算出来,就不要存储。
动机: 值对象(不可变)更易于理解和处理。如果对象不需要共享更新,尽量作为值对象。
动机: 如果需要共享数据的更新(例如:一个客户对象被多个订单引用),则需要将其转为引用对象(通常使用 Repository)。
简化条件逻辑 (Chapter 10)
动机: 复杂的条件检查掩盖了意图。提炼条件判断、then分支、else分支为独立的函数。
动机: 一系列检查都导致相同的结果。使用 AND/OR 合并它们,并提炼函数(如 isDisabilityEligible())。
动机: if-else 暗示两个分支同等重要。如果某个分支是异常情况,检查后直接 return,让主流程保持平铺。
动机: 多个函数中有同样的 switch/if 逻辑基于类型分发。使用类和多态使逻辑更清晰。
别名: Introduce Null Object。
动机: 代码中充满了对特殊值(如 null, "unknown")的检查。创建一个特例对象来封装通用行为。
动机: 明确代码对状态的假设。断言是程序员间的沟通工具,且有助于调试。
重构 API (Chapter 11)
优化模块间的连接点。
原则: 命令查询分离(CQS)。任何有返回值的函数都不应该有副作用(Observable Side Effects)。
动机: 两个函数逻辑非常相似,只有字面量不同。合并它们,将不同之处作为参数。
动机: 布尔标记参数(如 `render(true)`)让调用代码晦涩难懂。应拆分为明确命名的函数。
动机: 不要解构对象传多个参数,直接传整个对象。这能应对未来变化,减少参数列表。
动机: 如果函数可以通过其他参数自己获取数据,就不要让调用者麻烦去计算并传参。减少参数列表。
动机: 为了解耦(去除函数内部对他物的依赖),或者为了纯函数化(引用透明性),将内部依赖改为参数传入。
动机: 如果字段在创建后不应改变,移除 setter,表明其不可变性。
动机: 构造函数有限制(名字固定、必须返回实例)。工厂函数更灵活。
动机: 函数太复杂。封装成命令对象(Command Object),将参数转为字段,便于分解复杂逻辑。
动机: 命令对象如果不复杂,就只是增加了复杂度。变回普通函数。
处理继承 (Chapter 12)
继承很强大,但也容易被误用。
动机: 子类中有重复代码。移到超类中。
动机: 超类中的某些行为只与部分子类有关。
动机: 子类构造函数中有重复代码。调用 super()。
动机: 子类做的事情太少,不值得存在。用超类中的字段(Type Field)代替。
动机: 两个类有相似的特性。创建一个共同的超类。
动机: 超类和子类太相似,没必要分开。
动机: 需要基于类型码表现多态行为,或特定类型有特定字段。
动机: 组合优于继承。当继承导致类关系过紧,或想沿不同轴向变化(继承只能一次)时,使用委托(策略/状态模式)。
动机: 误用继承(如 Stack 继承 List),导致子类暴露了不该暴露的接口。改用委托持有旧超类对象。