软件开发的艺术 — 代码为读而写,债为还而计

一句话:代码被读的次数远多于被改的次数,所以先为「下一个人 5 秒看懂」而写;技术债不是脏代码,而是每次改动都要多付的利息,能量化才谈得上还。

🎯 这 20% 能解决哪些问题

你遇到的困惑本质原因马上能改的动作
看懂一段代码要 20 分钟,改它更久抽象层级混乱:一个函数同时讲策略与细节每个函数只做同一层级的事;把细节抽成命名清楚的子函数
改一个 bug 要在 5 处做同样的修改霰弹式修改:同一知识散落多处先收敛到一处(移动函数/引入参数对象),再改行为
巨型类、巨型函数不敢动缺少测试安全网,任何改动都可能出问题先补特征测试(characterization test)钉住当前行为,再重构
「重构」永远排不上期债务按美观度量,不是按利息度量算一次「每次改动多花的时间 × 人数 × 周数」,用利息说话
PR 里为命名和写法反复争论没有把规则交给工具格式化与静态检查进 CI;评审只谈语义与设计
老系统与新系统并存半年一次性重写,没有渐进替换路径绞杀者模式:按功能切片迁移,边界用防腐层隔离
「屎山」越修越乱,加了 20 层 if用条件叠加代替多态与数据驱动分支超过 3 个就考虑策略表/多态/配置驱动

一、可读性:判据是「多久能看懂」

手法反例正例
名字说出意图d = 30retention_days = 30
用早返回代替嵌套四层 if 包住主逻辑校验不通过立即 return,主逻辑留在最外层
给条件表达式起名if (a > 0 and b.status == 2 and not c.expired)if is_refundable(order)
函数只做一层抽象一个函数里既有 HTTP 处理又有金额换算编排函数调若干同级子函数
参数少于三个create(u, a, b, c, d, e)合并为 CreateOrderInput 值对象
用数据代替控制流长 switch 按类型分派查表 / 策略字典 / 多态
注释解释为什么// i++// 补偿上一版本漏算的退款手续费,2026-03 起生效
删除死代码保留「以后可能用到」的注释块交给版本控制,代码只留当前事实

5 秒判据:任意一段代码,如果新同事 5 秒内说不出「它做什么」,就需要改名、拆分或加解释;如果 5 分钟还说不清,就是设计问题,不是排版问题。


二、坏味道 → 重构动作对照表

坏味道识别信号首选手法
重复代码同一段逻辑出现在 ≥2 处提取函数/类;参数化差异部分
过长函数需要滚动才能看完连续「提取函数」,直到每层只讲一件事
过大类/过大模块字段分组互不相干提取类;按变更原因拆分
数据泥团几个参数总是一起出现引入值对象/参数对象
基本类型偏执到处传 string status给状态建类型(枚举/值对象)
发散式变化一个模块因多种原因被改按变更原因拆分模块
霰弹式修改一个需求要改多个模块合并职责到一处
特性依恋函数大量使用别类的数据把函数移到它所依恋的数据旁
过深条件嵌套布尔表达式一长串分解条件、引入守卫子句
分支膨胀switch/if 链越加越长以多态或策略表替换条件
循环收集长 for 里边遍历边拼装改为 map/filter/reduce 或显式两阶段
注释当遮羞布注释解释混乱代码而非意图用改名和提取消除「需要解释」的部分

顺序建议:先做机械、零风险的(改名、提取函数、删死代码、格式化),再动语义的(搬移职责、替换条件、拆类)。两类改动分开提交,评审才看得清。


三、安全网:怎么改才不会出事

手段什么时候用要点
特征测试老代码无测试、行为不明断言「现在输出的就是这个」,不判定对错;先钉住再动
黄金法则所有重构一次重构不改变可观察行为;改行为与改结构绝不混在同一提交
小步 + 常绿大型机械改动每步都能编译、测试通过、随时可停;宁可拆十次提交
绞杀者模式替换旧模块新实现接管一片功能,边界处转发;旧代码逐步失去入口后删除
防腐层接外部/遗留系统在自己的模型与外部模型间加转换层,阻止旧概念渗入
特性开关新旧逻辑并存上线开关只保护「切换」,不作为长期分支;过期即清理
影子对比关键计算改写新旧同时跑,只出旧结果,比对差异达标再切
静态检查 + CI 门禁团队协作把风格问题交给工具,评审聚焦设计与正确性

