你有没有过这样的体验:租房住了一年后,抽屉里塞满数据线、衣柜里叠着换季被子、书桌上摊着三个快递箱——东西一样没少,但你就是找不到钥匙。代码也是这样。功能都在、测试能过,可每改一个需求都要翻遍五个文件,这就是“代码坏味道”在作祟。代码重构(Refactoring)就像一次彻底的房间整理:不扔东西(外部行为不变)、不换房子(不重写系统),只是把物品重新归位(改善内部结构),让每件东西待在它该待的地方,下次找起来又快又准。本文将从“整理房间”的视角,带你看懂 Martin Fowler 的经典重构手法:如何通过坏味道判断哪里该收拾,如何用提取函数、内联变量、搬移特性三步整理法把大而乱的函数拆成清爽的小模块,并借助“测试保护网”像拍照清单一样确保整理过程不弄丢任何东西。最后你会发现,重构不是一次性的周末大扫除,而是每天随手整理十五分钟的好习惯。
对 AI 从业者而言,重构能力决定了你能否在快速迭代中守住代码质量底线——无论是训练脚本、推理服务还是数据管道,会重构的人才能让代码越改越清晰、越跑越稳。
目录
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
重构本质:整理房间,不换家
房间乱了,摆在你面前的无非两个选择:搬新家,或者动手收拾。搬新家听着痛快,但要重新刷墙、置办家具、研究新小区的快递柜在哪儿,还可能搬家途中弄丢几件重要物件。收拾房间则完全不同——家具还在原位、东西一件不丢,只是把随处乱放的物品重新归类,让整个空间焕然一新。代码重构就是后一种:它不动摇系统的“地基”,不改变程序的“对外表现”,只在内部把结构理得更顺,让后来的人一眼就能看懂、一改就能下手。
“外部表现”在代码世界里就是程序的行为:同样的输入,重构前和重构后必须给出同样的输出。回想一下,你给房间拍照时,光线、角度、家具的轮廓都没变,变的只是内部每个抽屉的摆放逻辑。这正是重构与重写最本质的分水岭——重写是“拆了盖新的”,重构是“微创整理”。
在工程上,重构被定义为:在不改变软件可观察行为的前提下,通过一系列小步骤改善代码内部结构。Martin Fowler 在他的著作中强调,重构不是一次性的剧烈运动,而是大量细碎动作的累积,每个动作小到几乎不可能出错。小到什么程度?小到每一步之后都可以立刻运行测试确认没有破坏任何东西。
所以,重构的底气来自“等价性”。哪怕只是把一个表达式提出来变成变量,也必须保证前后行为毫无变化。有了这把标尺,你整理的每一步都有据可依:整理不是凭感觉乱摆,而是按照“等价”这条准绳精确归位。
- 全称量词$\forall x$ 表示“对任意一个输入 x”,这就像整理房间时承诺“房间里任何一件物品都不能丢”,而不是“大部分物品别丢就行”。
- 合法输入集合$x \in \text{Domain}$ 指明 x 来自程序的合法输入范围。Domain 是“活动区域”,测试用例都从这块区域里取样,证明了这片区域安全,才敢说重构成功。
- 行为等价$P_{\text{after}}(x) = P_{\text{before}}(x)$ 是整句话的核心,它要求重构前后的程序在相同输入下产出相同结果。这就是“物品清单对得上号”的数学表达。
坏味道识别:房间里那些不对劲的信号
判断房间要不要收拾,不必等它变成垃圾场。有些信号早就出现了:袜子出现在厨房水槽边、剪刀消失在一个月前的快递盒里、想找支笔要翻遍三个抽屉——这些细节都是房间的“坏味道”,它们不致命,却在悄悄耗尽你的时间。代码世界同样如此。代码坏味道(Code Smell)是代码中疑似存在深层问题的表面迹象,它不会让程序立刻崩溃,但会让每一次改动都变得沉重、危险。
最常见的坏味道有四种。重复代码:同一段业务逻辑在多处各写各的,改一处忘一处,就像两把一模一样的钥匙丢在家里的不同角落,出门前总担心其中一把是不是丢在了昨天地铁上。过长函数:一个函数动辄几百行,开头读得明白,看到结尾已经忘了开头,如同一只塞满杂物的抽屉,要找一根数据线必须把整抽屉倒出来。过长参数列表:函数签名排着七八个参数,调用者要小心翼翼对齐每一个位置,仿佛出门要带五个包,哪个装什么全靠记忆。霰弹式修改:改一个需求要同时跑遍五六个文件修修补补,就像为了挪一张餐桌,得先把客厅、餐厅所有的摆设全部挪动一遍。
这些坏味道有一个共同的“体检指标”——圈复杂度(Cyclomatic Complexity)。它统计函数中独立执行路径的条数:每条 if、for、while 分支都会多出一条路。路径越多,函数越难读、越容易埋 bug、越难写测试。把这个指标量化出来,就是经典的圈复杂度公式:
坏味道就是房间向你发出的求救信号,圈复杂度则是测量乱象的“凌乱指数”。学会识别这些信号,你才知道该从哪里开始动手,而不是对着整间屋子发呆。
- 边数 E$E$ 是控制流图中的边数,也就是程序可能走的“流转方向”。一个 if 的两个分支就是两条边,如同房间里的两条通道。
- 节点数 N$N$ 是控制流图中的节点数,每个语句块或判断条件是一个节点,如同房间里的每个“落脚点”。
- 连通分量 P$P$ 是图中互相连通的区域数。通常一个函数自成一体,$P = 1$,此时公式简化为 $M = E - N + 2$。
- 圈复杂度 M整个公式的结果就是独立路径数量。$M$ 越大,意味着测试需要覆盖的路线越多,代码理解成本越高。
提取函数:把杂物装进收纳盒
房间整理最常用的一招是“收纳盒法”:把散落在桌面、茶几、地板上的同类小物——充电线、文具、票据——收进一个盒子,贴上标签。从此桌面清爽,找东西时只需拿起对应盒子,掀开盖子就能看到。提取函数(Extract Function / Extract Method)正是代码世界的收纳盒。当一段代码能够表达一个完整意图,比如“计算订单折扣”“校验用户输入”,并且它和周围的逻辑相对独立时,就把它从大函数中抽取出来,变成一个带名字的新函数,原位置只留下一个调用。
具体怎么操作?第一步,在大函数里圈出高内聚的语句块,划定“收纳盒”的边界。第二步,处理局部变量:新函数要读取的变量作为参数传进来;新函数要改写的变量作为返回值带出去。第三步,命名。好的函数名应该描述“做什么”而非“怎么做”,就像收纳盒上写“充电线”而不是“黑色塑料捆”。第四步,把原位置的代码替换成对新函数的调用,立刻运行测试确认行为不变。
设大函数 F 的语句序列为 [s1, s2, s3, s4],我们把其中 s2、s3 提取成新函数 G:
收纳盒让桌面一次只放一种东西,提取函数让每个函数只操一份心,可读性与可测试性同步提升。不过要小心:如果被提取段既读又写同一个局部变量,就像一条毛巾同时搭在挂钩和椅背上,硬扯出来容易带翻旁边的花瓶。这不代表不能提取,而是要先做变量改名、拆分循环等预处理,让变量的归属变得清晰,再从容地打开收纳盒。
F(x) = \mathrm{Seq}\bigl(s_1(x),\, s_2(x),\, s_3(x),\, s_4(x)\bigr)\quad\Longrightarrow\quadF'(x) = \mathrm{Seq}\Bigl(s_1(x),\; G\bigl(s_2(x), s_3(x)\bigr),\; s_4(x)\Bigr)- 语句序列 Seq$\mathrm{Seq}(s_1, s_2, s_3, s_4)$ 表示函数体按顺序执行四段代码,如同整理前桌上依次堆着四堆杂物,顺序不能乱。
- 提取边界$G(s_2, s_3)$ 把第二、三堆杂物圈进同一个新盒子,这是“收纳盒”的边界划定——相关的东西放一起,不相关的留在外面。
- 执行顺序守恒$F'$ 依然是先 $s_1$、再调用 $G$、最后 $s_4$,执行顺序完全没有改变,这是“东西一件没少、也没挪错位”的数学保证。
- 调用关系$G(...)$ 出现在 $F'$ 内部,意味着新函数 G 就像一只贴好标签的收纳盒,主函数通过“调用”这个动作随时取用里面的东西。
内联与搬移:扔掉中转箱,物品归其位
收纳盒用久了,也会出现两种尴尬。第一种:家里堆了一排空盒子,明明可以把东西直接放到最终位置,偏要经手一层“中转箱”。第二种:厨房的烘焙工具躺在卧室衣柜里,每次要用都得从卧室搬到厨房,用完再搬回去。内联(Inline)解决第一种尴尬,搬移(Move)解决第二种。
内联是提取函数的逆操作。当一个函数的函数体比函数名更直观,或者一个中间变量的名字不比它代表的表达式更有意义时,这一层“包装”就纯属多余。举个例子,const price = base * 0.8 如果只被用到一次,而 base * 0.8 又一看就懂,那还不如直接把表达式写到使用处,把变量名删掉。内联函数同理:调用方读起来完全不比函数体费解,那就拆掉这层壳,让逻辑“裸露”在阳光下。
再说搬移。判断依据很简单:特性 f 被谁使用得最多,就应该住在谁家。函数调用次数不会说谎——一个函数被类 B 引用十次、被类 A 引用两次,那它就属于 B,应该搬家。
提取是“汇聚”,内联是“简化”,搬移是“归位”。这三招配合起来,就能把整理工作覆盖到房间的每一个角落。收放之间,代码结构越来越贴近真实世界的“高内聚、低耦合”。
- 使用次数$\text{Uses}(f, B)$ 统计 f 在类 B 中被引用的次数,这是“谁更依赖它”的量化证据。
- 比较关系$>$ 表示 B 对 f 的依赖强于 A,搬家方向由此确定,不靠感觉、靠数据。
- 搬移动作$\text{Move}(f, A \to B)$ 是执行动作,从模块 A 迁移到模块 B,同时更新所有引用点,并保证测试依然全绿。
测试保护网:整理前的照片与清单
整理房间最怕什么?怕收完发现东西丢了。聪明的做法是:动手前先给每个角落拍照片、列物品清单;每收拾完一个区域就对着照片清点一次。有了这套对照系统,哪怕不小心弄乱,也能立刻发现并对症找回。代码重构的“照片”就是测试。重构圈子里有一条铁律:没有测试就动手重构,等于在悬崖边跳舞。
具体的舞蹈节奏叫“红绿重构循环”(Red-Green-Refactor)。第一步“红”:先写一个会失败的测试,确认新行为确实缺失;第二步“绿”:用最直接的办法让测试通过,不管代码美不美,先保证行为正确;第三步“重构”:在测试持续为绿的前提下,把刚写出来的代码整理干净。整个过程像过马路看红绿灯——红灯停(测试失败)、绿灯行(测试通过)、黄灯整理(重构优化)。
那这层保护网织得够不够密,怎么度量?看覆盖率。
测试就是那张“物品清单”,让你有底气说“我整理过,东西一件没少”。测试全绿,重构才能小步快跑、随时收手;测试如果飘红,说明刚才那步动坏了什么,立刻回滚到上一个绿点即可。保护网越密,重构的手越敢下。
- 被覆盖行数$\text{executed\_lines}$ 是测试执行时真正跑到的代码行数,相当于清单上已经逐项核对过钩的物品。
- 总行数$\text{total\_lines}$ 是被测模块的全部代码行数,等于房间里所有物品的总数。
- 覆盖率$C$ 是两者之比乘以 100%,得到 0 到 100 之间的百分比。60% 的覆盖率意味着清单上还有四成物品没核对,别急着大动干戈。
- 实践参考关键路径的覆盖率应尽量逼近 100%,但普通代码不必盲目追求 100%——你不会为了一粒灰尘给整个房子套上防尘罩。
重构节奏:每天十五分钟胜过周末大扫除
同样是整理房间,有人一年只大扫除一次,累到虚脱、期间还容易弄丢东西;有人每天睡前随手收拾五分钟,房间永远保持八十分以上的整洁。重构的哲学是后者。与其攒出一座“代码屎山”再发起总攻,不如把整理变成日常呼吸的一部分。代码质量不是靠一次“重构周”救回来的,而是靠无数个微小的好习惯堆出来的。
三个纪律决定了节奏是否健康。时机判断:不用专门留出“重构月”,最好的时机就藏在日常里——添加新功能时发现现有结构碍手碍脚,修 bug 时发现相关代码绕成一团,代码评审时发现明显的坏味道,都是顺手重构的黄金时刻。一次一处:一次只做一个重构动作,做一步、测一步、交一步。像整理房间时一次只收拾一个抽屉,绝不把整个衣柜都倒出来。频繁提交:小步提交让每一次改动都有记录、可回溯,出问题秒回退,避免了“改了一整天最后不知道哪里错了”的绝望。
把这些微小的改进叠加起来,你会发现一个惊喜——复利效应。质量不是线性增长,而是指数增长:
整理房间的秘诀从来不是“大扫除”,而是“物归原位 + 每日微调”。重构也一样:小步、频繁、有测试守护,让代码库像滚雪球一样越滚越干净。你今天花十五分钟提取的那个函数,可能为三个月后的自己省下一个下午的排查时间。
- 起始质量$Q_0$ 是起点时刻代码库的可维护程度,可能是 60 分也可能是 20 分,不重要,重要的是开始。
- 改进率$r$ 是每次重构带来的质量提升比例。它不必大,3% 已经很好——关键是“每次都有”。
- 迭代次数$t$ 是重构发生次数的统计,可以是天数、提交数或评审轮数。$t$ 越大,复利效果越惊人。
- 复利结果$Q(t)$ 是 $t$ 次改进后的质量水平。$(1+r)^t$ 表明持续高频小幅改进的长期回报,远胜于憋大招式的集中翻新。
AI 应用场景
CI 流水线中的坏味道自动守卫
阶段:持续集成 / 质量保障
解决的问题:人工 Code Review 往往只关注功能正确性,坏味道会悄悄溜进主干。一个函数从 20 行长到 80 行,每次评审都“下一版再改”,最终无人敢动。在 CI 流水线中集成圈复杂度检查与重复代码扫描(如 SonarQube),就像给房间装了一台“凌乱指数传感器”,数值超标立刻报警。
具体案例:某支付团队在每次合并请求触发流水线,新增代码圈复杂度超过 10 时黄色告警,超过 15 时直接阻塞合并。开发者被迫当场拆分函数,三个月后仓库内“超级大函数”数量下降 60%。
遗留系统重构的测试先行策略
阶段:测试策略设计 / 技术债治理
解决的问题:一个运行了八年的订单系统无人敢动,因为没人说得清每个分支在干什么。直接重构如同在黑暗的房间里挪家具,随时可能绊倒。采用“测试先行”策略:先用“行为快照测试”把核心函数当前行为固化下来,再小步提取函数、搬移特性,每一步都跑全量回归。
具体案例:某银行核心模块重构前,团队花两周为 30 个核心函数编写特征测试,之后重构期间每次回归从 12 分钟压缩到 40 秒,三个月的重构周期内线上零事故。
代码评审中的重构讨论
阶段:代码评审 / 团队协作
解决的问题:评审意见常常是“这个函数太长了”,但缺少具体改进路径,提出者与改进者都只有模糊感觉。以重构目录作为共同语言,评审意见可以精确到“建议将第 14-18 行提取为 saveOrder()”,让讨论从“我感觉”升级为“我们用这个手法”。
具体案例:某前端小组每周五举办“重构下午茶”,轮流挑选一小段代码做提取、内联、搬移演示,并用重构前后对比图展示效果。半年后团队代码平均函数长度下降 41%,评审通过率明显提升。
Python 演示
import ast class SmellDetector(ast.NodeVisitor): def __init__(self, max_lines=15, max_args=5, max_cc=10): self.max_lines = max_lines self.max_args = max_args self.max_cc = max_cc self.smells = [] def visit_FunctionDef(self, node): lines = node.end_lineno - node.lineno if lines > self.max_lines: self.smells.append( "long-function: {} has {} lines, suggest extract function".format( node.name, lines ) ) args = len(node.args.args) if args > self.max_args: self.smells.append( "long-parameter-list: {} has {} args".format(node.name, args) ) cc = self._cyclomatic_complexity(node) if cc > self.max_cc: self.smells.append( "high-complexity: {} has CC={}, try extract function".format( node.name, cc ) ) self.generic_visit(node) def _cyclomatic_complexity(self, node): score = 1 for n in ast.walk(node): if isinstance(n, (ast.If, ast.While, ast.For, ast.ExceptHandler)): score += 1 elif isinstance(n, ast.BoolOp): score += len(n.values) - 1 return score SAMPLE = """def place_order(user, cart, coupon, addr, pay, note): total = 0 for item in cart: total += item.price * item.count if coupon and total > 100: total = total * 0.9 if not addr: raise ValueError("no address") if not pay: raise ValueError("no pay method") if note: print("note:", note) print("total:", total) print("ship to:", addr) print("pay by:", pay) return total""" def main(): tree = ast.parse(SAMPLE) detector = SmellDetector(max_lines=6, max_args=4, max_cc=4) detector.visit(tree) if detector.smells: print("Found {} smell(s):".format(len(detector.smells))) for s in detector.smells: print("- " + s) else: print("No smell found. Great!") if __name__ == "__main__": main()
class SmellDetector(ast.NodeVisitor):继承 AST 访问者类,让代码自动“逛”遍每一个语法节点,如同带着清单逐间检查房间。def visit_FunctionDef(self, node):访问者回调方法,碰到任意函数定义就停下来执行检查,是扫描仪的核心触点。lines = node.end_lineno - node.lineno用函数结束行号减开始行号算出函数体长度,对应“房间面积”测量。cc = self._cyclomatic_complexity(node)调用自定义方法计算圈复杂度,相当于给每个函数测“凌乱指数”。elif isinstance(n, ast.BoolOp):识别and/or逻辑运算符,每个运算符都会增加一条独立路径,让圈复杂度上升。tree = ast.parse(SAMPLE)把源码字符串解析成 AST 抽象语法树,就像给房间画一张结构图,之后所有测量都在结构图上进行。detector.visit(tree)启动全屋巡检,所有坏味道被逐一记录进 smells 列表,最终打印成“整改建议书”。
常见误区
误区看到一段代码乱得离谱,第一反应是“这没法救了,全部重写吧”。觉得重构太麻烦,重写更干脆利落,还能用上最新技术栈。
正解重构是保持行为不变的结构调整,重写则是抛弃旧代码另起炉灶。重写意味着丢失旧系统里积累的无数边界处理逻辑——那些历史 bug 修复留下的“补丁”往往是最宝贵的资产。就像你嫌衣柜乱,把整个衣柜扔了买新的,结果新衣柜里找不到那条女朋友送的围巾。重构的风险在过程中可控,重写的风险在交付前不可见。
为何常见:重写的诱惑在于“从零开始”的掌控感,而重构要求你面对真实存在的混乱并一小步一小步地啃,前者更符合人对“干净”的想象。
误区“这个改动很小,就是挪一下位置,不用写测试了吧?”许多开发者在没有任何测试保护的情况下直接开始重构,还自信地认为凭细心就能避免问题。
正解重构的本质是“保持行为不变”,但没有测试,你拿什么证明行为没变?人类对复杂系统的记忆和判断力都极其有限,哪怕移动三行代码也可能改变变量作用域。测试保护网的意义在于把“我觉得没问题”变成“测试证明没问题”。没有测试的重构,就像闭着眼睛整理衣柜,还声称“我肯定没弄乱”。
为何常见:写测试需要额外时间,而重构又常常发生在时间紧迫的时候。人总是高估自己的专注力,低估代码的隐蔽关联。
误区学完提取函数后兴奋不已,见 5 行代码就提一个函数,最后整个文件变成了几十个短小但毫无关联的函数片段,阅读时要在函数之间反复跳转。
正解提取的目的是“高内聚、有意义的单元”,不是为了切碎。一个函数该不该提取,取决于它是否表达了一个完整独立的意图。比如“计算折扣”值得提取,“把 total 加一”就不值得。过度的提取,就像把房间用无数个小盒子塞满,表面上整洁,实际找东西要翻几十个盒子——比原来更累。
为何常见:初学者容易把“短函数”等同于“好代码”,忽略了“命名”和“语义边界”才是提取的关键。功能的碎片化会带来新的理解成本。
误区“反正这段代码重构后也要改,不如把新需求一起做了,省得改两遍。”于是重构混合着需求变更,一次提交里既有结构变动又有新行为。
正解一次只做一件事是重构的黄金法则。重构要保证行为不变,加功能要改变行为,两者混在一起,一旦测试失败,你无法判断是重构引入的回归还是新功能的 bug。这就像整理房间时还顺带买了几件新家具,最后发现房间乱了,分不清是挪旧的弄乱的还是新家具摆错了。
为何常见:人天然想节省往返成本,但工程上“可定位性”比“省一趟”重要得多。小步提交让每一次差异都纯净可查。
误区平时顾不上整理,计划在某个“黄金周”对系统做一次彻底重构,一次性解决所有坏味道。
正解代码越烂,一次性重构的风险就越大、耗时越长、回归越难排查。正确的节奏是持续小步地重构:每次改代码时顺手清理附近的坏味道,让系统时刻保持“可重构”的健康状态。就像每天睡前收拾五分钟,房间永远不会烂到需要周末大动干戈。憋大招式重构往往进行到一半就被迫中止,留下一个比原来更烂的烂摊子。
为何常见:人们倾向于把重构看成“一次工程”而非“一种日常习惯”,但无数实践表明,高频小步的重构才是可持续的。
面试题
Q1. 关于代码重构,正确的理解是:
- A.一次性推翻现有代码并重新编写
- B.为提升性能而牺牲代码可读性
- C.在保持外部行为不变的前提下改善内部结构← 正确答案
- D.重写代码以添加全新功能
答案:C。重构的核心约束是“外部行为不变”,C 准确描述了这一点。A 是重写,B 违背了可读性目标,D 是新增功能而非重构。就像整理房间不能把书桌换成跑步机,物品内容不能变,只改变摆放方式。
Q2. 下列哪一项属于典型的“代码坏味道”?
- A.重复代码← 正确答案
- B.统一的缩进风格
- C.规范的变量命名
- D.完善的测试覆盖
答案:A。重复代码是经典坏味道,同样的逻辑散落多处,改一处忘一处。B、C、D 都是良好工程习惯,分别对应整洁、可读性和质量保障,它们恰恰是消灭坏味道的手段,而不是坏味道本身。
Q3. 提取函数(Extract Function)最核心的收益是:
- A.让代码总行数变得更少
- B.彻底消灭所有局部变量
- C.让程序运行速度更快
- D.让高内聚的逻辑独立成带名字的单元← 正确答案
答案:D。提取函数的本质是把一段完整意图装进“收纳盒”并贴上标签,使逻辑独立、可复用、可测试。行数未必减少,局部变量只是被重新组织而非消灭,运行速度通常不受影响,所以 A、B、C 都是对提取函数目标的误读。
Q4. 圈复杂度公式 M = E - N + 2P 中,P 代表:
- A.函数的参数个数
- B.控制流图中的候选路径数
- C.控制流图中的连通分量数← 正确答案
- D.函数的节点总数
答案:C。P 是连通分量数,通常一个函数 P=1,此时公式简化为 M = E - N + 2。A 由参数列表决定,B 是 M 本身的含义,D 是 N 的含义。记住“P 是区域块数”就能准确选对。
Q5. 提取函数时,若被提取段需要修改某个局部变量,正确处理是:
- A.跳过提取,放弃这次重构
- B.把局部变量直接提升为全局变量
- C.在新函数内部重新定义同名变量
- D.将该变量作为返回值从新函数返回← 正确答案
答案:D。被提取段需要修改的变量,应当通过返回值“带出来”,让新函数成为一个输入输出明确的独立单元。A 过度保守,B 引入全局状态污染,C 会导致修改丢失、行为不一致。这就像把东西从房间拿走时,要明确记录“带走了什么”。
Q6. “内联变量”所针对的典型场景是:
- A.中间变量只被使用一次,且名字不比表达式更易懂← 正确答案
- B.变量名太长导致代码换行
- C.变量在多个地方被修改
- D.变量类型与预期不符
答案:A。内联变量适用于“多余中转箱”场景——变量只出现一次,直接写上表达式反而更清晰。B 是命名问题,C 是数据流复杂问题,D 是类型错误,都不是内联变量要解决的事情。
Q7. 为什么强调重构要“小步前进”?
- A.为了让 commit 提交记录看起来更短
- B.为了减少代码的总修改量
- C.为了每一步都能用测试验证,出错可快速定位回滚← 正确答案
- D.为了减少打字的工作量
答案:C。小步前进的核心价值是“可验证性”和“可回退性”。每一步都足够小,测试能精确定位是哪一步破坏了行为,失败时回滚到上一个绿点即可。A、D 关注表面工夫,B 与事实无关,小步未必减少总量,但能控制风险。
Q8. 下列哪些属于常见的代码坏味道?
- A.霰弹式修改:改一个需求要动五六个文件← 正确答案
- B.过长函数:一个函数数百行,难以阅读测试← 正确答案
- C.性能优良且注释完善的算法
- D.重复代码:同一逻辑散落多处← 正确答案
答案:ABD。霰弹式修改、过长函数、重复代码都是 Fowler 列出的经典坏味道,它们分别对应“牵一发动全身”“塞满杂物的抽屉”“多处维护易漏改”。C 是良好代码的表现,不属于坏味道。坏味道的判断标准是“是否增加后续修改成本”。
Q9. 关于安全重构,下列做法正确的有:
- A.重构前先补充测试,建立行为保护网← 正确答案
- B.一次只做一个重构动作,不把多个动作混在一起← 正确答案
- C.将新功能开发与重构放在同一次提交中
- D.重构过程中频繁运行测试,发现变红立即回退← 正确答案
答案:ABD。A 是“先拍照再整理”,B 是“一次整理一个抽屉”,D 是“随时核对清单”,三者都是安全重构的核心纪律。C 违背“一次只做一件事”的黄金法则:重构要求行为不变,新功能要求行为改变,混在一起出错时无法定位根因。
Q10. 下列哪些时机适合启动代码重构?
- A.添加新功能时发现现有结构严重阻碍新需求落地← 正确答案
- B.修复 bug 之前先理顺相关代码,便于定位根因← 正确答案
- C.暂停所有功能开发,专门安排一个月集中重写系统
- D.代码评审时发现明显坏味道,当场或随后的提交中修复← 正确答案
答案:ABD。重构的最好时机藏在日常开发中:加功能受阻时、修 bug 前、评审发现坏味道时,都是顺势整理的好机会。C 是“集中重写”而非重构节奏,违背小步高频原则,风险巨大且容易中途失败。记住:重构是每天十五分钟的习惯,不是一年一次的大扫除。