第 20 章 代码质量与协作
副题:轨道、把关、账本——1 个人写得动,8 个人改得动
"每道防御都要有人维护,谁来维护?"结果就是需要新成员,团队从 3 人涨到 8 人(阶段 4 编制:2 前端/3 后端/1 测试/1 运维/1 产品)。但人来了,问题也来了:合并冲突、互相踩代码、没人敢重构——一个人写得动,三个人改不动。本章回答:协作的质量怎么守住——Git 工作流(轨道)、Code Review(把关)、技术债管理(账本)。
分工地图(最后一站):16 性能管"快"、17 交易管"对"、18 测试管"稳"、19 安全管"防"、本章(20)管"久"——前面四件事(快/对/稳/防)都是"一次做对",本章管"长期不改坏":质量是每一天的协作(第 19 章说"安全是持续过程",质量也是)。
本章要建立的认知
- 协作需要基础设施:轨道(Git 工作流)、把关(Code Review)、账本(技术债)——三件套让多人协作"有轨可循";
- 流程是团队的函数:1 人不需要 CR,8 人必须——流程的严与松跟着团队规模走;
- 技术债是快交付的利息:积累是必然,偿还靠机制(重构+测试兜底)。
20.1 三个人改不动
阶段 4 的第一周,三起协作事故。
团队从 3 人扩到 8 人——新招 2 前端、3 后端、1 测试、1 运维、1 产品。防线有人维护了,但人带来的新问题比解决的问题还先到:新人要熟悉代码,大家要同时改同一份代码,发布要协调。代码质量的问题从"写得好不好"变成"协作得好不好"。
事故一:合并冲突,互相踩。
新来的同事和原作者同时改 contentService.js——新同事加"内容举报",原作者修"列表排序"的 bug。合并时冲突了,git 标出两边改动。新同事只认识自己写的举报逻辑,把同事那段不认识的排序改动删了——线上排序又乱了。
覆盖的根源是不认识。 解决冲突的规则:不认识对方改动时,先问清楚,不要按"我认识的留下"猜。猜错的代价,测试都不一定拦得住。
事故二:没人敢重构。
代码库里有一段"能跑但很乱"的代码。新同事提议重构,被大家否决:"万一改坏了怎么办?"
没有测试兜底,谁都不敢动。 代码从"我的"变成"我们的",责任从"个人记得"变成"机制保证"。
事故三:发布互相踩。
8 个人还靠"谁改完谁部署"。两个同事同一天上线,后一个把前一个覆盖了——先上线的功能"消失",用户报障。
这次事故后,开发者在周会上说:"我们得有个发布流程。" 这句话就是发布流程的起点(第 21-23 章:构建/CI/CD/部署)。本章先解决代码怎么协作,第 21-23 章解决代码怎么上线。
临时手段先撑着:发布清单(改了什么、影响哪些接口、回滚方案)和发布窗口(每天下午 4 点前合入,4 点后不发布)。先使用代价较低的过渡方案,这时不必投资过多。
三起事故的共同点是协作质量,不是代码质量。代码各自写都没问题,合起来就出问题。
优先级: 事故一最痛(代码直接错),事故二最隐蔽(质量慢慢烂),事故三最急(用户直接受害)。本章先解决一和二(Git 工作流 + 评审 + 技术债),三交给第 21-23 章。
20.2 协作的基础设施:轨道、把关、账本
之前说过,仔细不是解决方案,机制才是——协作也一样。协作章要做的事只有一件:把前面章节的"质量约定"变成团队制度。制度落在三件套上:
轨道:Git 工作流。 代码的"路"——分支怎么开、怎么合、谁有合入权。没有轨道,8 个人在同一份代码上乱改(事故一);有轨道,各干各的,合入走统一入口。轨道解决"同时改"的问题。
把关:Code Review。 代码合入前的检查——另一个人读你的代码,确认没改坏、没漏测、没埋雷(第 18/19 章的检查单在这里上岗)。没有把关,问题合入后靠线上发现(第 19 章的教训:攻击者悄悄地来,你最后才知道);有把关,问题在合入前被拦下。把关解决"改坏了没人发现"的问题。
账本:技术债管理。 代码质量不是一次到位的——为了赶发布,有些代码写得急(事故二的残骸),这些"欠的账"要有记录、有偿还计划。没有账本,债越积越多(第 4 章说过"需求分析欠的债,最后都在代码质量上还");有账本,债是可见的、可还的。账本解决"质量怎么下滑的没人知道"的问题。
三件套的关系一句话:轨道管"代码怎么流动",把关管"流动时怎么检查",账本管"检查不过关的债怎么还"。 协作的质量,是这三件事的日常。
而"日常"是最容易被忽略的部分。 三件套不是建完就完的——轨道要维护(分支命名、合流节奏),把关要执行(每次 PR 都看,不是"重要的才看"),账本要更新(每次妥协都记,不是"想起来才记")。
协作基础设施的价值在持续使用,不在建设完成。 就像缓存上线后要看命中率,流程建完后要看执行率——建完是起点,日常是全部。
三件套之外,还有第四块基石:知识传递。 新人怎么知道"为什么这么设计"(决策记录)、"这段代码为什么这样写"(commit message、PR 描述、注释)?代码会流动,知识要跟着流动——评审时的教学、文档、决策记录,决定下次迭代"能不能改"(20.5 的评审教学是它的主通道)。
20.3 Git 工作流:代码的轨道
Git 是版本管理工具,协作要的是**"代码怎么流动"的约定**,光"能存版本"不够——这就是工作流。先看 Git 的机制,它是所有工作流的地基。
Git 的本质用一句话说,就是分布式版本管理:每个开发者的本地都有一份完整的历史。改动先提交到本地,再推送到共享仓库(远程)。协作的入口是共享仓库的分支:每个人在自己的分支上改,改完合并回主分支。
先建立心智模型,不急着记命令——命令跟着心智走。提交(commit)就是一份快照加一段说明:快照记录"这一刻代码长什么样",说明(commit message)告诉未来的人"这一刻为什么长这样"。分支是一条独立的时间线:从主时间线的某个点分出去,各走各的,最后合并回来。分支是 Git 让"多人同时改"成为可能的关键——没有分支,8 个人只能排队。合并则是把两条时间线接回一条:大多数合并是自动的,因为两边改的地方不重叠;只有改到同一处时才需要人来决定,这就是冲突。
工作流用三个手段解决"同时改"的问题。第一是分支隔离:每个人在独立分支上干活,互不干扰。事故一中"同时改同一个文件"的混乱,由此变成"各自分支各自改"——冲突不再散落在代码里,而是集中在合并那一刻解决。第二是小步合入:分支不要活太久,一天到几天,改完就合并。分支活得越久,两边各自攒的改动越多,合并时的冲突就越大;小步合入把"大冲突"拆成了"小冲突"。第三是统一入口:合并不是"谁想合就合",要走 PR(Pull Request):推送分支、发起合并请求、有人评审、通过才合。入口处的把关,就是 20.5 的 Code Review。
接着看冲突。git 合并两个分支时,如果同一段代码被两边改了,它不知道该留哪边,就停下来等人解决。所以冲突是一个信号:两个人都改了同一处。解决冲突有一条铁律:不确定对方改了什么,先问,别猜——覆盖对方的改动是冲突事故最常见的形态。这也呼应第 19 章的原则:按攻击面修,不按事故现场修。但更值钱的是预防:改之前先同步,拉最新主干再开分支,这是第 15 章"先同步再干活"的 Git 版;改的时候大声说,在团队频道声明"我在改 contentService,你们别动"——协调是同步的,Git 是异步的,先同步,后异步;还要小步提交,改一点提交一点,冲突发生时好定位。一句话总结:冲突的预防靠的是"少重叠",不是"会解决"——重叠少,冲突就少,踩代码就少。
分支名和提交信息,本质上是命名纪律在 Git 上的两个延伸:**名字和说明都要说出意图,因为它们是将来检索的索引。**分支命名的格式是 feature/举报功能、fix/列表排序——分支名就是 PR 的标题、评审看到的第一行,也是将来在 git 历史里查"这个功能从哪来的"的索引。提交信息同理:commit message 是写给未来的人看的,将来用 git blame 排障时,第一个看的就是它。"修复列表排序"比 "fix" 有用一百倍,写不清的提交,等于给未来留了一个谜。PR 描述也是这个纪律:PR 是"这次改动为什么存在"的文档——改了什么、为什么改、怎么测的,评审人和三个月后的自己都靠它理解这次改动。这也对应 20.5 评审的"教学心态":描述是教学的第一页。
合并有两种方式,一句定位就够。merge 保留两条时间线的合并记录,历史完整,但会留下"合并节点",适合记录"这条功能从哪来的"。rebase 把分支的提交"重新接到"主分支顶端,历史线性干净,但改写了提交历史,所以多人共用的分支不要 rebase。团队约定一种即可,本章的 trunk-based 用 merge,简单。为这两种方式吵架的团队不少,其实选哪种都行——不选才是问题。不选工作流,等于默认接受混乱。
最后是回滚。Git 让"回到上一个版本"成为一条命令:git revert 生成一个"撤销这次改动"的新提交,而不是抹掉历史。之前提到的"先回滚",在代码层就是这条命令;部署时,会把它变成发布流程的一部分。
20.4 工作流选型:git flow vs trunk-based
工作流不是只有一种。主流模型有两个,选型就照老规矩,应用决策模型。
先看两个候选。git flow 是多分支模型:master、develop、feature,外加 release 和 hotfix,发布节奏慢的项目常用,适合"版本发布制"——固定版本号、发版窗口。要理解它,先看懂它的分支矩阵:master 是正式发布的历史,develop 是开发主线,feature 从 develop 分出,release 是发版分支——版本号在这里定稿,只修 bug、不添功能,hotfix 则是线上紧急修复的通道,从 master 分出。五类分支,对应的正是"固定版本号 + 发版窗口"的产品节奏:每个版本一个 release 窗口,hotfix 走紧急通道。所以分支模型映射的是发布节奏——节奏变了,模型就得跟着变。另一个候选是 trunk-based(主干开发):所有人在主干或短命分支上干活,频繁合并,发布节奏快的项目常用,适合"持续发布制"——随时可发布。
决策模型。① 业务目标:多人协作不踩代码、发布不互相覆盖、质量有把关。② 业务约束:阶段 4 团队 8 人,产品迭代快,周更甚至双周更。③ 技术问题:用哪种工作流?④ 候选:A,git flow,多分支严谨;B,trunk-based,主干快速。⑤ 评价维度:冲突成本——分支多是不是意味着冲突多?发布节奏匹配——版本制还是持续制?学习成本——8 个人的团队要人人都会。⑥ 打分:git flow 的五类分支矩阵,对 8 人团队是过度设计——release 和 hotfix 分支是为"版本发布制"服务的,而案例是持续迭代,根本没有"发版窗口"。trunk-based 的分支模型简单,主分支加短命特性分支,和发布节奏匹配,冲突靠"小步合入"控制。打分的过程,其实就是"先想清楚自己的发布节奏"的过程——没想清楚节奏就选工作流,等于没看路就开车。⑦ 选择:B,trunk-based 简化版——主分支、特性分支、PR 合入,分支短命,一到两天。⑧ 收益:冲突可控,因为分支短命;发布随时可做,因为主干总是可发布状态;8 人团队学习成本低。⑨ 代价与演进:代价是主干必须"绿"——测试永远通过,测试是工作流的保障;主干红了是第一优先级的修复,在红着的主干上继续开分支,等于在坏地基上盖楼。演进条件也记下:如果将来做"版本发布制"的对外产品,有固定版本号了,再评估 git flow 的 release 分支。
这里展开一句"主干绿",因为它是第 22 章 CI 的前提。trunk-based 能成立,靠的是"主干随时可发布"。怎么保证?先靠纪律——"主干红了先修"是团队的第一条纪律,刚才第 ⑨ 步说过;再靠机器——第 22 章的 CI,推送自动跑测试,红了不能合,"主干绿"就从人保证升级到机器保证,这就是 CI 的登场理由。这也是本书"人保证→机制保证"规律的又一次实例:第 18 章,测试从手工到自动化;本章,主干绿从纪律到 CI——人总会忘,机制不会。
照例,决策记录又添一行:工作流等于 trunk-based 简化版,演进条件是版本发布制出现。
trunk-based 的"主干总是可发布",是第 21-23 章发布流水线的前提:构建从主干出制品,CI 保证主干绿,部署把制品送到生产。可见工作流选型决定了发布体系长什么样。
20.5 Code Review:质量的第一道闸
评审是质量的第一道闸——现在,这道闸上岗了。Code Review 把"检查"变成了制度,流程是:
写代码 → 自测(本地跑测试,第 18 章)→ 推分支 → 发 PR → 评审人看 → 提意见/通过 → 合并
评审看什么?第一张来自第 12 章:分层有没有被破坏——表现层有没有直连数据库;中间件顺序对不对(第 12 章说过,排列顺序是每次评审要看的第一行)——鉴权在路由前吗?第二张来自第 18 章:测试覆盖了吗——新功能有测试吗,改 bug 补测试了吗?第 18 章说"测试是评审的检查单",这是第一项。第三张来自第 19 章:安全表过一遍——新输入点校验了吗,SQL 参数化了吗,挂鉴权了吗?第 19 章的四问,逐条对。
三张单之外,评审还有两块"软检查",它们决定代码好不好改。一块是可读性:名字起得清不清楚,变量名、函数名说出意图了吗?还有一条更直觉的标准:一段代码 30 秒能不能看懂?看不懂,说明该拆函数了。另一块是边界处理:输入为空、超长、异常时怎么办——这是第 4 章验收标准的反向追问:正常路径写了吗?边界路径呢?硬检查拦事故,软检查保可维护,两者合起来才是代码质量的日常感知。
评审用哪种心态,决定它是有用还是走过场。一种是找茬心态:把评审当考试,专挑毛病,结果是作者防御、评审对抗,最后评审变成形式。另一种是教学心态:把评审当"两个人一起把代码改好"——作者说明意图,评审提建议,新人从评审里学。评审是团队知识传递的主通道,新人培训靠"命名清楚+评审把关",评审就是新人培训的第一课。说到底,评审的价值不在抓 bug,而在让代码被第二个人理解——能讲清楚的代码,才有人敢改,这正是事故二"没人敢重构"的解法之一。
流程能不能持续,取决于评审的节奏和氛围,这里要诚实面对成本。PR 要小:一个 PR 只装一个功能或一个修复,大 PR 评审人看不进去,只能走过场。评审要快:当天评审完,拖几天的评审等于没有评审——作者等不及,就自己合了。意见要对事不对人:说"这里缺测试",不说"你总是忘测试"。流程死在"太慢"或"太凶"上,都是常见死法,所以节奏和氛围是流程能不能持续的关键。
评审和测试是什么关系?评审发现的问题,能写成测试的,就写成测试。"这里没处理空值",就补一条空值测试。评审问不出问题,说明测试覆盖不全——反过来,评审提出的每个问题,都是测试的一个候选用例。所以它们是同一件事的两只手:测试问"行为对吗",评审问"还有没有没测的行为"。测试是评审的检查单,评审反过来喂测试新用例,双向的。
评审的问题数,是评审质量的度量指标之一。另有一个软指标:合入后线上 bug 的密度。评审拦住的 bug 不会到线上,所以线上的 bug 就是评审漏的——评审的合格标准,是线上的问题在减少。机器能查的先让机器查,把低级问题过滤掉,评审的人力才能留给机器查不了的东西:逻辑、边界、设计。lint 就是评审的前置过滤。
20.6 技术债:快交付的利息
代码质量最诚实的部分,是技术债。定义用一句话:为了快交付,暂时放弃的"本该做对"的部分。它像贷款——借的时候痛快,利息每天都在还,而利息就是后续改动的额外成本。
先说清楚一件事:债的积累是必然的,不是失误。每个项目都有"这周先这样,下周再改"的时刻——发布压力来自第 4 章的三周 MVP,需求变化来自第 4 章说的返工和蔓延,新人加入则是对旧代码不熟就动了手。债是常态,关键是"债有没有被看见"。
前面章节埋的债,一共四种形态。需求债来自第 4 章:需求没分析清楚就开发,返工的代码、蔓延的功能,都是它——第 4 章的原话是"需求分析欠的债,最后都在代码质量上还"。优化债来自第 16 章:为了性能引入的复杂度,缓存层、索引——"优化是负债管理,还不起的债就是技术债"。安全债来自第 19 章:为了体验后置的防御,比如验证码和双因素,窗口期就是安全债在收利息。复杂度债来自第 17 章:交易系统的机制复杂度。但要注意区分——业务逼出来的复杂度是良性债,不欠不行;而没有注释、没有测试的复杂,是恶性债,欠得没道理。
依赖是代码的"外债":升级依赖——安全补丁、版本跟进——是这笔债的利息;弃坑的库,是收不回的坏账。依赖管理有三个基本动作:依赖清单要审,因为第 19 章说过依赖也是攻击面,是供应链攻击的入口;升级要测,第 18 章的测试是升级的安全带;锁版本,它是环境一致性的第二道保险,在协作语境里就是"所有人用同一份依赖"。依赖债是技术债里最容易被忽略的一种:它不写在代码里,写在 package.json 里。
还债要先把口号变成动作,机制分三步。第一步是看见:债要有记录,用 TODO 清单或技术债看板——债没被记录的,等于不存在的债。记录形态不用复杂,一行一个债,写清什么时候欠的、欠在哪、利息多高。第二步是偿还,手段是重构——重构的定义是行为不变、结构变。敢做重构靠测试兜底:先补测试,再重构,测试在这里变成重构的胆量来源。偿还有两个节奏:路过就还——每次改到这段代码时顺手把旁边的烂代码整理一下;以及定期做一次专门的还债迭代,第 27 章的迭代循环里会给它留位置。第三步是止损:识别恶性债——没有测试的复杂、没有注释的魔法数字,账本要标出哪些债在收高利息。利息最高的债先还。
利息是改动成本,重构是还债的执行细节,也是测试兜底的展开,分三步。先补测试:重构对象的现有行为先测住,没有就先补核心路径的。然后小步重构:一次只做一个小变换,改个名字、拆个函数、换个结构,不做"顺手优化"。每步跑测试,绿了才走下一步,测试在这里是重构的安全带。注意重构的节奏和写代码相反:写代码追求快,重构追求慢——慢,意味着每步可验证。
技术债有一笔隐性成本,落在新人身上。"这段代码我不敢动,怕改坏"——这是事故二在 8 人团队里的普遍形态。而新人不敢动,老代码就更没人维护,债就更重,恶性循环。解法还是那两件事:测试兜底,让人敢动;评审教学,让人会动。
技术债还和两份旧记录有关。一份是第 5 章的决策记录:它记"当时为什么选",技术债记"当时为什么没做对"——一个是选择的历史,一个是妥协的历史,两份合起来,就是代码的完整病历,未来迭代时翻病历做诊断。另一份是第 2 章的演进循环:它说"业务复杂度上升,原方案无法承受,新技术出现"。所以债的积累是演进的前奏,不是失败,关键是"被看见、有计划";恶性债才是失败。
最后把良性和恶性再区分一遍,防止"零容忍"的极端。良性债,是明知且记录的妥协——"这版先硬编码,下版接配置",有记录、有偿还计划,是健康的决策记录形态。恶性债,是不知且无记录的混乱——"这段代码谁写的?不知道",无主、无记录、无测试。所以管理技术债的目标是消灭恶性债:良性债是决策;恶性债是失明。账本的全部目的,是让债从恶性变成良性:看不见的债才可怕,看见的债可以排队还。
20.7 流程的边界:流程是团队的函数
流程的多少,由团队的规模、吃过的事故这些因素决定——规模变了、事故变了,流程就该跟着变,就像 f(x) 里 x 变了输出跟着变一样。
先看规模这头的两个极端。1 人团队,对应阶段 1 到 2,不需要 PR——自己合自己看,没有意义;也不需要评审,安全审查是评审流程的一部分,但 1 人时,审查就是自己过一遍表。所以 1 人团队的正确流程,是最少的流程,流程也是复杂度。再看 2-3 人团队,阶段 3 末:PR 可以开始上了,两个人互相看,"先看代码后联调"在两人之间开始有意义。但评审可以是轻量的,口头加 PR 评论,不设专门评审人。流程的爬坡,从"第二个人加入"开始。
到 8 人团队,流程要具体落地了,原则是"事故痛什么先建什么"。第一步,PR 加评审,解决事故一和事故二——互相踩、没人敢改——第一周就上。第二步,测试进 PR,PR 里测试必须过,评审的第二张单就此上岗。第三步,CI,推送自动跑测试,"主干绿"从人保证升级到机器保证。第四步,部署流程,第 21 到 23 章的构建、制品、发布,解决事故三。流程是跟着事故和规模长出来的——这是演进循环的团队版。
还有一个现实挑战要交代一句:团队可能不在一个办公室。远程协作里,异步沟通——PR 评审、文档、提交信息——比同步沟通更依赖"写得清楚"。commit message、PR 描述、评审意见的可读性,在远程团队里是硬通货:提交纪律、评审教学,在远程不是加分项,是必需品。
代价也要诚实交代。流程有成本:PR 等待,评审慢就是合入慢;还有流程仪式,每个改动都要走一遍流程。所以要有取舍。
那什么时候加流程、什么时候减?看两个信号。加的信号:事故重复发生——同一个坑踩两次,说明缺流程。减的信号:流程变成纯仪式——PR 没人看就合、评审走过场。仪式化的流程比没有更糟,因为它给了虚假的安全感。合格的流程标准是它能拦住事故。拦不住的事故,是流程的缺口;拦住了但没人知道为什么,是流程的仪式化。
合格标准不能躺在文档里,要变成日常动作,这就是流程的体检:每季度对一遍流程清单——哪些流程还在拦事故,留着;哪些流程没人用了,删掉或简化;哪些事故还没有流程拦,新增。为什么要每季度?因为流程和代码一样会腐化:人走了、工具换了、规模变了,流程跟不上就变成仪式。流程的维护是协作的日常一部分。
20.8 本章对应表
| 业务诉求 | 技术选择 | 为什么 | 代价/取舍 |
|---|---|---|---|
| 多人同时改代码互相踩 | Git 工作流(分支+PR 合入) | 分支隔离+统一入口,冲突集中解决 | 分支要短命,否则冲突更大 |
| 工作流选哪种 | 九步决策(trunk-based 简化版) | 发布节奏匹配+8 人学习成本低 | 主干必须"绿"(测试保障) |
| 改坏了没人发现 | Code Review(三张单) | 合入前把关,问题不流到线上 | 评审节奏(慢=流程死) |
| 没人敢重构 | 测试兜底 + 评审教学 | 第 18 章测试=重构胆量;评审=知识传递 | 补测试要时间 |
| 质量下滑没人知道 | 技术债账本(看见/偿还/止损) | 债被记录才可还,利息高的先还 | 还债要排期 |
| 流程多严 | 团队规模匹配(流程是团队的函数) | 1 人最少流程,8 人跟着事故长 | 流程成本 vs 事故成本 |
本章小结
第 16-20 章收官:性能管"快"(16)、交易管"对"(17)、测试管"稳"(18)、安全管"防"(19)、质量管"久"(20)——业务增长期的五件事全部讲完:从"首页打不开"到"三个人改不动",这五章回答的是"业务变大之后,系统怎么还能又快又对又稳又安全又能改"——第 21 章开始,回答"怎么把这些东西送到用户面前"。
-
协作需要基础设施:轨道(Git)、把关(CR)、账本(技术债)——"大家小心一点"不是解法;
-
流程是团队的函数:跟着事故长、跟着规模调——仪式化的流程比没有更糟;
-
技术债是快交付的利息:看见、偿还、止损——第 18 章的测试是重构的胆量来源;
-
阶段 4 开篇:1 个人写得动,8 个人改得动——下一站,代码从"开发机能跑"到"生产可部署"。
下一章
协作轨道建好了:分支、评审、账本——代码在团队里流动有了规矩。但下一个问题出现了:代码在"开发机"上能跑,不等于在"生产环境"能部署——本地跑得好好的,到了服务器上为什么起不来?
第 21 章,构建与制品:开发环境到生产环境的转换——打包、依赖、产物——代码写好了,怎么让它"可部署"。
写到这里,第 16-20 章全部完成——全书进度:第 1-3 章地图、第 4-7 章设计、第 8-15 章走通请求、第 16-20 章业务增长期的快对稳防久——第 21-23 章开始:把设计、走通、守住的这一切,送到用户面前。