你有没有过这样的体验:租房住了一年后,抽屉里塞满数据线、衣柜里叠着换季被子、书桌上摊着三个快递箱——东西一样没少,但你就是找不到钥匙。代码也是这样。功能都在、测试能过,可每改一个需求都要翻遍五个文件,这就是“代码坏味道”在作祟。代码重构(Refactoring)就像一次彻底的房间整理:不扔东西(外部行为不变)、不换房子(不重写系统),只是把物品重新归位(改善内部结构),让每件东西待在它该待的地方,下次找起来又快又准。本文将从“整理房间”的视角,带你看懂 Martin Fowler 的经典重构手法:如何通过坏味道判断哪里该收拾,如何用提取函数、内联变量、搬移特性三步整理法把大而乱的函数拆成清爽的小模块,并借助“测试保护网”像拍照清单一样确保整理过程不弄丢任何东西。最后你会发现,重构不是一次性的周末大扫除,而是每天随手整理十五分钟的好习惯。

岗位关联

对 AI 从业者而言,重构能力决定了你能否在快速迭代中守住代码质量底线——无论是训练脚本、推理服务还是数据管道,会重构的人才能让代码越改越清晰、越跑越稳。

目录
  1. 可视化演示
  2. 重构本质:整理房间,不换家
  3. 坏味道识别:房间里那些不对劲的信号
  4. 提取函数:把杂物装进收纳盒
  5. 内联与搬移:扔掉中转箱,物品归其位
  6. 测试保护网:整理前的照片与清单
  7. 重构节奏:每天十五分钟胜过周末大扫除
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

重构本质:整理房间,不换家

房间乱了,摆在你面前的无非两个选择:搬新家,或者动手收拾。搬新家听着痛快,但要重新刷墙、置办家具、研究新小区的快递柜在哪儿,还可能搬家途中弄丢几件重要物件。收拾房间则完全不同——家具还在原位、东西一件不丢,只是把随处乱放的物品重新归类,让整个空间焕然一新。代码重构就是后一种:它不动摇系统的“地基”,不改变程序的“对外表现”,只在内部把结构理得更顺,让后来的人一眼就能看懂、一改就能下手。

“外部表现”在代码世界里就是程序的行为:同样的输入,重构前和重构后必须给出同样的输出。回想一下,你给房间拍照时,光线、角度、家具的轮廓都没变,变的只是内部每个抽屉的摆放逻辑。这正是重构与重写最本质的分水岭——重写是“拆了盖新的”,重构是“微创整理”。

在工程上,重构被定义为:在不改变软件可观察行为的前提下,通过一系列小步骤改善代码内部结构。Martin Fowler 在他的著作中强调,重构不是一次性的剧烈运动,而是大量细碎动作的累积,每个动作小到几乎不可能出错。小到什么程度?小到每一步之后都可以立刻运行测试确认没有破坏任何东西。

所以,重构的底气来自“等价性”。哪怕只是把一个表达式提出来变成变量,也必须保证前后行为毫无变化。有了这把标尺,你整理的每一步都有据可依:整理不是凭感觉乱摆,而是按照“等价”这条准绳精确归位。

∀x∈Domain:Pafter(x)=Pbefore(x)\forall x \in \text{Domain}:\quad P_{\text{after}}(x) = P_{\text{before}}(x)
这个公式用数学语言宣示了重构的“铁律”:对于程序合法输入集合中的任意输入 x,重构后的程序 P_after 与重构前的程序 P_before 产生同样的输出。这是“整理不丢东西”的严格承诺。
  1. 全称量词$\forall x$ 表示“对任意一个输入 x”,这就像整理房间时承诺“房间里任何一件物品都不能丢”,而不是“大部分物品别丢就行”。
  2. 合法输入集合$x \in \text{Domain}$ 指明 x 来自程序的合法输入范围。Domain 是“活动区域”,测试用例都从这块区域里取样,证明了这片区域安全,才敢说重构成功。
  3. 行为等价$P_{\text{after}}(x) = P_{\text{before}}(x)$ 是整句话的核心,它要求重构前后的程序在相同输入下产出相同结果。这就是“物品清单对得上号”的数学表达。

