每日好书推荐:《重构:改善既有代码的设计》
推荐日期:2026-05-04
作者:Martin Fowler(马丁·福勒),第2版示例以 JavaScript 为主
英文名:Refactoring: Improving the Design of Existing Code
适合谁读:写过一段时间代码、开始维护老项目、经常被“祖传代码”折磨的开发者;技术负责人、架构师、测试工程师也很适合。
一句话推荐:这本书教你把“能跑但难改”的代码,一小步一小步变成“能跑、好懂、好改”的代码。
先说结论:这本书到底解决什么问题?
很多人以为写代码的难点是“从0到1写出来”。但真实项目里,更大的难点往往是:代码已经存在,功能也能跑,可是没人敢改。变量名看不懂,函数又长又绕,业务规则散落在各处,一改就炸。这个时候,不是推倒重来,而是要重构。
重构的核心定义很朴素:在不改变软件外部行为的前提下,改善代码内部结构。换成大白话就是:用户看到的功能不变,但程序员看到的代码变清楚、变稳、变容易维护。
马丁·福勒这本书的主线可以概括为四句话:
- 先保证安全:重构不是凭感觉大改,必须有测试兜底,小步修改、频繁验证。
- 先闻到坏味道:重复代码、超长函数、奇怪命名、到处传参数、复杂条件,都是代码在向你求救。
- 再选合适手法:提炼函数、封装变量、搬移函数、拆分阶段、以多态取代条件等,都是有章法的工具。
- 长期持续改进:重构不是“有空做一次大扫除”,而是平时开发时随手整理,让代码始终保持可演进。
第1章:重构,第一个示例——先看一场“代码整理现场”
第一章不是先讲大道理,而是直接拿一个计费程序做示范。原始代码能跑,但逻辑混在一起:既算金额,又算积分,还负责拼输出文本。它的问题不在于“错”,而在于“难读、难扩展”。
作者的第一步是建立测试,确认现在的输出是什么。因为重构的底线是外部行为不变,测试就像安全绳。没有安全绳,你以为自己在整理代码,其实可能在制造事故。
接着,作者把一个大函数逐步拆开:把金额计算提炼成函数,把积分计算提炼成函数,把临时变量替换成查询函数。这样做的结果是:每个小函数只回答一个问题,名字也能直接表达意图。
然后,作者把“计算”和“格式化输出”分成两个阶段。大白话说,就是别让一个人同时做会计、排版、客服。先把账算清楚,再决定怎么展示。这样以后如果要输出 HTML、JSON 或纯文本,就不用改核心计算逻辑。
最后,作者用多态把不同戏剧类型的计费规则分开。原来靠 if/switch 判断类型,现在让不同类型各自负责自己的算法。重点不是“炫技用面向对象”,而是把变化点放到合适的位置:以后新增类型,就少改旧代码。
这一章想告诉读者:重构不是一次性大手术,而是一串非常小的动作。每一步都很小,小到可以测试;但积累起来,代码结构会发生质变。
第2章:重构的原则——什么时候该改,什么时候别乱改
这一章给重构立规矩。首先,重构有一个严格边界:不添加新功能,不修复可见行为,只改善内部结构。如果你一边加功能一边整理代码,就要知道自己在换“帽子”:开发新功能是一顶帽子,重构是一顶帽子。两顶帽子可以交替戴,但不要同时戴,否则出了问题很难定位。
为什么要重构?第一,重构能改善设计。代码如果长期只加不整理,设计会慢慢腐烂。第二,重构能让代码更容易理解。机器不在乎代码好不好懂,人会在乎。第三,重构能帮助发现 bug。因为你把逻辑拆清楚以后,隐藏的矛盾更容易暴露。第四,重构能让开发更快。短期看像是在“停下来整理”,长期看是在减少每次改动的阻力。
什么时候重构?作者强调“见缝插针”。加功能前,如果旧结构挡路,先整理一下;修 bug 时,如果发现代码看不懂,先把逻辑理清;做代码评审时,如果发现坏味道,也可以顺手重构。最好的重构不是开一个“重构大项目”,而是日常开发中的持续清洁。
重构也有挑战。数据库、公开 API、复杂继承、性能敏感代码、缺少测试,都会提高风险。作者不是鼓励盲目重构,而是提醒:越危险的地方,越要小步、测试、沟通。
关于架构,作者的态度很实用:不要幻想一开始就设计完美。可以先做够当前需求的设计,再通过重构逐步演进,这和 YAGNI(你不会需要它)是一致的。架构不是神谕,而是可以在反馈中成长。
关于性能,作者也很清醒:不要为了想象中的性能牺牲清晰度。大多数时候,先写清楚,再测量瓶颈,再针对性优化。清楚的代码反而更容易优化。
第3章:代码的坏味道——代码不会说话,但会“发臭”
这一章由 Kent Beck 和 Martin Fowler 总结了常见坏味道。坏味道不是绝对错误,而是提示你:“这里可能值得看看”。
- 神秘命名:变量、函数、类的名字看不出意图。解决方向是改名,让名字讲人话。
- 重复代码:同样逻辑出现多次。重复越多,修 bug 越容易漏。常用提炼函数、搬移函数来消除。
- 过长函数:一个函数塞太多事。它通常需要被拆成一组有名字的小函数。
- 过长参数列表:函数需要一长串参数,说明数据可能应该打包成对象,或者函数依赖关系不清。
- 全局数据:谁都能改,谁都可能背锅。应该封装访问入口,减少失控修改。
- 可变数据:数据被多处修改,状态变化难追踪。可以用封装、拆分变量、减少共享可变状态来控制。
- 发散式变化:一个模块因为多种原因被频繁修改。说明职责混杂,需要拆分。
- 霰弹式修改:一个小需求要改很多地方。说明相关逻辑散落,需要聚合到一起。
- 依恋情结:一个函数总是访问别的对象的数据,说明它可能应该搬到那个对象附近。
- 数据泥团:几个字段总是一起出现,比如 start/end、x/y。它们可能应该组成一个对象。
- 基本类型偏执:什么都用字符串、数字表示,业务含义藏不住。可以用小对象表达领域概念。
- 重复的 switch:同样的类型判断散落各处。可以考虑多态或集中管理变化点。
- 循环语句:循环本身不是坏事,但复杂循环会藏逻辑。可以拆分循环或用管道表达。
- 冗赘的元素:类、函数、接口存在但没提供价值。该删就删,别为形式而形式。
- 夸夸其谈通用性:为了未来可能的需求搞一堆抽象。未来没来,复杂度先来了。
- 临时字段:对象里有些字段只在特殊场景使用,平时为空。说明对象职责可能不稳定。
- 过长的消息链:调用像 a.getB().getC().getD() 一路挖。调用者知道太多内部结构。
- 中间人:一个类只是转发请求,不做实事。该移除就移除。
- 内幕交易:模块之间过度访问彼此内部。边界不清,容易牵一发动全身。
- 过大的类:一个类知道太多、做太多。需要提炼类或拆职责。
- 异曲同工的类:两个类做类似事情却接口不同。可以统一接口或合并抽象。
- 纯数据类:只有字段和 getter/setter,没有行为。可能需要把相关行为搬进去。
- 被拒绝的遗赠:子类继承了一堆不想要的父类能力。继承关系可能错了。
- 注释:注释本身不是坏事,但如果注释是在解释糟糕代码,优先让代码自解释。
这一章最有价值的地方是:它给程序员一套“诊断词典”。你不用凭模糊感觉说“这段代码怪怪的”,而可以说:“这里是过长函数加数据泥团,建议提炼函数并引入参数对象。”
第4章:构筑测试体系——没有测试,重构就是裸奔
重构最怕的是:你以为没改行为,结果行为悄悄变了。所以作者强调自测试代码。所谓自测试,就是程序自己能告诉你“我现在还对不对”。
这一章用示例说明怎样从第一个测试开始。不要等测试体系完美了再写测试,先给关键行为加一个能跑的测试。然后逐步补充更多场景,尤其是边界条件。边界条件往往是 bug 最爱躲的地方,比如空值、零、最大值、临界日期、异常输入。
作者还强调测试夹具,也就是测试前准备好的对象和数据。好的测试夹具能让测试更清楚,不用每个测试都重复搭环境。
这章的大白话结论是:测试不是为了证明你很专业,而是为了让你敢改。没有测试时,人会越来越怕碰老代码;有测试时,重构才有底气。
第5章:介绍重构名录——每个手法都要讲清楚“何时用、怎么做”
从第6章开始,书进入大量具体重构手法。第5章相当于使用说明书,告诉你每个重构条目会怎样展开:名称、动机、做法、示例。
这很重要,因为重构不是“灵感创作”,而是工程动作。一个好重构手法应该说清楚:它解决什么坏味道,适用什么场景,步骤如何拆小,风险在哪里,怎样验证。
作者也提醒,选择重构要看上下文。没有哪一种手法永远正确。比如提炼函数能提升可读性,但如果函数名起不好,反而更绕;封装能保护数据,但过度封装也会让代码啰嗦。重构是判断力和工具箱的结合。
第6章:第一组重构——最常用的基础动作
这一章是重构工具箱的入门套装,几乎每天都能用。
- 提炼函数:把一段有独立含义的代码抽成函数,并用好名字表达意图。它能降低阅读负担。
- 内联函数:如果函数内容和名字一样简单,或者抽出来反而绕,就把它放回去。重构不是只会拆,也要会合。
- 提炼变量:复杂表达式看不懂时,用一个变量名解释中间含义。
- 内联变量:如果变量只是重复表达式,没有增加理解价值,就去掉。
- 改变函数声明:函数名、参数、返回值不合适时就调整,因为函数声明是模块之间的契约。
- 封装变量:别让重要数据到处被直接访问,先包一层访问函数,给未来控制变化留入口。
- 变量改名:命名是最便宜也最有效的重构之一。好名字能省掉很多注释。
- 引入参数对象:一组参数总是一起出现,就把它们变成一个对象,让关系显性化。
- 函数组合成类:一堆函数围绕同一组数据工作,可以把数据和函数放进一个类里。
- 函数组合成变换:如果一组函数都是把原始数据加工成派生数据,可以集中成一个变换过程。
- 拆分阶段:一段代码同时做两类事情,就拆成两个阶段,比如先解析、再执行;先计算、再展示。
这一章的精神是:先把意图显露出来。很多烂代码并不是算法多难,而是“意图被实现细节淹没了”。
第7章:封装——把该藏的藏起来,把该表达的表达出来
封装不是为了把代码包得神神秘秘,而是为了控制变化影响范围。
- 封装记录:如果数据结构只是裸字段,外部到处依赖字段名,未来改字段会很痛。封装后可以通过访问函数保护变化。
- 封装集合:集合不能随便暴露给外部直接改,否则对象不变量会被破坏。应该提供受控的添加、删除方式。
- 以对象取代基本类型:比如电话号码、金额、日期范围,不只是字符串或数字,它们有规则和行为,适合变成对象。
- 以查询取代临时变量:临时变量如果表达的是某种可计算含义,可以变成查询函数,让逻辑更可复用。
- 提炼类:一个类做太多事,就把其中一组相关字段和行为抽出去。
- 内联类:如果一个类太瘦、没有独立价值,就合回去,减少不必要结构。
- 隐藏委托关系:调用者不应该知道太多中间对象,让被调用对象提供更直接的方法。
- 移除中间人:如果一个对象只会转发,没有隐藏复杂度,就别让它挡路。
- 替换算法:当你找到更清楚、更可靠的算法时,可以整体替换,但前提是测试覆盖行为。
这一章背后的判断标准是:让外部依赖稳定的概念,而不是内部细节。代码越少暴露细节,未来越好改。
第8章:搬移特性——东西应该放在最懂它的地方
很多代码变乱,是因为函数、字段、语句放错了位置。第8章专门讲“搬家”。
- 搬移函数:函数总是使用某个模块的数据,就考虑搬到那个模块。让行为靠近数据。
- 搬移字段:字段如果更属于另一个对象,就搬过去。数据位置决定了依赖方向。
- 搬移语句到函数:某些语句每次调用函数前后都要写,就把它们并入函数,减少重复仪式。
- 搬移语句到调用者:如果函数内部某些语句只适合部分调用场景,就移出去,让函数更纯粹。
- 以函数调用取代内联代码:同样逻辑已经有函数了,就别复制粘贴,直接调用。
- 移动语句:把相关语句靠近放置,让阅读顺序更符合数据流。
- 拆分循环:一个循环里做多件事,拆开后每个循环表达一个目的,之后更容易优化或复用。
- 以管道取代循环:对于过滤、映射、聚合类逻辑,用管道式表达能更直接说明“数据怎么流动”。
- 移除死代码:不用的代码不要留着当纪念品。版本控制会记住历史,代码库应该保持清爽。
这章的核心问题是:读者看到一段逻辑时,能不能自然找到它?如果总要跨文件、跨类追来追去,就说明位置可能不对。
第9章:重新组织数据——数据结构一乱,逻辑迟早跟着乱
程序的很多复杂度来自数据。数据表达不清,函数再漂亮也救不了。
- 拆分变量:一个变量被拿来表示多个含义,就拆成多个变量。变量应该“一生只干一件事”。
- 字段改名:字段名会传播到很多地方,名字不准会长期误导人。
- 以查询取代派生变量:如果某个值可以从其他数据算出来,且保持同步很麻烦,就用查询函数动态计算。
- 将引用对象改为值对象:如果对象只由值决定身份,比如金额、坐标、日期范围,可以用值对象,减少共享状态问题。
- 将值对象改为引用对象:如果对象有独立身份,需要被多处共享并保持一致,比如客户、账户,就用引用对象。
这一章最重要的是区分“值”和“身份”。两个 100 元金额通常可以视为相等的值;但两个用户即使名字一样,也不是同一个身份。这个区别会影响对象建模。
第10章:简化条件逻辑——别让 if/else 长成迷宫
条件逻辑是业务代码最常见的复杂度来源。第10章教你把条件写得更像人话。
- 分解条件表达式:复杂 if 条件、then 分支、else 分支都可以提炼成函数,用名字解释含义。
- 合并条件表达式:多个条件导致同一结果,就合并成一个表达式,减少重复分支。
- 以卫语句取代嵌套条件表达式:先处理特殊情况并提前返回,主流程就不用被层层嵌套包住。
- 以多态取代条件表达式:当条件是根据类型选择行为时,可以让不同类型自己实现行为。
- 引入特例:对于空对象、未知客户、默认用户这类特殊情况,可以建一个特例对象,避免到处判空。
- 引入断言:把代码运行所依赖的前提明确写出来,帮助读者和程序尽早发现不合理状态。
这章的大白话原则是:正常路径要清楚,特殊路径要明确。不要让读者在一层又一层缩进里找主线。
第11章:重构 API——接口设计就是沟通方式
API 是代码之间的边界。边界设计得好,调用者轻松;边界设计得差,所有人都痛苦。
- 将查询函数和修改函数分离:一个函数要么回答问题,要么改变状态,最好别两者混在一起,避免调用者被副作用坑到。
- 函数参数化:多个函数逻辑类似、只是常量不同,可以合成一个带参数的函数。
- 移除标记参数:函数传 true/false 来决定两套行为,调用处很难懂,通常应拆成两个明确函数。
- 保持对象完整:如果函数需要对象的多个字段,直接传整个对象,而不是拆成一堆零件。
- 以查询取代参数:如果参数可以由被调用方自己算出来,就别让调用者传,减少出错机会。
- 以参数取代查询:反过来,如果函数不该依赖某个外部查询,就把结果作为参数传入,让依赖更明确。
- 移除设值函数:不该被随意修改的字段,就不要提供 setter。对象创建后保持稳定更安全。
- 以工厂函数取代构造函数:当创建逻辑有多种意图或需要隐藏具体类型时,工厂函数更清楚。
- 以命令取代函数:当一个操作很复杂,需要状态、撤销、排队、日志时,可以变成命令对象。
- 以函数取代命令:如果命令对象已经过度复杂,而实际只是简单操作,就退回普通函数。
这一章的关键是权衡:参数少不一定好,函数多不一定坏。好 API 应该让调用者一眼知道“我要做什么”,同时减少误用空间。
第12章:处理继承关系——继承能帮忙,也能添乱
继承是强工具,但也容易造成强耦合。最后一章讲如何整理继承体系。
- 函数上移:多个子类有相同方法,就提到父类,消除重复。
- 字段上移:多个子类有相同字段,也可以上移到父类。
- 构造函数本体上移:子类构造过程有共同部分,就提到父类构造逻辑中。
- 函数下移:父类方法只对部分子类有意义,就下移到真正需要的子类。
- 字段下移:父类字段并非所有子类都需要,也应该下移。
- 以子类取代类型码:对象里有 type 字段决定行为时,可以用子类表达不同类型。
- 移除子类:如果子类差异很小或不再需要,就合并回去,别保留空架子。
- 提炼超类:多个类有相似行为时,可以抽出共同父类。
- 折叠继承体系:父类和子类区别不大,就合并,减少层级。
- 以委托取代子类:当子类只是表达一种可变角色或策略时,用组合/委托更灵活。
- 以委托取代超类:如果继承只是为了复用一部分能力,却带来不合适的父子关系,就改用委托。
这一章的底层思想是:不要迷信继承。继承适合表达稳定的“是一种”关系;如果只是想复用代码,或者对象角色会变化,组合和委托常常更稳。
读完这本书,最该带走的实践清单
- 改代码前先找安全绳:没有测试就先补关键测试,至少覆盖你准备动的行为。
- 小步前进:一次只做一个小重构,跑测试,再继续。别把十个想法揉成一次提交。
- 命名优先:如果你不知道怎么重构,先把变量、函数、类的名字改准,很多结构问题会自己浮出来。
- 消除重复:重复代码不是省事,是未来维护成本的贷款。
- 让行为靠近数据:函数总是用谁的数据,就考虑搬到谁身边。
- 把复杂条件变成清楚结构:提炼条件、卫语句、特例对象、多态,都是为了让主线更明白。
- 公开 API 要慎重:内部代码好改,公开接口难改。改 API 时要考虑调用者迁移成本。
- 别过度设计:为真实变化重构,不要为脑补的未来堆抽象。
- 持续整理:重构像刷牙,靠日常习惯,不靠一年一次大扫除。
为什么今天推荐它?
《重构》是少数能真正改变程序员工作方式的书。它不是讲宏大架构,也不是堆概念,而是把“如何把一段糟糕代码慢慢变好”拆成了可操作动作。
对于小白来说,这本书能帮你建立代码审美:什么叫清楚,什么叫职责单一,什么叫变化点,什么叫坏味道。对于有经验的开发者来说,它能提醒你:真正厉害的工程能力,不只是写新代码快,而是让旧代码也能继续生长。
如果你的项目里有“不敢碰”的模块,这本书就是一套拆弹手册。它不会让坏代码一夜变完美,但会教你每次安全地变好一点。