重构:何时重构与如何安全重构
一、什么是重构
重构(Refactoring)是在不改变软件可观察行为的前提下,改进其内部结构的活动。关键词:行为不变——用户、调用方、测试看到的结果与之前一致;只改结构——命名、拆分、移动、去重复,让代码更易读、易改、易扩展。因此重构既不是「修 bug」也不是「加功能」,而是专门为「把代码收拾好」留出的动作;做完重构再加功能,往往事半功倍。
二、坏味道与重构时机
坏味道(Code Smell)是代码里「可能有问题」的信号,不一定错,但往往提示该看看能不能重构。典型坏味道包括:过长函数/过大类、重复代码、神秘命名、过长参数列、发散式修改(改一个需求要动很多类)、依恋情结(函数更关心别的类的数据 than 自己的)、数据泥团(总是一起出现的一坨数据)等。发现坏味道不一定要立刻动手,但在「要在这里加功能」或「这里已经改不动了」时,优先做一轮重构再继续。
重构时机:触手可及的是「三次法则」——第一次写就那样、第二次重复时留意、第三次重复时一定要抽;或者「加功能前」——要改的这块若很难下手,先小步重构再加;或者专门留「重构时间」在迭代里,避免技术债只增不减。
三、小步重构与测试保护
重构最怕改坏:行为一变就成 bug。所以小步进行、每步都可验证。每次只做一种、范围小的改动(例如只重命名一个变量、只提取一个方法),改完立刻跑测试或手工验证;通过再下一步。这样若出错能立刻定位到「哪一步」出的问题。测试是重构的安全网:有自动化测试(单元测试、回归测试)时,重构后跑一遍即可确认行为未变;没有测试时,至少要有可重复的验证方式(如主流程走一遍),否则大动干戈容易引入隐性缺陷。
四、常见重构手法:提取方法、重命名、移动
手法很多,这里挑最常用、最安全的几种。
calculateOrderTax(items),原处调用它。data → orderItems,proc() → validateAndSaveOrder()。ShippingService。提取前
function processOrder(o) { // ... 校验 let tax = 0; for (const i of o.items) tax += i.price * i.qty * 0.1; o.tax = tax; // ... 保存 }提取后
function processOrder(o) { validateOrder(o); o.tax = calculateOrderTax(o.items); saveOrder(o); } function calculateOrderTax(items) { return items.reduce((s, i) => s + i.price * i.qty * 0.1, 0); }五、重构与新功能的平衡
重构不是「有空再做」的附加项,而是「要在这里加功能时,若结构太差就先收拾一下」的必选项。但也要避免两种极端:只加功能不重构,债越堆越多;没完没了重构,需求迟迟不交付。建议:在动到某块代码时顺带做局部重构(如加一个需求时顺便提取方法、重命名),而不是动辄「全盘重写」;把「重构」写进任务或 DoD(Definition of Done),每个迭代留一点时间还技术债;大范围重构要有测试或分阶段、可回滚,避免一次改太多导致难以收尾。
一句话: 重构是在行为不变的前提下改善结构。通过坏味道发现改进点,在加功能前或重复出现时动手;用小步 + 测试保证安全。常用手法有提取方法、重命名、移动。重构与加功能要平衡:动到哪块就顺带收拾哪块,避免只堆功能或只搞大重构。
六、小结
重构是不改行为只改结构的活动。坏味道(过长函数、重复、神秘命名等)提示改进时机;在加功能前或「改不动」时优先重构。小步重构 + 测试保证安全;常用手法有提取方法、重命名、移动。重构与需求要平衡,动到哪块顺带收拾,并留时间还技术债。下一章讲单元测试与 TDD,把「测试保护」和红-绿-重构循环说细。