实习生最该避免的技术债务:不做测试、不写注释、不画架构图
一、深度引言与场景痛点:那段"一个月后没人看得懂的代码"是我写的
7 月初,我接手了一段"前实习生的遗留代码"。需求文档上没有,接口文档上没有,代码里的注释只有一行:# TODO: 优化。我花了三天时间逆向阅读那段 200 行的函数,才搞清楚它在做什么——一个本来可以用 30 行完成的数据聚合功能。
这段经历让我深刻反思:我自己的代码在别人眼里是不是也这样?我翻了翻自己 6 月份写的代码——测试覆盖率 5%,关键业务逻辑没有注释,模块之间的数据流关系只能靠记忆。如果一个月后的自己需要维护这段代码,大概率也看不太懂。
技术债务有一个残酷的特性:它的利息是复利。今天省 10 分钟不写测试,下次改代码时要花 30 分钟手动验证;今天不画架构图,每次新人(或未来的自己)都需要重新逆向理解;今天不写注释,3 个月后这段代码就变成了"考古现场"。本文总结了实习生最容易积累的三类技术债务,以及如何在日常开发中主动避免。
二、底层机制与原理深度剖析:技术债务的复利模型
技术债务的本质是"将当前开发成本转移为未来维护成本"。这个转移是有利息的——今天的 1 小时债务,一个月后可能需要 3 小时来偿还。
利息产生的机制有三种:
遗忘利息。你今天写的代码逻辑,你的大脑中有上下文。一个月后,这个上下文已经模糊了,你需要重新加载。测试用例是"可执行的需求文档"——它不需要你加载上下文,直接告诉你这段代码应该怎么工作。写了测试,就冻结了这份理解,不随时间衰减。
复杂度利息。当你没有架构图时,你对代码的理解是基于"当前的记忆"的。当代码量从 1000 行增长到 10000 行时,你无法在脑中容纳所有模块的关系。此时每一次改动都可能触发意想不到的连锁反应——因为你不知道"改这个 Service 会影响哪些 Controller"。
沟通利息。当设计决策分散在聊天记录里而不是文档里时,每次有人需要理解决策背景,都需要重新沟通。而且聊天记录是不可索引的、非结构化的,查找成本随沟通次数线性增长。
三、生产级代码实现与最佳实践:技术债务预防工具包
""" 技术债务预防工具 设计理念:把"不产生债务"变成默认行为 通过自动化检查,让"遗漏测试/注释/文档"在提交前被拦截 """ from dataclasses import dataclass from typing import List, Dict, Optional from enum import Enum class DebtType(Enum): """债务类型分类""" TEST_MISSING = "test_missing" # 缺少测试 COMMENT_INSUFFICIENT = "comment_insufficient" # 注释不足 ARCH_DOC_MISSING = "arch_doc_missing" # 缺少架构文档 NAMING_POOR = "naming_poor" # 命名不规范 @dataclass class CodeReviewCheck: """代码审查检查项 —— 每条检查项都有对应的处理策略""" check_name: str debt_type: DebtType threshold: float # 阈值(如:测试覆盖率低于 70% 触发警告) severity: str # 严重程度:blocker / warning / info auto_fix_hint: str # 自动修复提示 class DebtPreventionChecker: """ 技术债务预防检查器 在 PR 提交前运行,自动发现潜在的技术债务 """ def __init__(self): self.checks: List[CodeReviewCheck] = [ CodeReviewCheck( check_name="新代码测试覆盖率", debt_type=DebtType.TEST_MISSING, threshold=0.7, severity="blocker", auto_fix_hint="为新函数添加单元测试,至少覆盖正常路径和主要边界", ), CodeReviewCheck( check_name="公开方法文档注释", debt_type=DebtType.COMMENT_INSUFFICIENT, threshold=1.0, severity="warning", auto_fix_hint="为每个 public 方法添加 docstring,注明参数含义和返回值", ), CodeReviewCheck( check_name="模块结构文档", debt_type=DebtType.ARCH_DOC_MISSING, threshold=0.0, severity="info", auto_fix_hint="新建包或模块时,同步更新 README 或架构文档", ), CodeReviewCheck( check_name="变量命名规范", debt_type=DebtType.NAMING_POOR, threshold=0.0, severity="warning", auto_fix_hint="避免单字母变量名(循环变量除外),使用语义化命名", ), ] def check_pr(self, changes: Dict) -> List[Dict]: """ 检查一次提交是否存在技术债务 返回检查结果列表 """ results = [] for check in self.checks: # 生产环境中,这里的数值来自实际的代码分析工具 # 如:pytest-cov 的覆盖率、pylint 的评分等 result = { "检查项": check.check_name, "状态": "通过", # 生产环境根据实际数据判断 "建议": check.auto_fix_hint, } results.append(result) return results # 实习生代码交付清单 INTERN_DELIVERY_CHECKLIST = [ { "类别": "测试", "检查项": [ "核心业务逻辑的单元测试覆盖率 ≥ 70%", "至少包含 3 个边界场景的测试用例", "PR 描述中贴出测试通过截图", ], }, { "类别": "文档", "检查项": [ "每个 public 方法包含 docstring", "复杂逻辑有行内注释,说明'为什么'而非'做什么'", "新接口的请求/响应示例写入接口文档", "模块级别的 README 包含模块职责和依赖关系", ], }, { "类别": "设计", "检查项": [ "新增/修改模块的架构图(哪怕是手绘稿的拍照)", "技术方案选型的原因记录在 PR 描述中(trade-off 说明)", "关键的数据流或状态变更,在 PR 中附流程图", ], }, ] # 每行代码都是写给"三个月后的自己"看的 CODING_PRINCIPLES = [ "变量名能从语义上判断用途,不需要注释来解释变量名本身", "函数做到'输入-处理-输出'的单一职责,不要有副作用", "关键决策用注释解释 WHY(为什么这样设计),而不是 WHAT(这段代码做什么)", "如果你觉得一段逻辑需要注释才能看懂,先考虑重构让代码自解释", ]这个预防工具包的设计理念是:把"不积累债务"从"自律行为"变成"流程约束"。就像 CI/CD 流水线会在编译失败时阻止合并一样,债务预防器会在测试缺失或文档不足时提醒你修复。
四、边界分析与架构权衡:什么时候可以接受技术债务
不是所有的技术债务都是坏的。有策略地积累技术债务,在特定场景下是合理的。
可以接受的场景:
- 原型验证阶段——快速验证一个想法是否可行,测试和文档可以后补
- 一次性脚本——只会执行一次的数据迁移脚本,写完就能删
- 明确标注了 TODO 且计划在一个 Sprint 内偿还的债务
绝对不能接受的场景:
- 核心业务逻辑没有测试——这是生产事故的直接原因
- 对外接口没有文档——调用方无法正确使用,错误成本指数级放大
- 架构图的缺失——当模块数超过 5 个时,无图=不可维护
判断标准:债务的偿还成本是否随时间指数增长?如果是,就不能欠。测试的偿还成本不会随时间指数增长(你今天和一个月后写测试花费的时间差不多)。但架构图的缺失——一个月后你需要重新逆向理解系统,三个月后你可能需要重读全部代码——这类债务的利息是复利。
五、总结
技术债务在实习生阶段有一个特别危险的特性:你往往意识不到你在积累债务。当 Leader 对你说"先做功能,测试后面补",你以为这是合理的优先级排序。但当"后面补"变成了"永远不会补",你留下的是一个无人愿意维护的代码黑洞。
避免技术债务不需要完美主义——不需要每个函数都有 100% 覆盖率的测试,不需要为每个变量写注释。需要的是三个最低限度的习惯:核心逻辑有测试、关键决策有注释、模块关系有图。这不是"做得更好",而是"做得及格"。
转正答辩时,面试官可能会看你的代码仓库。一个测试覆盖率 80%、文档齐全的仓库,和一个"功能能跑就提交"的仓库,传递出的工程素养是天壤之别的。