坏味道识别:房间里那些不对劲的信号

判断房间要不要收拾,不必等它变成垃圾场。有些信号早就出现了:袜子出现在厨房水槽边、剪刀消失在一个月前的快递盒里、想找支笔要翻遍三个抽屉——这些细节都是房间的“坏味道”,它们不致命,却在悄悄耗尽你的时间。代码世界同样如此。代码坏味道(Code Smell)是代码中疑似存在深层问题的表面迹象,它不会让程序立刻崩溃,但会让每一次改动都变得沉重、危险。

最常见的坏味道有四种。重复代码:同一段业务逻辑在多处各写各的,改一处忘一处,就像两把一模一样的钥匙丢在家里的不同角落,出门前总担心其中一把是不是丢在了昨天地铁上。过长函数:一个函数动辄几百行,开头读得明白,看到结尾已经忘了开头,如同一只塞满杂物的抽屉,要找一根数据线必须把整抽屉倒出来。过长参数列表:函数签名排着七八个参数,调用者要小心翼翼对齐每一个位置,仿佛出门要带五个包,哪个装什么全靠记忆。霰弹式修改:改一个需求要同时跑遍五六个文件修修补补,就像为了挪一张餐桌,得先把客厅、餐厅所有的摆设全部挪动一遍。

这些坏味道有一个共同的“体检指标”——圈复杂度(Cyclomatic Complexity)。它统计函数中独立执行路径的条数:每条 if、for、while 分支都会多出一条路。路径越多,函数越难读、越容易埋 bug、越难写测试。把这个指标量化出来,就是经典的圈复杂度公式:

坏味道就是房间向你发出的求救信号,圈复杂度则是测量乱象的“凌乱指数”。学会识别这些信号,你才知道该从哪里开始动手,而不是对着整间屋子发呆。

M=E−N+2PM = E - N + 2P
圈复杂度 M 衡量一段代码中“独立路径”的条数,也可以理解为“至少需要多少个测试用例才能覆盖所有分支”。它是房间凌乱指数的数字化:M 超过 10 就该警惕,超过 20 基本宣告这段代码已经不可维护。
  1. 边数 E$E$ 是控制流图中的边数,也就是程序可能走的“流转方向”。一个 if 的两个分支就是两条边,如同房间里的两条通道。
  2. 节点数 N$N$ 是控制流图中的节点数,每个语句块或判断条件是一个节点,如同房间里的每个“落脚点”。
  3. 连通分量 P$P$ 是图中互相连通的区域数。通常一个函数自成一体,$P = 1$,此时公式简化为 $M = E - N + 2$。
  4. 圈复杂度 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)
这个公式描述了提取函数前后代码结构的变化:原本四个步骤按顺序堆在一起,现在把中间两步装进新函数 G,外层函数依然按原顺序执行,只是中间变成“调用 G”。顺序没变,行为就没变。
  1. 语句序列 Seq$\mathrm{Seq}(s_1, s_2, s_3, s_4)$ 表示函数体按顺序执行四段代码,如同整理前桌上依次堆着四堆杂物,顺序不能乱。
  2. 提取边界$G(s_2, s_3)$ 把第二、三堆杂物圈进同一个新盒子,这是“收纳盒”的边界划定——相关的东西放一起,不相关的留在外面。
  3. 执行顺序守恒$F'$ 依然是先 $s_1$、再调用 $G$、最后 $s_4$,执行顺序完全没有改变,这是“东西一件没少、也没挪错位”的数学保证。
  4. 调用关系$G(...)$ 出现在 $F'$ 内部,意味着新函数 G 就像一只贴好标签的收纳盒,主函数通过“调用”这个动作随时取用里面的东西。

内联与搬移:扔掉中转箱,物品归其位

收纳盒用久了,也会出现两种尴尬。第一种:家里堆了一排空盒子,明明可以把东西直接放到最终位置,偏要经手一层“中转箱”。第二种:厨房的烘焙工具躺在卧室衣柜里,每次要用都得从卧室搬到厨房,用完再搬回去。内联(Inline)解决第一种尴尬,搬移(Move)解决第二种。

