沿「软件工程观 → 团队与知识共享 → 规则与代码评审 → 测试文化 → 技术债与演进」的实践主线,掌握一个人写代码和一万个人维护同一个代码库差在哪,以及每个环节失效时该往哪修。
两种经典反模式对应两种失衡。冰激凌筒:大量端到端测试、很少单元与集成测试——常见于从原型快速转正的项目,套件慢、不可靠、难用,团队被迫不停偿还测试债务。沙漏:端到端与单元测试都多,集成测试极少——通常因为紧耦合让中间层组件难以独立实例化,只能「要么测细节、要么测全程」。沙漏比冰激凌筒好些,但大量本可在中等粒度更快捕获的失败,被推到昂贵的端到端层。两者的共同解法都是补金字塔中段:让组件可独立实例化(依赖注入、伪实现),把断言下沉到能快速反馈的层级。
预提交测试在变更合入前运行,经验法则是必须「快速且可靠」——工程师的生产力昂贵,等待与虚假失败都是浪费,所以不可靠的测试不配上预提交。提交后测试可以更慢、容忍一定不稳定,前提是有机制处理失败(回滚、定位)。两者是明确的交换关系:预提交牺牲覆盖换速度,牺牲的部分必须由提交后测试接住,并承受一定数量的回滚。只做预提交还有结构性漏洞:两个改动完全不同文件的变更,可能因共享测试而「空中碰撞」——在谷歌的规模下这种小概率事件经常发生,只有提交后全量测试能兜住。
原书用「牛 vs 宠物」类比两种变更观。宠物变更:工程师手工打造,花数天数周创建、测试、评审,了如指掌,提交时带着自豪——被拒绝容易被当成针对个人。牛群变更:LSC 自动化产生的大量独立切片,任何一片都可能因测试未捕捉的问题或合并冲突被回退或拒绝——「仅仅只是工作而已」,工具可以再生成新变更,损失几头牛不是问题。这个心智模型解释了 LSC 的文化规则:代码所有者对大范围 LSC 没有否决权(质疑按普通评审意见处理);作者不纠缠单片得失;评审者对机器生成的变更只审分配给自己的部分、不扩大范围。
原书给出依赖管理的四种理论选择,核心三种:静态依赖模型(永不改变,只允许不破坏用户的修复)是大多数新组织的正确起点,但时间足够长后必然失效——安全漏洞没有长期预警,一次升级可能被迫波及全网;SemVer + 求解器(用版本号表达兼容估计,求解器找兼容组合)是行业现状,但版本号是有损估计,网络规模一大就过度约束或过度承诺;Live at Head(总是依赖最新版本,提供方在提交前对整个生态测试变更,必要时提供自动迁移工具)把「时间与选择」从依赖管理中拿掉,成本前移到提供方,依赖 CI 与测试基础设施的存在,在开源生态中难以激励。绑定的发行版模式(如 Linux 发行版)则把网络聚合成单个依赖。
建议性弃用没有截止日期、不投入执行资源,只发警告「希望」用户迁移——SRE 的评语一针见血:希望不是一种策略。它的正当用途是宣传新系统、鼓励早期试用,且只在收益「变革性」而非增量时才有效;单纯发警告通常只能减缓旧系统的用户增长,因为用户对现有用法有惯性。强制性弃用给出移除期限,逾期系统不可用;它的最佳实践是把迁移专业知识集中到一个专家团队(通常就是负责移除旧系统的团队),该团队有动机、能沉淀工具。强制力有边界:若最后一个用户是关键基础设施,政治现实会让截止日期难以执行——所以要先发现隐藏依赖(临时停机测试、改纯实现符号名、静态分析)。
伪实现(fake)是 API 的轻量级实现:行为与真实实现类似(有状态、逻辑可推理),只是不适合生产(如内存数据库)。打桩(stub)是把行为赋给一个本身没有行为的函数:硬编码「调用就返回这个值」。取舍在于:伪实现保真度高、可跨测试复用、能表达状态变化(存了能查到),但需要有人编写并持续与真实实现同步,不是每个依赖都配得起的;打桩几乎零成本、随写随用、能模拟真实实现难以触发的返回值与错误,但无法存储状态、不保证与真实实现一致、大量使用会把实现细节泄漏进测试。原书的排序是:实际实现 > 伪实现 > 打桩 > 交互测试。
状态测试调用系统后验证「结果状态对不对」——返回值、数据库里的记录、对外可见的变化;交互测试验证「过程对不对」——某个函数是否被调用、以什么参数、几次、什么顺序。原书立场明确:状态测试优先,因为它验证真实假设(存进去的东西查得出来),而交互测试只能验证「调用发生过」,要靠额外假设才能推出系统正确。交互测试暴露实现细节,生产代码任何重构都可能让它误报——谷歌戏称之为变更检测器测试。交互测试的合法场景只有两类:无法状态验证时的兜底(真实实现太慢又没有伪实现),以及调用次数或顺序本身有业务含义(如缓存必须把数据库调用限制在一次以内)。
规则是法律:普遍可执行、不可随意忽视,违反需要「必要使用」级别的豁免审批。指南是建议:提供值得遵循、甚至高度建议遵循的最佳做法,但不像规则那样强制,留有差异空间。原书一句话概括:「如果规则是『必须』,我们的指南就是『应该』」。两者互补:规则保证底线一致性(可被工具强制),指南承载难以一刀切的最佳实践(如「不要耍小聪明」)。指南虽无强制力,但因为从观察到的真实问题中提炼,往往成为事实上的共识规范。
设计文档的价值在写代码之前:它强迫你把设计目标、实现策略和关键决策的权衡写清楚,并接受评审——相当于代码评审的前置形式。合格的设计文档有三个标志:目标可检验(发布时能对照衡量是否达成)、决策有权衡记录(为什么选 A 不选 B)、包含设计替代方案及其优缺点。原书建议把 WHO(文档的受众)和 WHAT(写作目的)放在文档的前两个段落交代清楚;对设计文档,WHY(为什么做这个设计)与 HOW 同等重要。
LSC 的流程分四阶段。授权:写明动机、影响范围与潜在问题答案,获得被重构 API 所有者的领域评审与监督委员会批准;委员会常建议把所有切片交给一名全局审批人,比逐个找本地所有者更省。变更创建:用工具生成全局变更,人力投入必须与代码库规模呈次线性关系——这是 LSC 工具的设计标准;经验法则是需要编辑 500 个以上文件时,学工具比手改划算。切片管理:把大变更拆成可独立测试、评审、提交的小块,按「测试-邮件-提交」管道流转,像对待牛群一样低成本地提交与回退。清理:用静态分析在评审时拦截已弃用对象的新用法,防止倒退。
编程视角下引入依赖几乎免费(重用优于重造),软件工程视角下则有隐藏成本:引入即签订契约——你承诺随它演进,它影响你的安全性与升级节奏。原书给出的评估清单围绕三类问题:兼容性承诺(它承诺 API 还是 ABI 兼容?支持多久?变更如何处理——不承诺兼容的库定位是实验验证场);来源与声誉(谁提供?维护是否活跃?受欢迎程度?);长期成本(自己实现有多复杂?谁执行升级?升级难度多大?多久一次重大变更?)。核心洞见是「谁将执行升级」:依赖引入时人人热情,两年后被迫因安全漏洞升级时,当初的引入者可能早已转岗,而间接依赖已经长满整个代码库。
弃用失败最常见的形态不是技术失败,而是管理失败:发了警告没人迁移,系统永远「正在下线」。原书的要点:先选对类型(建议性还是强制性,见辨析卡);警告必须可操作(告诉用户具体怎么换)且相关(在用户写代码的时刻出现,而不是合入几周后),否则会积累成警告疲劳;必须指定流程负责人——没有明确负责人的弃用不太可能取得进展,把弃用委托给用户等于退化为建议性弃用;设定增量里程碑(删除关键子组件)而不是只盯着「完全移除」这个唯一终点,否则团队士气与外部感知都会崩;工具上备齐三类:发现(代码搜索、依赖索引、日志采样)、迁移(大规模变更工具)、防止倒退(评审时标记新用法、构建白名单)。
大型测试的宿敌是慢与不稳:工程师不会等待缓慢的测试,测试越慢跑得越少、价值越低。加速的核心手段:缩小范围(把巨型测试拆成可并行的较小测试);用轮询、事件处理或订阅完成通知替代 sleep 等待——机群过载时 sleep 会连锁失败;调节内部超时并让故障模式显式(生产优雅降级返回的「无广告」对测试是模糊信号);构建层面对不变的依赖用预构建版本。可靠性从缩小 SUT 开始(密封环境隔离网络与他人流量)。可理解性同样关键:失败要有一条明确指出故障的消息、最小化定位根因的工作(分布式调用链靠请求 ID 关联,堆栈跟踪在跨进程场景没用)、提供所有者联系方式。最后必须有记录在案的所有者,否则测试会腐烂。
大型测试由四步工作流构成:获得被测系统(SUT)、喂测试数据、执行操作、验证行为。设计决策集中在三处。SUT 形式:单进程、单机、多机、共享环境或混合——封闭性(与其他组件隔离)与保真度(贴近生产)通常相互矛盾,按风险选档。数据:种子数据(预置的初始状态,复杂度比单元测试高几个数量级)加测试流量(执行中注入),来源可手工制作、复制生产或智能采样。验证:断言(显式检查)、A/B 差异(双副本对比输出,人工审差异)或手动探索。常见类型如功能测试、部署配置测试、A/B 差异(回归)测试、探针与金丝雀分析,各自是这三元的组合。
替身技术有优先级顺序,越靠前保真度越高:第一选择永远是实际实现(快速、确定、构造简单时);不行则用伪实现——API 的轻量级实现,行为与真实相似但不适合生产(如内存数据库),它有状态、可查询,是替身里的理想解;伪实现也没有时才打桩——为无行为的函数硬编码返回值,适合「把系统驱动到特定状态」,但每个打桩函数都应与断言直接相关,需要打桩很多函数就是被测系统该重构的信号;交互测试是最后手段,只验证「函数是否被以某种方式调用」,仅对有副作用的状态变更函数使用,并避免过度指定参数。
清晰测试的两个高层属性是完整(主体包含理解它所需的全部信息)与简洁(不含无关干扰)。落地有四个手法:按行为组织测试而不是按方法——方法与行为是多对多映射,一个测试只验证一个 given/when/then;用行为给测试命名——命名是失败报告里第一眼看到的信息,名字里需要「and」说明你在测多个行为;不放逻辑——测试里的运算符、循环、条件都要靠心算验证,一个字符串拼接就能藏住双斜杠缺陷;写清失败信息——好的失败消息直接给出期望结果、实际结果与相关参数,让人不打开测试就能诊断。
评审者的职责是让变更「正确、可理解、适合入库」,同时不拖慢团队。原书的操作要点:及时反馈(目标 24 小时内给出初始反馈,做不到也要先告知已看到);避免零碎响应(别让作者改一条意见后又收到一条不相关的);先问为什么(在假定作者方案有错之前,先了解其考虑);尊重作者偏好(几种方案同样有效时接受作者的选择,只在有缺陷时提替代);区分意见类型(必须处理的与仅供参考的分开标注);把格式、风格类检查交给自动化工具,人的注意力留给逻辑与设计。
作者侧决定评审质量的上限。核心是三件事:变更小而原子(一个变更只解决一个问题);描述可考古(第一行是摘要的「黄金区域」,要说明改了什么和为什么,「Bug 修复」这种描述对未来的考古者毫无帮助);心态专业(变更发出后它就不再是「你的」代码,而是团队的)。评审意见是待办项:不必无脑接受,但不能无视——不同意就说明理由并提供替代方案,请对方再看(PTAL),而不是标记已解决。
大规模变更(LSC)指逻辑上相关、但无法作为单个原子提交的一组变更——文件太多、合并冲突太频、或存储库拓扑根本不允许原子提交。当组织具备了执行 LSC 的能力(自动化生成、切片提交、全局审批),一个深刻变化发生了:过去因为「改不动」而被视为不可改变的技术决策——被广泛使用符号的命名、流行类的位置、语言版本——重新回到了可决策范围。原书数字:一个项目中两位数百分比的变更由 LSC 引起;迄今最大的 LSC 系列三天删除了超过 10 亿行代码。LSC 让「保持代码库在空间与时间上的一致性」从愿望变成可执行的流程。
语义化版本号(主版本破坏、次版本添加、补丁修复)是事实标准,但原书指出它本质是维护者对变更风险的**有损估计**,而非保证。两类失真:过度约束——破坏性变更按规则必须升主版本,但下游只用了一个没被改到的函数,实际上完全兼容,版本求解器却拒绝组合,把人推进依赖地狱;过度承诺——补丁版本被假设安全,但海勒姆定律保证可观察行为(日志格式、返回顺序、一毫秒延迟)都会被依赖,理论上安全的变更照样破坏用户。根本困境:一个变更是否破坏,只能在使用它的上下文中评估,而版本号里没有这个信息。缓解方向是 MVS(最小版本选择:选满足约束的最低版本,贴近作者测试过的组合)与用测试提供实际证据。