t3code 是我给代码治理立的一个规矩三层体系从规范到架构一次说清代码越写越多之后团队成员平均每天看的代码量也在涨可真正能沉下心维护代码的时间反而越来越少。我立了一个叫 t3code 的项目不是为了发明新框架也不是为了搞一套花哨的工程流程而是想搞清楚一个问题在不动架构大手术、不推翻存量代码的前提下怎么让一个中大型项目的代码质量肉眼可见地变好并且这个变好是可以持续、可度量、团队愿意跟着做的。t3code 的 T3指的是 Tier 1、Tier 2、Tier 3 三层治理第一层管可读性第二层管可维护性第三层管可扩展性。这三层不是并列关系而是递进关系——先让代码长得规整再让代码变得容易改最后让代码的边界稳定到不敢乱动也没必要乱动。这篇文章把我从立项到落地、从踩坑到调整的完整过程整理出来适合正在带团队做代码治理、或者自己维护一个模块总觉得哪里不对又说不上来的朋友参考。1. t3code 的项目拆解三层治理到底在治什么1.1 项目背景与核心需求最开始触发我做这件事是因为接手了一个上了年纪的微服务仓库。这个仓库单模块代码量已经接近十万行CI 时长还算能忍受但每次发版前总有几个文件雷打不动地出现改了一行牵连一片的连锁反应。我统计过一个月内的 Pull Request 评论高频词排在前几位的是缩进乱了方法太长了这层不该依赖那层能不能拆开。说白了问题不在某个人的编码水平而在整个代码库缺少一层约束机制大家凭自觉写代码没形成可执行的统一标尺。做 t3code 之前我列了几个核心需求第一约束必须能自动化执行不能靠 Code Review 时人工提醒因为人总会漏第二约束必须有优先级不可能一口气解决所有历史问题要把最影响开发效率的几类先按住第三约束要有弹性存量代码和新代码要有不同的治理力度否则团队会把大量时间花在纯格式化而非真正的设计改进上。1.2 T3 体系的整体设计思路三层设计其实来源于一个很朴素的类比。盖房子的时候装修标准是一回事墙体承重是另一回事整个地块的规划红线又是另一回事。你在装修层做得再好天花板和楼板之间如果结构上就不合理后面迟早要砸墙重来。代码也一样格式化工具解决的是墙面平整度静态检查解决的是承重墙有没有乱开洞架构约束解决的是整个地块的规划是否符合预期。所以 t3code 的三层分别对应Tier 1格式与编码风格落地工具是 Prettier、EditorConfig、ESLint 的规则子集把空格、分号、引号、命名风格这类长得好不好看的问题先解决掉。Tier 2复杂度与坏味道治理落地工具是 ESLint 的复杂度规则、SonarQube 静态扫描、圈复杂度统计脚本把好不好改的问题摆上台面。Tier 3依赖方向与模块边界治理落地工具是依赖矩阵检查脚本、ArchUnitJava 项目或 eslint-plugin-import 的 import/no-restricted-paths前端项目把能不能长期演进的问题制度化。很多团队做代码治理喜欢把这三层混在一起上一个 SonarQube 就想着把所有存量问题都清零结果存量告警上万条CI 直接跑不过去最后只能关掉规则。t3code 花时间最多的其实不是选工具而是把三层目标的边界划清楚每一层只解决自己那一层的事不允许跨层把问题混在一起算账。2. Tier 1 实操让格式化成为不用过脑子的肌肉记忆2.1 规范文件与自动化落地Tier 1 的第一个动作不是写规则而是先在项目根目录放一套 EditorConfig 和统一的格式化配置。我见过很多团队直接把 Prettier 装进 devDependencies 就完事结果开发者的 IDE 没有安装对应插件保存时依然按各自习惯格式化导致 diff 里永远混着大量无关的空白符变动。我在 t3code 里的做法是三步走。第一步在 .editorconfig 里固定 charset、缩进风格、缩进宽度、换行符尽量用 insert_final_newline 这类纯文本级约束让每个文件的开头结尾保持一致。第二步统一 Prettier 核心参数包括 printWidth 设置为 100、semi 设置为 true、singleQuote 设置为 truetrailingComma 设置为 all。这一步的价值在于格式问题从人可以争论的品味问题变成了工具统一输出的客观结果。第三步接入 husky 和 lint-staged在 pre-commit 钩子里对暂存文件跑一次格式化确保只有干净代码能进入 Git 历史。这里有个值得注意的细节为什么是 lint-staged 而不是全量 prettier --write因为大仓库全量格式化一次会产生巨大的 diff不但把 Git 历史搞得一团糟还会让 Code Review 失去焦点。用 lint-staged 只处理本次提交涉及的文件既能逐步覆盖所有文件又不产生无效噪音。2.2 提交信息与 PR 约束Tier 1 不止包括代码格式我把提交信息的规范也归到了这一层。t3code 采用的方案是 Commitlint 搭配 Conventional Commits 规范。commit 类型限定在 feat、fix、refactor、docs、style、test、chore 这七类subject 用祈使句且不超过 72 个字符。刚开始有同事觉得这类约束是小题大做但跑过一个月后最大的收益其实是自动生成 CHANGELOG 和自动关联需求编号变得非常顺滑。以前查这个改动是哪个需求引入的要翻半天日志现在 grep commit message 就能定位。还有一个小技巧是把 PR 标题与 commit 类型绑定比如 PR 标题里的[FEAT]或[FIX]前缀可以在 CI 里用脚本校验与第一个 commit 类型一致避免团队把一堆乱七八糟的改动塞进一个 PR。Tier 1 的落地节奏是头两周只推格式化统一不动任何业务代码逻辑。我当时的判断依据是格式化是争议最小、收益最直接的改动先让全组人对什么是规范的代码建立共同语言再往下一层走才不会被抵触。3. Tier 2 实操复杂度治理不是删代码是拆认知负担3.1 静态扫描配置与阈值设计Tier 2 的核心关注点是圈复杂度和函数长度。我现在习惯把圈复杂度规则 max-statements 设为 40max-depth 设为 4max-params 设为 5。这些数值不是拍脑袋定的而是参考了认知负荷研究的基本结论——人类工作记忆能同时处理的独立信息单元大约在四到七之间。当函数的判断分支层数超过四层阅读者就需要在工作记忆里维护多个并行状态很容易漏掉某条边界路径。SonarQube 在这里起到的是汇总和看板作用。我建了一个质量门禁要求新增代码的圈复杂度不得超过 15新建函数的语句数不得超过 60 行覆盖率的门槛暂不提因为覆盖率需要业务测试配合不能靠简单的门槛一刀切。关键是把存量问题和增量问题分开存量问题在 SonarQube 里只记录不阻塞增量的复杂度超标直接红灯。3.2 复杂度治理的实操路径我在 t3code 里总结了一套拆函数的模板遇到一个超过 60 行的函数先不急着重构按以下步骤处理画出函数内部的逻辑分支图用注释标出主要分段。找出至少两个没有共享局部变量的分段提取成独立函数。提取后的新函数命名要能解释业务意图比如 parseRequestBody 而不是 doProcess。如果找不到干净的分割点多半是数据耦合太重这时候可以引入参数对象把一组相关的参数封装成一个对象减少参数个数。最后跑一次全量测试确认行为等价。有一个很容易被忽略的小细节拆分函数之后为了让 Git blame 更清晰拆分动作单独作为一个 commit不要和业务改动混在一起。我在实践里拆过一个 300 多行的订单状态机函数拆出 7 个小函数之后不仅 Code Review 变快了后面修 bug 时定位问题的速度也明显提升。因为每个函数只负责一个判断阶段报错堆栈会直接指向具体阶段处理逻辑不用再从头到尾读一遍 300 行。Tier 2 最让我意外的是复杂度治理的隐性收益在交接时体现得最明显。新同事上手模块时面对一堆小函数的阅读理解成本远比面对一个巨大的上帝函数低虽然总代码行数变多了但单位信息密度和可掌握性都强得多。4. Tier 3 实操用依赖约束守住架构的规划红线4.1 架构约束的落地方式Tier 1 和 Tier 2 解决的是代码长得好不好、好不好改Tier 3 解决的是模块之间的依赖方向对不对。这一层最容易被赶工期的团队跳过但恰恰是它决定了一个项目能否在三年后依然可以持续交付。t3code 在 Node.js 和 Java 两个技术栈分别做了落地。前端这边用的是 eslint-plugin-import 的 import/no-restricted-paths把 src 分成 app应用层、components通用组件层、features业务功能模块层、shared基础共享层。允许依赖方向是 app - features - components - shared反过来则直接报错。还额外限制了不能跨 features 相互引用比如 features/order 不能 import features/user 的组件必须通过 shared 或 app 层再分发。Java 侧用的是 ArchUnit 写架构测试。我会在测试目录里建一个 ArchitectureTest 类用 JUnit 5 ArchUnit 断言 specific dependency 规则controller 不能直接依赖 mapper 或 repository 接口以外的具体实现domain 层不能反向依赖 infrastructure 层的任何类。这些规则跑在 CI 的 test 阶段一旦有人写出反向依赖构建直接失败RT反转测试的简称会立刻给出违规调用链。有人觉得这种约束太刚性说我这次就是临时跨层拿一个数据后面会重构。我的经验是所谓临时绕行最后 90% 都会留在代码里成为永久路径。因为你欠下的技术债不会自己消失它只会等一个更忙的时刻再爆发。所以 t3code 的原则是架构约束不允许任何人开白名单真有绕不过去的场景必须在 PR 描述里写清原因由至少两个人评审签名才能临时豁免。4.2 依赖方向与模块边界治理Tier 3 的另一个重点是依赖方向的可视化。我用脚本在 nightly CI 里跑一次依赖矩阵输出 json 格式的依赖关系表再转成一份带异常标记的报告上传到内部文档站。这个报告不是为了看而是为了在月度技术评审时讨论这个月有没有新的跨层依赖哪些模块的扇入扇出数据出现异常扇入fan-in是衡量一个模块被多少其他模块依赖的指标扇出fan-out是这个模块依赖了多少其他模块。我经验中比较健康的状态是基础工具类可以高扇入低扇出业务编排类应该低扇入中扇出最怕的是出现一个中间层模块扇入和扇出同时很高那它几乎就是系统中的上帝服务所有业务都找它要数据它又到处拉取别的模块的数据。出现这种信号就要考虑是不是该做数据模型拆分或引入防腐层了。实际操作里还有一个可复用的速查经验当一个模块的依赖方向检查连续三周触发同一类告警不要继续在约束规则层面打补丁应该直接组织一次模块重构评审。规则只能警示不能代替设计决策。t3code 第三层项目能真正跑起来靠的其实是团队里每周一次半小时的依赖健康站会规则负责打断违规站会负责讨论为什么会有违规意图。5. 常见问题与排查经验实录5.1 增量门槛和存量告警的平衡最常见的第一个问题是规则一上存量代码告警爆炸。SonarQube 全量扫描一个十万行仓库初始告警上千条非常正常。这时候如果直接开 CI 门禁团队一天之内就会反感因为所有人都在为自己去年写的代码买单。我用的是存量冻结 增量严管策略SonarQube 里调整质量配置为存量问题创建 issue把它们的状态统一设成 Wont Fix 并写明原因遗留债务待专项重构新代码则在质量门禁里全量执行规则。这里有个操作技巧删改一个旧文件的代码行时改动行会算进增量扫描范围所以要求程序员在顺手改旧代码时至少保证不新增同类告警而不是把整段逻辑全推倒重来。事实证明这个策略推进阻力小很多团队会把精力放在我现在写的代码而不是刚接手时别人挖的坑。5.2 规则冲突与误报处理第二个常见问题是不同工具之间的规则互相冲突。我在 t3code 里真实碰到过一次ESLint 的 no-use-before-define 与 TypeScript 类型定义偶尔打架Prettier 格式化后的多行三元表达式在某些情况下会被复杂度规则判定为嵌套过深。处理这类问题我的原则是尽早确定规则的唯一权威来源。具体做法是在 t3code 的 rules 目录里维护一份 decisions 文档每条冲突都记录决策日期、涉及工具、最终裁决和原因让 ESLint、Prettier、SonarQube 之间的边界可追溯。另外ESLint 可以用 overrides 为测试文件、脚本文件单独放开某些规则比如测试文件可以放宽 max-params因为它们经常需要用多个 mock 参数。这种差异化配置不是纵容而是保证规则本身是合理信号而非噪音。如果规则报警内容对团队完全没有指导意义要么调阈值要么删规则最忌讳的就是把规则一直开着但大家有了告警也习以为常。狼来了喊多了真正有价值的告警也会被忽略。5.3 CI 门禁的落地与性能开销第三类问题集中在规则跑不跑得动。一个复杂前端项目跑 lint 全量可能需要两到三分钟把复杂度检查、架构约束检查和格式化校验全部塞进 CI 会让主流水线变得很长。我踩过这个坑之后是这么调配的在本地 pre-commit 阶段跑 lint-staged只处理本次暂存文件耗时控制在秒级。在 CI 的 merge request 流水线里跑增量 lint通过 git diff 只检查本次 MR 涉及文件。把全量 SonarQube 扫描和依赖矩阵生成放进 nightly 流水线结果在内部平台展示供次日跟进。这样的好处是开发者平时几乎感觉不到质量门禁的存在因为最快的反馈留在了本地而最重的全量分析又不打断发版节奏。我用这个配置把 CI 中代码质量相关的耗时从天级别压到了分钟级别团队接受度随之明显上升。6. t3code 落地过程中的三个体会收尾项目跑了两个多月以后我最直观的感受是代码治理这件事真正困难的地方不是缺少工具也不是缺少规则而是缺少一个能让大多数人愿意配合的分层策略。t3code 把可读性、可维护性、可扩展性当成三个独立阶段每阶段只处理一个问题反而比一口气把所有质量指标全亮出来更容易推进。管代码质量像管一条河流你不可能一次性把整条河的水都换成干净的但可以在上游设取水标准、在中游设沉淀池、在下游设排放检测。T3 三层做的事情不过是把这三道闸门的位置和开启条件定清楚。如果你正在为自己仓库的代码质量发愁可以先从 Tier 1 的格式化落地开始跑通一周再往下叠加复杂度治理和架构约束别急着一天之内把全部高地都拿下来。最后再分享一个小技巧每层规则落地时都在团队公告里配一条违规案例的演示 commit用真实 diff 展示改前和改后的区别。人只有看到具象的好处才愿意持续遵守抽象的规则。这是 t3code 项目走到现在我自己的总结里最有价值的一件事。
阅读完成 · 觉得有帮助?