内联是提取函数的逆操作。当一个函数的函数体比函数名更直观,或者一个中间变量的名字不比它代表的表达式更有意义时,这一层“包装”就纯属多余。举个例子,const price = base * 0.8 如果只被用到一次,而 base * 0.8 又一看就懂,那还不如直接把表达式写到使用处,把变量名删掉。内联函数同理:调用方读起来完全不比函数体费解,那就拆掉这层壳,让逻辑“裸露”在阳光下。

再说搬移。判断依据很简单:特性 f 被谁使用得最多,就应该住在谁家。函数调用次数不会说谎——一个函数被类 B 引用十次、被类 A 引用两次,那它就属于 B,应该搬家。

提取是“汇聚”,内联是“简化”,搬移是“归位”。这三招配合起来,就能把整理工作覆盖到房间的每一个角落。收放之间,代码结构越来越贴近真实世界的“高内聚、低耦合”。

K(x)=F(G(x))⟹K′(x)=H(x)K(x) = F\bigl(G(x)\bigr) \quad\Longrightarrow\quad K'(x) = H(x)
这是搬移特性(Move Function)的决策公式:当特性 f 在类 B 中被使用的次数多于在类 A 中时,就把 f 从 A 搬到 B。就像牙刷应该住在卫生间而不是卧室抽屉里——哪里最常用,就放在哪里。
  1. 使用次数$\text{Uses}(f, B)$ 统计 f 在类 B 中被引用的次数,这是“谁更依赖它”的量化证据。
  2. 比较关系$>$ 表示 B 对 f 的依赖强于 A,搬家方向由此确定,不靠感觉、靠数据。
  3. 搬移动作$\text{Move}(f, A \to B)$ 是执行动作,从模块 A 迁移到模块 B,同时更新所有引用点,并保证测试依然全绿。

测试保护网:整理前的照片与清单

整理房间最怕什么?怕收完发现东西丢了。聪明的做法是:动手前先给每个角落拍照片、列物品清单;每收拾完一个区域就对着照片清点一次。有了这套对照系统,哪怕不小心弄乱,也能立刻发现并对症找回。代码重构的“照片”就是测试。重构圈子里有一条铁律:没有测试就动手重构,等于在悬崖边跳舞。

具体的舞蹈节奏叫“红绿重构循环”(Red-Green-Refactor)。第一步“红”:先写一个会失败的测试,确认新行为确实缺失;第二步“绿”:用最直接的办法让测试通过,不管代码美不美,先保证行为正确;第三步“重构”:在测试持续为绿的前提下,把刚写出来的代码整理干净。整个过程像过马路看红绿灯——红灯停(测试失败)、绿灯行(测试通过)、黄灯整理(重构优化)。

那这层保护网织得够不够密,怎么度量?看覆盖率。

测试就是那张“物品清单”,让你有底气说“我整理过,东西一件没少”。测试全绿,重构才能小步快跑、随时收手;测试如果飘红,说明刚才那步动坏了什么,立刻回滚到上一个绿点即可。保护网越密,重构的手越敢下。

C=executed_linestotal_lines×100%C = \frac{\text{executed\_lines}}{\text{total\_lines}} \times 100\%
覆盖率 C 等于测试真正执行到的代码行数除以代码总行数。它回答的问题是:你的“物品清单”涵盖了房间里的多少东西?覆盖率越高,整理时越有底气。
  1. 被覆盖行数$\text{executed\_lines}$ 是测试执行时真正跑到的代码行数,相当于清单上已经逐项核对过钩的物品。
  2. 总行数$\text{total\_lines}$ 是被测模块的全部代码行数,等于房间里所有物品的总数。
  3. 覆盖率$C$ 是两者之比乘以 100%,得到 0 到 100 之间的百分比。60% 的覆盖率意味着清单上还有四成物品没核对,别急着大动干戈。
  4. 实践参考关键路径的覆盖率应尽量逼近 100%,但普通代码不必盲目追求 100%——你不会为了一粒灰尘给整个房子套上防尘罩。

重构节奏:每天十五分钟胜过周末大扫除

