外观
接手祖传代码三个月:从不敢改到敢重构的安全网搭建实录
接手祖传代码,第一步不是重写
很多人第一次接手祖传代码,都会经历相似的情绪曲线:第一天觉得“这也能跑”,第一周觉得“必须重写”,第一个需求上线前又变成“千万别动”。三个月后我最大的体会是:遗留系统并不可怕,可怕的是在没有反馈机制时凭感觉修改。
所谓祖传代码,通常不是某一段代码特别烂,而是业务规则、历史兼容、隐性依赖和人员流动叠加后的结果。它可能没有文档、测试稀缺、命名失真、分支里藏着十年前的客户补丁,但它每天都在生产环境产生真实价值。新人 onboarding 的目标,也不是立刻证明自己比前任强,而是尽快建立一张“改动—影响—验证”的安全网。
重构信心不是性格特质,而是工程条件。你能多安全地修改一个模块,取决于你是否知道它的输入输出、依赖边界、关键路径、异常场景和验证方式。下面这套路径,是我在三个月里把一个不敢碰的核心模块,逐步改到可以小步替换的实践复盘。
先判断模块类型,再决定理解深度
接手初期最容易犯的错误,是从第一行代码开始顺序阅读。业务系统不是教材,调用链往往来回跳转,状态散落在数据库、缓存、配置中心和第三方接口里。线性读代码的投入产出比很低,还容易被实现细节带偏。
更有效的方法是先给模块分类:
- 资金或交易核心模块:优先补齐测试和对账,任何重构都要有数据校验。
- 流程编排模块:重点画清状态机、节点顺序和失败补偿。
- 数据转换模块:用典型输入样本锁定输出,适合先做特征测试。
- 外部适配模块:重点识别协议差异、超时重试和兼容性分支。
- 低频管理后台模块:不必一开始追求完美抽象,先保证入口和权限清晰。
分类决定策略。比如我接手的模块表面上叫“订单同步服务”,实际同时承担状态转换、第三方推送、失败重试和人工补偿四件事。若把它当成单一同步逻辑,很容易低估风险;把它拆开看,才能找到第一处安全落点。
第一个月:不要急着抽象,先建立行为基线
我的前两周没有改生产代码,主要做三件事:找入口、抓样本、问历史。
入口不只是 Controller 或定时任务,还包括消息消费者、批处理脚本、人工修复页面、失败补偿任务。很多祖传 bug 之所以诡异,是因为同一个状态可能被五个入口修改。可以用一张表记录:触发方式、请求参数、读写表、调用外部服务、成功结果、失败结果、是否可重试。
抓样本时,优先选择有代表性的真实数据:正常订单、边界金额、已取消订单、重复通知、第三方超时、历史兼容字段为空。注意不要只在本地造“干净数据”,遗留系统最稳定的部分往往是主流程,最危险的部分是异常和脏数据。
问历史也有技巧。不要问“这段代码为什么这么写”,这容易得到“以前就是这样”的回答。更好的问法是:
- 这个字段曾经兼容过哪个客户端?
- 哪种订单不能走新流程?
- 过去线上事故通常从哪个监控发现?
- 有没有人工修数脚本或临时开关还在使用?
- 这个异常吞掉是刻意的,还是当年没补完?
这些问题能把代码里没有表达的约束挖出来。所谓行为基线,就是用文档、日志和样本回答:系统现在到底做了什么,而不是我们以为它应该做什么。
方案对比:重写、封装、绞杀式重构怎么选
面对祖传代码,常见方案有三种。
全量重写听起来最痛快,实际风险最高。重写期间旧系统仍在变化,新系统很容易漏掉长期积累的边界规则;等新系统上线时,团队面对的是一次无法拆分的大爆炸切换。除非业务已停止演进、模块边界清晰、验收样本完整,否则不建议新人在 onboarding 阶段推动。
外围封装适合先止血。比如把难测的核心逻辑包在一个适配器后面,新增功能走新接口,旧接口继续保留。它的优点是短期风险低,缺点是如果不继续偿还债务,容易形成“新旧两套逻辑并存”的新混乱。
绞杀式重构是我最终采用的方式:先在外部加测试和监控,再选择一小块纯逻辑迁出,通过开关并行校验新旧结果;稳定后扩大范围,逐步让旧代码只剩很薄的兼容层。它不追求一次优雅,而是让每次变更都可验证、可回滚、可暂停。
三者的关键差异不是技术新旧,而是反馈周期。反馈越接近实时,重构胆子才越有依据。
第二个月:用特征测试给旧代码系安全带
遗留代码大多不适合一上来补细粒度单元测试,因为依赖隐藏、函数过长、副作用太多。此时更适合从“特征测试”开始:不评价设计好坏,只记录当前输入下的输出和副作用。
例如订单状态转换函数里混着大量条件,先不要拆它,而是构造一组历史订单样本,把转换后的状态、通知类型、重试次数固定下来。下面是一段简化示例,重点是用测试锁住当前行为:
python
# 特征测试:先描述“现在的行为”,而不是理想行为
# 等测试稳定后,再逐步拆分状态判断、通知、重试逻辑
def test_legacy_order_sync_characterization():
# 来自生产日志的脱敏样本:重复通知 + 第三方超时过一次
sample_order = load_fixture("orders/retry_after_timeout.json")
before = capture_side_effects()
result = legacy_order_sync(sample_order, now="2024-05-01T10:00:00Z")
after = capture_side_effects()
# 这些断言来自旧系统实际结果,先保护行为,再讨论命名和结构
assert result.status == "SYNCED"
assert result.notify_type == "ORDER_PARTNER_V2"
assert after.count_created_tasks(before) == 1
assert after.external_calls == ["queryOrder", "pushOrder"]写这种测试时要克制,不要顺手把觉得不合理的结果改成“应该如此”。如果发现行为疑似 bug,先标成待确认,用业务样本或监控占比判断:它可能确实是缺陷,也可能是某个客户依赖的历史兼容。
当纯逻辑有了保护后,再处理外部依赖。数据库、消息队列、第三方 HTTP 不必全部 mock 掉,否则测试会变成对实现细节的复制。更推荐分层:
- 纯规则测试:输入订单,输出状态和动作,速度快、覆盖边界。
- 仓储集成测试:使用独立测试库,验证 SQL、事务和唯一约束。
- 契约测试:验证第三方请求字段和响应解析,不依赖对方实时环境。
- 端到端冒烟测试:只覆盖主链路,用来确认装配没有断。
第三个月:小步重构,让每次变更都能回滚
有了基线后,重构仍要遵守“一次只改变一种属性”。一次提交里既改结构、又改业务规则、又换框架,出了问题几乎无法定位。安全节奏通常是:先改名,再搬函数;先抽纯逻辑,再隔离 IO;先并行比对,再切流;先小流量,再全量。
我处理那段同步逻辑时,先把状态判断抽成无副作用函数,旧方法内部委托给新函数;随后把第三方推送拆到端口接口后面;再通过配置开关让 1% 流量同时执行新旧两套实现,但只以旧结果为准,新结果只记录差异。
伪代码如下:
python
def sync_order(order, *, feature_flags, metrics):
# 旧逻辑仍作为兜底,避免一次性切换
old_result = legacy_sync(order)
if feature_flags.is_on("refactor.sync.parallel_compare", order.tenant_id):
try:
new_result = new_sync(order)
if not result_semantically_equal(old_result, new_result):
# 记录差异样本,不影响用户请求
metrics.emit_diff("order_sync_diff", order.id, old_result, new_result)
except Exception as exc:
metrics.emit_error("order_sync_new_error", exc)
return old_result并行比对比普通测试更接近真实流量。它能发现测试样本里没有的地区、版本、历史脏数据和特殊租户。差异连续稳定为零后,再让新结果接管小流量;一旦核心指标异常,立刻通过开关回退。
此外,重构期间要补三类可观测信息:入口量级和成功率、关键分支命中率、新旧结果差异数。没有这些指标,“看起来没问题”只是心理安慰;有了它们,才能判断代码路径是否真的被执行,异常是否被吞掉,切换是否造成数据偏移。
避坑总结:这些坑会直接消耗重构信任
1. 不要把“看不懂”误判成“可以删”
常见现象:看到某个奇怪字段、空判断或休眠逻辑,认为冗余后删除,结果特定老版本客户端立刻报错。修正方法是先查调用入口、历史提交、线上日志和监控命中率;没有证据前,用适配层隔离并加注释说明“待验证的兼容逻辑”,而不是直接删除。
2. 不要用大量 mock 伪造安全感
常见现象:测试里把数据库、消息、HTTP 全 mock 一遍,覆盖率很高,但 SQL 别名改错、事务边界丢失、第三方字段大小写变化都测不出来。修正方法是测试金字塔分层:纯逻辑多用 mock 或假对象,SQL 和协议用集成测试与契约测试,主链路保留少量真实组件冒烟。
3. 不要在一次提交里同时重构和修需求
常见现象:产品催一个小规则,顺手重命名、拆函数、改框架,上线后出问题却无法判断是需求变化还是重构引入。修正方法是把提交拆成“无行为变化”和“业务规则变化”两类;前者靠特征测试验证输出一致,后者单独评审、单独发布。
4. 不要忽略人工入口和定时任务
常见现象:主接口测试通过,结果补偿任务、后台手动重放、离线脚本仍在写旧状态,造成数据来回跳转。修正方法是建立写入口清单,所有能修改核心状态的 Job、脚本、消息消费者都纳入依赖地图,并在监控里按入口统计成功率。
5. 不要把大开关当成永久架构
常见现象:为了安全加了新旧双写和并行比对,上线稳定后开关长期保留,团队没人敢删,最后形成第三套复杂系统。修正方法是在切换计划里写明观测周期、退出条件和清理责任人;差异归零且小流量稳定后,按计划删除旧路径。
结语:重构信心来自可验证,而不是无所畏惧
三个月后再看那个模块,它并不优雅,仍有历史包袱,但我已经能清楚说明每个入口的风险、每组样本的预期、每条核心链路的监控,以及每次改动失败时如何回滚。这时的“敢重构”不是胆子变大,而是不确定性被一点点转化成了可验证的事实。
新人 onboarding 最有价值的产出,也不只是完成了几个需求,而是把口口相传的恐惧,变成团队下一次修改时可以复用的安全网。好的遗留系统治理从来不是英雄式推倒重来,而是耐心地理解行为、锁住行为、替换行为,再把验证手段留给后来的人。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。