回归判定标准:重构前后测试集合完全一致地通过,且没有新增被禁用的用例。若为了「让它通过」而改了断言,那已经不是重构。


四、技术债:分类与计量

quadrantChart
    title "技术债四象限"
    x-axis "无意" --> "故意"
    y-axis "鲁莽" --> "谦逊"
    quadrant-1 "审慎的技术债"
    quadrant-2 "无意的债务"
    quadrant-3 "鲁莽且无意"
    quadrant-4 "鲁莽且故意"
类型例子正确处理
故意且审慎为赶大促上线,明确知道缺测试、缺幂等记账:写明代价、还债时点、临时兜底措施
故意且鲁莽「先跑通再说」,把临时方案放成永久禁止无主的临时代码;要么转正要么删除
无意且谦逊团队后来才明白的正确抽象补文档、沉淀规范、加检查项
无意且鲁莽不懂设计而写出的耦合通过重构消除,靠评审与工具防止复发

利息怎么算:

指标取法用途
改动成本同类需求近三次耗时趋势判断模块是否在恶化
缺陷逃逸率该模块线上事故数 / 变更数说明风险,不是说明代码丑
交付前置时间提交到上线的时长债务常表现为等待与排队
构建/测试时长CI 平均耗时慢反馈会放大所有其他债务
知识单点能安全修改该模块的人数人数 ≤1 就是高息债务

登记方式:债务要有条目(位置、代价、影响范围、还债动作、触发条件),并且和 TODO 标记绑定到同一个 issue 上;没有条目的债务等于没有管理。


五、争取还债时间:把「我想重构」翻译成决策

决策者关心你要给出的数字
值不值得投每次改动多花 X 小时 × 团队人数 × 每周改动次数 → 月成本
风险多大该模块近半年事故数、影响面、是否有单点知识
要多久拆成 2–3 天可见效的小块,而不是一次性两个月的大计划
怎么验收前置时间下降、构建时长下降、同类需求耗时下降、缺陷逃逸下降
会不会失控约定容量比例(常规 15–20% 用于还债与工具改进),并设红线清单

红线清单(触线即优先处理):关键路径无自动化测试、依赖已到 EOL 且无补丁、单点知识模块、无法回滚的发布、无法观测的异步链路。


六、日常落地清单

□ 写代码前先问:这段将来谁读?名字能不能表达意图?
□ 函数超过 40 行或参数超过 3 个 → 停下想想是否混了抽象层级
□ 条件分支第 4 次增加 → 换成数据驱动或多态
□ 改动任何无测试的老函数前 → 先补一条特征测试
□ 提交拆分:格式化 / 机械重构 / 语义改动 各自独立提交
□ 每个临时方案登记债务条目,写明还债触发条件
□ 每次上线后回顾:本次有没有留下「无主临时代码」

🎓 实战场景(TIL)

  • 场景:一个 500 行的下单函数,谁改谁出错。本质:编排、校验、计算、持久化四种抽象层级混在一起,且无测试。要点:先补边界用例钉住现状,再把四类拆成同级子函数,机械拆分与行为修改分两次提交。
  • 场景:同一折扣规则散在 4 个模块,改价需求做了两周。本质:霰弹式修改,规则没有单一归属。要点:把规则收敛为独立的策略对象(含测试),其余位置只调用;先做归属,再谈抽象。
  • 场景:重构计划两次被砍。本质:用「代码太乱」作为理由,没有可核对的代价。要点:换成「同类需求平均耗时从 3 天涨到 9 天,季度多花 120 人时」,并把目标拆成 2 天可见效的三块。

📋 一页速查

我要做记住这条
判断代码好不好不看行数看时间:新同事能否 5 秒说出它做什么
决定要不要拆函数函数里出现了不同抽象层级的内容就该拆
决定要不要用多态同一处 switch 第三次被修改时
安全重构的第一步先加测试,不动行为;小步、常绿、可停
提交怎么切格式化、机械重构、语义改动分三次提交
技术债的度量用利息(多花的时间与事故率),不用行数与覆盖率
争取还债时间换算成人时成本 + 分块计划 + 可验证指标
替换遗留模块绞杀者 + 防腐层 + 影子对比,绝不大爆炸重写
注释该写什么只写「为什么」与约束,代码自己说明「做什么」

🔗 相关链接