同样是整理房间,有人一年只大扫除一次,累到虚脱、期间还容易弄丢东西;有人每天睡前随手收拾五分钟,房间永远保持八十分以上的整洁。重构的哲学是后者。与其攒出一座“代码屎山”再发起总攻,不如把整理变成日常呼吸的一部分。代码质量不是靠一次“重构周”救回来的,而是靠无数个微小的好习惯堆出来的。

三个纪律决定了节奏是否健康。时机判断:不用专门留出“重构月”,最好的时机就藏在日常里——添加新功能时发现现有结构碍手碍脚,修 bug 时发现相关代码绕成一团,代码评审时发现明显的坏味道,都是顺手重构的黄金时刻。一次一处:一次只做一个重构动作,做一步、测一步、交一步。像整理房间时一次只收拾一个抽屉,绝不把整个衣柜都倒出来。频繁提交:小步提交让每一次改动都有记录、可回溯,出问题秒回退,避免了“改了一整天最后不知道哪里错了”的绝望。

把这些微小的改进叠加起来,你会发现一个惊喜——复利效应。质量不是线性增长,而是指数增长:

整理房间的秘诀从来不是“大扫除”,而是“物归原位 + 每日微调”。重构也一样:小步、频繁、有测试守护,让代码库像滚雪球一样越滚越干净。你今天花十五分钟提取的那个函数,可能为三个月后的自己省下一个下午的排查时间。

Q(t)=Q0×(1+r)tQ(t) = Q_0 \times (1 + r)^{t}
这是“持续改进复利公式”:起始质量 $Q_0$ 乘以每次改进的增益 $(1+r)$ 的 $t$ 次方。今天顺手做的每个小重构,都会在明天、后天的代码维护中持续产生收益。
  1. 起始质量$Q_0$ 是起点时刻代码库的可维护程度,可能是 60 分也可能是 20 分,不重要,重要的是开始。
  2. 改进率$r$ 是每次重构带来的质量提升比例。它不必大,3% 已经很好——关键是“每次都有”。
  3. 迭代次数$t$ 是重构发生次数的统计,可以是天数、提交数或评审轮数。$t$ 越大,复利效果越惊人。
  4. 复利结果$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 演示

python76 行
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()
目的:这个脚本像一个“房间乱象扫描仪”,用 Python 内置的 AST 模块解析源码,自动识别过长函数、过长参数列表和高圈复杂度三种常见坏味道。
  1. class SmellDetector(ast.NodeVisitor):继承 AST 访问者类,让代码自动“逛”遍每一个语法节点,如同带着清单逐间检查房间。
  2. def visit_FunctionDef(self, node):访问者回调方法,碰到任意函数定义就停下来执行检查,是扫描仪的核心触点。
  3. lines = node.end_lineno - node.lineno用函数结束行号减开始行号算出函数体长度,对应“房间面积”测量。
  4. cc = self._cyclomatic_complexity(node)调用自定义方法计算圈复杂度,相当于给每个函数测“凌乱指数”。
  5. elif isinstance(n, ast.BoolOp):识别 and/or 逻辑运算符,每个运算符都会增加一条独立路径,让圈复杂度上升。
  6. tree = ast.parse(SAMPLE)把源码字符串解析成 AST 抽象语法树,就像给房间画一张结构图,之后所有测量都在结构图上进行。
  7. detector.visit(tree)启动全屋巡检,所有坏味道被逐一记录进 smells 列表,最终打印成“整改建议书”。

常见误区

误区看到一段代码乱得离谱,第一反应是“这没法救了,全部重写吧”。觉得重构太麻烦,重写更干脆利落,还能用上最新技术栈。

正解重构是保持行为不变的结构调整,重写则是抛弃旧代码另起炉灶。重写意味着丢失旧系统里积累的无数边界处理逻辑——那些历史 bug 修复留下的“补丁”往往是最宝贵的资产。就像你嫌衣柜乱,把整个衣柜扔了买新的,结果新衣柜里找不到那条女朋友送的围巾。重构的风险在过程中可控,重写的风险在交付前不可见。

为何常见:重写的诱惑在于“从零开始”的掌控感,而重构要求你面对真实存在的混乱并一小步一小步地啃,前者更符合人对“干净”的想象。

误区“这个改动很小,就是挪一下位置,不用写测试了吧?”许多开发者在没有任何测试保护的情况下直接开始重构,还自信地认为凭细心就能避免问题。

