5.2 遗留代码:先补网再动刀


5.2 遗留代码:先补网再动刀

本节摘要:没有测试的老代码不是禁区,而是"先织网、再动刀"的施工面。本节给出完整工序:识别接缝、写特征测试把现状刻下来、在网内重构、最后用新红灯接手新需求。读完你应能独立给一个无测试的旧函数建立安全网,并安全地完成一次改造。

老代码的定义常被误解:写了五年不算老,没有测试就该按无证驾驶处理——这才是灯塔小组给老结算系统定下的规矩。规矩听起来苛刻,道理却简单:没有测试的代码你不敢改,不敢改的代码只能绕着写,绕出来的补丁越长,下次改动的成本越高。本节的织网工序,就是给"不敢改"松绑。

老代码的地形

Michael Feathers 在《修改代码的艺术》里给过一个流传很广的界定:遗留代码就是"没有测试的代码",与年龄无关。这类代码有三种典型地形,工序要按地形调整:逻辑密集区(计费、对账——最值得织网,特征测试收益最大);接线区(协议对接、配置读取——织网性价比低,交给集成层);死代码(无人引用——先删后测,删除是最高级的重构)。

动刀前先做地形勘察:找入口(谁调用它)、找出口(它改了什么世界状态)、找依赖(它伸手向哪里要数据)。勘察结果决定接缝从哪里下针。

接缝:从哪里下针

接缝(seam)是可以"在不改动此处的前提下改变其行为"的接点,也就是 3.3 那条边界线的旧代码版。老代码没有依赖注入,接缝要靠轻手术制造:

# 术前的老函数:运费计算,时间与税率都硬编码在里面 def calc_shipping_fee(order): if order.weight > 50: rate = db.query("SELECT rate FROM rates WHERE tier='HEAVY'") # 直连库 if datetime.now().month in (11, 12): rate = rate * Decimal("1.2") # 旺季上浮 return order.weight * rate return Decimal("8.00")

两个接缝清晰可见:数据库查询可以提成方法再覆写;datetime.now() 按第 5.3 节的手法抽成可注入的时钟。接缝手术只有一条纪律:改动本身不能改变行为——提取方法、参数上移,仅此而已。手术做完立刻手跑既有验证(哪怕是手工冒烟),确认没动坏。

特征测试:先刻下现状

安全网的第一层不是"正确的测试",而是特征测试(characterization test):不管现状对不对,先把现状刻下来。这步反直觉但必要——你迟早要知道"改完之后行为有没有变",而现状是唯一基线:

def test_shipping_fee_characterizes_current_behavior(): order = SimpleOrder(weight=Decimal("60")) with patch_db_rate(Decimal("0.50")), frozen_time("2026-11-15"): assert calc_shipping_fee(order) == Decimal("36.00") # 60 * 0.50 * 1.2 def test_shipping_fee_light_weight_flat_rate(): order = SimpleOrder(weight=Decimal("3")) assert calc_shipping_fee(order) == Decimal("8.00")

第一条刻下"旺季重货"的现状(含那个 1.2 的上浮),第二条刻下轻货平底价。写特征测试时会不断撞见"这结果好像不对"的瞬间——记录它,但别当场修:织网阶段修改行为等于拆自己的脚手架。把可疑行为开成清单,网织完后按正常循环逐条处理。

刻网的采样策略:分支每个走向至少一条(重货/轻货)、边界值各一条(重量恰好 50)、可疑行为一条不落(那两个"好像不对")。覆盖率不是目标,是副产品——刻完后 pytest --cov 的数字顺便告诉你哪些分支还没踩到。

织网的规模控制也值得交代:别贪大。一次织一个函数或一个类,刻完立刻网内重构,重构完再织下一块——"织网、动刀"交替推进,收益立刻兑现,动力才能维持。试图把整个模块一次性刻完再统一重构的做法,通常死于第二周:网太大、刀太久,中途任何紧急需求都会把半成品晾成新的遗留代码。

网内动刀

特征测试绿稳之后,重构窗口才开。老函数内联着三层职责(查税率、判断旺季、算钱),按 2.2 的手法清单拆:

def calc_shipping_fee(order, clock=None, rate_source=None): clock = clock or system_clock rate_source = rate_source or db_rate_source if order.weight <= 50: return FLAT_RATE rate = rate_source.heavy_tier_rate() if clock.today_month() in PEAK_MONTHS: rate = rate * PEAK_SURCHARGE return order.weight * rate

拆完特征测试仍绿——行为没变,这才有资格谈下一步。命名顺手也修了:PEAK_MONTHSPEAK_SURCHARGE 把魔法值具名化,下一任维护者不用再猜 1.2 是什么。

新需求:红灯接手

网织好,新需求"偏远地区加收附加费"按标准循环进场:

def test_remote_area_adds_surcharge(): order = SimpleOrder(weight=Decimal("60"), region="REMOTE") # 刻现状的测试保持绿,新行为由新红灯驱动 ...

注意这次和改造前的本质区别:需求现在有地方落——参数有、分支清晰、测试有基线可对照。织网的意义就在这一刻兑现:遗留代码的可改性被追平甚至反超了新代码。

⚠️ 特征测试最大的坑是"顺手修正":刻现状时发现 bug 忍不住修掉,结果基线变了,后续所有对照失去意义。修可以,先把网织完——bug 清单是网内动刀阶段的第一批红灯。

💡 织网有个隐藏红利:写特征测试是最高效的旧代码逆向工程。刻完一个模块,你对它的理解会超过读两周文档——因为每条测试都在问"这里到底想干嘛"。

织网工序五步

  • 勘察地形:逻辑密集区织网,接线区交给集成层,死代码直接删。
  • 造接缝:提取、上移,手术本身零行为变化。
  • 刻特征:分支走向加边界值加可疑行为,现状即基线。
  • 网内重构:手法清单照用,特征测试全程绿是动刀资格证。
  • 红灯接手:新需求走标准循环,可疑行为清单变成第一批红灯。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U