正解重构的本质是“保持行为不变”,但没有测试,你拿什么证明行为没变?人类对复杂系统的记忆和判断力都极其有限,哪怕移动三行代码也可能改变变量作用域。测试保护网的意义在于把“我觉得没问题”变成“测试证明没问题”。没有测试的重构,就像闭着眼睛整理衣柜,还声称“我肯定没弄乱”。

为何常见:写测试需要额外时间,而重构又常常发生在时间紧迫的时候。人总是高估自己的专注力,低估代码的隐蔽关联。

误区学完提取函数后兴奋不已,见 5 行代码就提一个函数,最后整个文件变成了几十个短小但毫无关联的函数片段,阅读时要在函数之间反复跳转。

正解提取的目的是“高内聚、有意义的单元”,不是为了切碎。一个函数该不该提取,取决于它是否表达了一个完整独立的意图。比如“计算折扣”值得提取,“把 total 加一”就不值得。过度的提取,就像把房间用无数个小盒子塞满,表面上整洁,实际找东西要翻几十个盒子——比原来更累。

为何常见:初学者容易把“短函数”等同于“好代码”,忽略了“命名”和“语义边界”才是提取的关键。功能的碎片化会带来新的理解成本。

误区“反正这段代码重构后也要改,不如把新需求一起做了,省得改两遍。”于是重构混合着需求变更,一次提交里既有结构变动又有新行为。

正解一次只做一件事是重构的黄金法则。重构要保证行为不变,加功能要改变行为,两者混在一起,一旦测试失败,你无法判断是重构引入的回归还是新功能的 bug。这就像整理房间时还顺带买了几件新家具,最后发现房间乱了,分不清是挪旧的弄乱的还是新家具摆错了。

为何常见:人天然想节省往返成本,但工程上“可定位性”比“省一趟”重要得多。小步提交让每一次差异都纯净可查。

误区平时顾不上整理,计划在某个“黄金周”对系统做一次彻底重构,一次性解决所有坏味道。

正解代码越烂,一次性重构的风险就越大、耗时越长、回归越难排查。正确的节奏是持续小步地重构:每次改代码时顺手清理附近的坏味道,让系统时刻保持“可重构”的健康状态。就像每天睡前收拾五分钟,房间永远不会烂到需要周末大动干戈。憋大招式重构往往进行到一半就被迫中止,留下一个比原来更烂的烂摊子。

为何常见:人们倾向于把重构看成“一次工程”而非“一种日常习惯”,但无数实践表明,高频小步的重构才是可持续的。

面试题

Q1. 关于代码重构,正确的理解是:

单选题 · easy

  • A.一次性推翻现有代码并重新编写
  • B.为提升性能而牺牲代码可读性
  • C.在保持外部行为不变的前提下改善内部结构← 正确答案
  • D.重写代码以添加全新功能

答案:C。重构的核心约束是“外部行为不变”,C 准确描述了这一点。A 是重写,B 违背了可读性目标,D 是新增功能而非重构。就像整理房间不能把书桌换成跑步机,物品内容不能变,只改变摆放方式。

Q2. 下列哪一项属于典型的“代码坏味道”?

单选题 · easy

  • A.重复代码← 正确答案
  • B.统一的缩进风格
  • C.规范的变量命名
  • D.完善的测试覆盖

答案:A。重复代码是经典坏味道,同样的逻辑散落多处,改一处忘一处。B、C、D 都是良好工程习惯,分别对应整洁、可读性和质量保障,它们恰恰是消灭坏味道的手段,而不是坏味道本身。

Q3. 提取函数(Extract Function)最核心的收益是:

单选题 · easy

  • A.让代码总行数变得更少
  • B.彻底消灭所有局部变量
  • C.让程序运行速度更快
  • D.让高内聚的逻辑独立成带名字的单元← 正确答案

答案:D。提取函数的本质是把一段完整意图装进“收纳盒”并贴上标签,使逻辑独立、可复用、可测试。行数未必减少,局部变量只是被重新组织而非消灭,运行速度通常不受影响,所以 A、B、C 都是对提取函数目标的误读。

Q4. 圈复杂度公式 M = E - N + 2P 中,P 代表:

单选题 · mid

  • A.函数的参数个数
  • B.控制流图中的候选路径数
  • C.控制流图中的连通分量数← 正确答案
  • D.函数的节点总数

答案:C。P 是连通分量数,通常一个函数 P=1,此时公式简化为 M = E - N + 2。A 由参数列表决定,B 是 M 本身的含义,D 是 N 的含义。记住“P 是区域块数”就能准确选对。

Q5. 提取函数时,若被提取段需要修改某个局部变量,正确处理是:

单选题 · mid

  • A.跳过提取,放弃这次重构
  • B.把局部变量直接提升为全局变量
  • C.在新函数内部重新定义同名变量
  • D.将该变量作为返回值从新函数返回← 正确答案

答案:D。被提取段需要修改的变量,应当通过返回值“带出来”,让新函数成为一个输入输出明确的独立单元。A 过度保守,B 引入全局状态污染,C 会导致修改丢失、行为不一致。这就像把东西从房间拿走时,要明确记录“带走了什么”。

Q6. “内联变量”所针对的典型场景是:

单选题 · mid

  • A.中间变量只被使用一次,且名字不比表达式更易懂← 正确答案
  • B.变量名太长导致代码换行
  • C.变量在多个地方被修改
  • D.变量类型与预期不符

答案:A。内联变量适用于“多余中转箱”场景——变量只出现一次,直接写上表达式反而更清晰。B 是命名问题,C 是数据流复杂问题,D 是类型错误,都不是内联变量要解决的事情。

Q7. 为什么强调重构要“小步前进”?

单选题 · mid

  • A.为了让 commit 提交记录看起来更短
  • B.为了减少代码的总修改量
  • C.为了每一步都能用测试验证,出错可快速定位回滚← 正确答案
  • D.为了减少打字的工作量

答案:C。小步前进的核心价值是“可验证性”和“可回退性”。每一步都足够小,测试能精确定位是哪一步破坏了行为,失败时回滚到上一个绿点即可。A、D 关注表面工夫,B 与事实无关,小步未必减少总量,但能控制风险。

Q8. 下列哪些属于常见的代码坏味道?

多选题 · hard

  • A.霰弹式修改:改一个需求要动五六个文件← 正确答案
  • B.过长函数:一个函数数百行,难以阅读测试← 正确答案
  • C.性能优良且注释完善的算法
  • D.重复代码:同一逻辑散落多处← 正确答案

答案:ABD。霰弹式修改、过长函数、重复代码都是 Fowler 列出的经典坏味道,它们分别对应“牵一发动全身”“塞满杂物的抽屉”“多处维护易漏改”。C 是良好代码的表现,不属于坏味道。坏味道的判断标准是“是否增加后续修改成本”。

Q9. 关于安全重构,下列做法正确的有:

多选题 · mid

  • A.重构前先补充测试,建立行为保护网← 正确答案
  • B.一次只做一个重构动作,不把多个动作混在一起← 正确答案
  • C.将新功能开发与重构放在同一次提交中
  • D.重构过程中频繁运行测试,发现变红立即回退← 正确答案

答案:ABD。A 是“先拍照再整理”,B 是“一次整理一个抽屉”,D 是“随时核对清单”,三者都是安全重构的核心纪律。C 违背“一次只做一件事”的黄金法则:重构要求行为不变,新功能要求行为改变,混在一起出错时无法定位根因。

Q10. 下列哪些时机适合启动代码重构?

多选题 · extreme

  • A.添加新功能时发现现有结构严重阻碍新需求落地← 正确答案
  • B.修复 bug 之前先理顺相关代码,便于定位根因← 正确答案
  • C.暂停所有功能开发,专门安排一个月集中重写系统
  • D.代码评审时发现明显坏味道,当场或随后的提交中修复← 正确答案

答案:ABD。重构的最好时机藏在日常开发中:加功能受阻时、修 bug 前、评审发现坏味道时,都是顺势整理的好机会。C 是“集中重写”而非重构节奏,违背小步高频原则,风险巨大且容易中途失败。记住:重构是每天十五分钟的习惯,不是一年一次的大扫除。