接手过别人烂代码的人,大概都能理解那种感觉:一个函数五百行,变量名是data1、data2、temp,改一个bug要顺着调用链摸半天,最后发现坑在某个不起眼的全局状态里。我有一段时间就是在这种项目里挣扎,后来痛定思痛,慢慢整理出了一套自己的代码质量体系,给它取名叫t3code。这东西不是什么惊天动地的框架,也不是某门语言的特定规范,而是一套从"能跑"到"能看懂"再到"能演进"的三级标准,覆盖命名、结构、设计、优化、协作几个层面。这几个月我试下来,效果比我预想的要好不少,于是决定把这套方法完整地写出来,给那些正在被代码质量困扰的团队和个人做个参考。这套东西不挑语言、不挑项目规模,哪怕是个人开源项目或者一两个人的小产品,也能直接用。
1. 从"能跑"到"能演进":T3Code到底是什么
1.1 我为什么开始整理这套标准
一开始真的不是什么高深理由,就是受不了了。团队里代码review的时候,经常出现"这段代码逻辑没问题,但过一个月我们自己都看不懂"的尴尬局面。线上出bug,排查效率完全取决于当初写这段代码的人此刻在不在线。文档有,但文档跟代码是两套叙事,维护文档本身又成了额外负担。
后来有一次,一个同事请假两周,我接手他一个模块,光是搞明白那个状态机的流转逻辑就花了大半天,最后发现设计文档里描述的流程和实际代码对不上。那一刻我意识到,问题不是某个人的能力,而是缺少一套所有人心照不宣的代码组织方式。于是我开始观察那些开源界公认写得漂亮的项目,再对照我们自己的代码,逐条总结差异,慢慢形成了t3code的雏形。
定义上,t3code把一段代码从"写出来"到"被维护"的整个过程分成三个等级(T1、T2、T3),每一级都有明确的核心关注点。这三级不是彼此独立的,更像是层层递进的台阶:你不可能在函数命名一塌糊涂的情况下去谈什么架构演进,同样,代码能跑了也只是最低标准,离可维护、可复用还差着两层楼。
1.2 T3Code的三级结构:命名规范、设计手法、演进能力
我先直接给出框架图,再一个一个解释:
- T1(清晰层):目标是让人和机器都能快速读懂。重点在命名、格式化、函数边界、单一职责。这一层是地基,地基不打牢,上面全是空中楼阁。
- T2(复用层):目标是让代码可以被安全的复用和组合。重点在接口设计、依赖管理、模块划分、设计模式的应用。
- T3(演进层):目标是让系统能够低成本地应对需求变化和规模增长。重点在观测性、配置化、重构策略、领域边界。
需要注意,这套分级不是岗位职级,不是说架构师才需要T3,初级工程师只需要T1。实际上,一个刚入职的新人写出的工具函数,理论上也可以直接达到T2甚至T3的水准,因为它衡量的永远是一个具体的代码片段或模块,而不是某个人。
1.3 T3Code不是银弹:它解决什么、不解决什么
很多团队一听说"代码规范",第一反应是"我们也有规范,但没用"。这很正常,因为大多数规范只停留在T1层面——列了一堆命名规则、缩进规则,然后review的时候对着规则表检查。但是对于"为什么这个函数要拆成两个""为什么这个模块依赖反了"这类更深层的问题,基本没人管。
t3code想解决的问题,主要是这几类:
- 代码可读性差,新人上手慢
- 模块之间耦合严重,改一处崩一片
- 单元测试不好写,因为逻辑和副作用纠缠在一起
- 需求变更时,小改动变成大工程
- 知识只存在于老员工脑子里,人一走知识就没了
它不解决的问题也很明确:它不帮你做技术选型、不规定你用哪种设计模式、不是性能优化指南、也不是项目管理方法论。它是一套关于代码形态的组织原则,和具体技术栈无关。
打个比方,如果说编程语言是砖块,设计模式是户型图,那么t3code就是一套施工纪律——它保证每一堵墙都砌得直,每一根梁都放得正。你不按纪律施工,房子也能盖起来,只是盖不高罢了。
2. 第一级(T1):让代码先"能被看懂"
2.1 命名规范:变量、函数、文件的三层约定
我见过太多代码,问题不是写法错误,而是表达失败。变量叫flag,谁知道你代表什么flag?函数叫handleData,你处理了多少种数据?文件叫utils.js,里面装了二十个毫无关联的函数——这种文件我后来统一叫"垃圾收纳箱"。
t3code对于命名的要求,核心就三个:准确、完整、领域化。
- 准确:
isUserLoggedIn比checkUser准确;getPendingOrderList比getList准确。 - 完整:不要吝啬那几个字母,
idx不如currentIndex,tmp不如tempValue。代码读的次数是写的几十倍,那点打字成本早就在后续读代码时赚回来了。 - 领域化:用业务术语命名。用户模块里,
orderStatus而不是s;支付模块里,refundDeadline而不是endTime。领域化命名让代码自带上下文,读代码的人不需要再"翻译"一次。
文件名同理。我见过common.js、helper.js、utils.js这种文件,里面什么都放,久而久之成了无人敢动的黑洞。t3code的建议是:文件名要能让人猜出里面大概有什么,比如formatOrderData.js、validateEmail.js。如果一个文件必须叫utils才能装下所有东西,那大概率是拆分的粒度出了问题。
2.2 格式化与静态检查:先机器后人工
命名是主观的,但格式化不是。这也是t3code里我最"强硬"的一条:凡是机器能自动检查的问题,就别让人在review时浪费口水。
具体操作上,每个项目都应该配置好三样东西:
- 格式化工具(Prettier / Black / gofmt 等)
- 静态检查工具(ESLint / Ruff / golangci-lint 等)
- 提交前钩子(husky / pre-commit 等)
这三样配合起来,能拦住一大批低级问题。比如缩进不一致、分号缺失、未使用的变量、明显的类型问题、某种反模式。我以前团队里有个人特别喜欢把多个逻辑塞进一行,每次review都要提,后来直接在CI里强制跑检查,没过的一律打回,省了不知道多少口舌。
顺便说一句,静态检查工具的规则不要一开始就拉满。我见过有人直接开了一百多条规则,然后代码里一片红,最后大家干脆把检查关掉了。合理的做法是先开高标准的核心规则,然后随着项目演进逐步增加,且每一条规则的启用都要有明确理由。
2.3 函数边界:单一职责的落地判断
单一职责(Single Responsibility Principle)可能是被误解最深的一个原则。很多人以为"一个函数只做一件事"就是短——于是他们疯狂拆函数,最后每个函数只有三行,但调用链深不见底。
t3code对函数边界的判断方法,不是看长度,而是看三个问题:
- 这个函数能不能被一句话说清楚它做的事情?
- 这个函数是否需要多处修改才能应对一个需求变化?
- 这个函数的入参和出参是否都在同一个抽象层级?
举个例子,一个函数叫processOrder,里面前半段解析JSON、中半段查库存、后半段发邮件、再末尾记录日志,你很难用一句话说清楚它做什么——这就是不合格。但如果拆成parseOrderPayload、checkInventory、sendOrderConfirmationEmail、writeOrderAuditLog四个函数,每个函数你一眼就知道它干嘛,这就是合格。
这里有个技巧我用了很久很有用:写函数的时候先写注释——用一句话说明这个函数要做什么。如果写不出来,说明边界不清晰,继续拆;如果写出来了,就把这句注释当成函数名,注释能写多长,函数名也能近似参考这个长度。我发现这个"注释先行"的技巧,比任何设计原则都更容易落地。
抽象层级也是一个被忽略的点。parseOrderPayload是"数据处理层",sendOrderConfirmationEmail是"外部交互层",这两个就不该出现在同一个函数体里。很多"面条代码"就是因为不同抽象层级的逻辑混在一起,一会儿在解析字符串,一会儿在操作UI,一会儿又在调API,读者被迫反复切换上下文,极其消耗精力。
3. 第二级(T2):让代码能"被复用"
3.1 抽离通用逻辑的时机判断:不要早抽,不要死等
复用是好事,但过度追求复用会导致代码抽象过度,反而让人看不懂。t3code里我坚持一个原则:先写业务代码,当同一个模式出现两次以上,再考虑抽象。
不是三次,是两次。两次重复就是信号。事不过三那套延迟抽离的规则更适合架构层面的大规模抽象;对于函数级别的公共逻辑,第二次出现时就应该敏感起来。理由是:第一次出现是事实,第二次出现是趋势,第三次出现就是灾难。
比如你在A模块写了一个convertOrderToExportFormat函数,很快在B模块也发现了类似的逻辑,那就应该把公共的部分抽到一个共享层。抽的时候要保持一个红线:被抽出的函数不允许知道自己被谁调用,它只负责处理输入和返回输出,不持有调用方任何信息。这样后续C、D模块才能安全地复用它。
3.2 接口设计与依赖倒置
很多代码复用失败,问题出在上层直接依赖了具体实现。比如某个业务函数里直接new了一个MySQLOrderRepository,然后读写数据库。这导致如果后面想换数据源、加缓存、或者做单元测试时用内存版实现,就得改业务代码。
t3code推荐的接口设计思路,核心是面向接口编程,而不是面向实现编程。做法非常朴素:
- 调用方依赖抽象(接口、抽象类、函数签名)
- 具体实现在外部注入(构造函数、依赖注入容器、甚至手动传参)
写成代码大概是这种感觉:
// 不推荐:直接在业务逻辑里依赖具体实现 class OrderService { private repo = new MySQLOrderRepository(); async getOrder(id: string) { return this.repo.findById(id); } } // 推荐:依赖抽象,外部注入 interface IOrderRepository { findById(id: string): Promise<Order>; } class OrderService { constructor(private repo: IOrderRepository) {} async getOrder(id: string) { return this.repo.findById(id); } }这个设计够简单,但价值巨大。测试时你可以注入一个InMemoryOrderRepository,切换数据库时只改装配层,业务代码完全不动。依赖倒置原则听起来很高深,但落地不过就是这个程度。
3.3 模块划分与目录结构约定
模块划分是T2阶段的另一个重点。很多项目的痛点不是函数写不好,是整个代码组织方式是乱的。比如说,按"类型"划分目录——controllers、models、services、utils——这种划分方式在小型项目中还行,一旦业务复杂起来,就会变成灾难:每个模块里都有各自版本的"相关逻辑",改业务需求时你得同时改四五个目录下的文件。
试试按业务领域(Feature)划分,效果会完全不同:
src/ features/ auth/ components/ services/ types.ts utils.ts order/ components/ services/ types.ts utils.ts payment/ components/ services/ types.ts utils.ts shared/ http/ logger/ config/这种结构的好处是:每个业务领域的代码内聚,改动一个业务需求时,你待的地方只有一个目录。而真正跨领域共享的公共逻辑,才放进共享层。这个做法在DDD(领域驱动设计)里叫"聚合"的思想,但t3code不规定你必须用DDD那套战术模式,你只需把**"围绕业务划模块,而不是围绕技术划层"**这个理念用起来就够了。
模块之间的依赖关系也要定个规矩:业务领域之间不能互相依赖,共享逻辑只能放在shared里,并且shared不能反向依赖任何业务模块。有了这条红线,你就不会出现"为了图省事,直接在一个模块里import另一个模块的内部实现"的情况——这种import一旦出现,两三天后两个模块就彻底耦合了。
4. 第三级(T3):让代码能"被演进"
4.1 状态管理与调试友好:从可观测性入手
代码写出来是给人看的,但更是给"未来调bug的人"看的。一个系统的可维护性,很大程度上取决于当线上出问题时,你能否快速定位到根因。t3code在T3层面最重视的,就是可观测性。
怎么落地?不是非得搭一套分布式追踪系统,对于大多数项目来说,做好这几件事就够了:
- 统一的日志规范:什么级别的操作打info,什么打debug,什么打error,错误日志必须包含上下文(订单ID、用户ID等)。
- 结构化日志:不要打 "user save success" 这种,要打
{ "event": "user.save.success", "userId": 123, "elapsedMs": 45 },方便检索和聚合。 - 关键路径埋点:创建订单、支付回调、退款、任务队列消费,这类关键节点必须有点位。
- 异常链的穿透性:不要吃掉堆栈,不要
catch了打个日志就完事,能向上抛就向上抛,让最顶层统一处理。
这里有个反面案例我印象很深。之前排查一个线上bug,日志显示Error: null pointer at UserService.java:150,但没有任何上下文信息。我们得靠部署时间和日志时间推断是哪个用户触发的,后来又拿时间戳反查Nginx访问日志才找到线索。从那以后,我要求所有业务异常必须携带业务唯一标识(订单号、traceId、userId),排查成本直接降了一个量级。
4.2 重构策略:小步快走的节奏控制
演进不是一次轰轰烈烈的"重写",而是无数个小重构的累积。t3code对这个阶段的建议非常明确:永远不要在大型重构的同时加新功能。重构的每一步都应该是可以独立合入的、不改变外部行为的修改。
实操上的节奏大概是:
- 发现坏味道(比如一个函数过长、依赖混乱)
- 决定重构方式(拆函数、改接口、下推/上提逻辑)
- 只做重构,不做功能变更
- 跑测试,确认行为未变
- 合入
这个节奏看着慢,实际上长期来看是最快的。因为每次变化都很小,review的人容易通过,出问题了也容易回滚。最怕的是有人憋了三个星期,扔出一个几千行改动的大PR,说是"重构优化",实际上没人能review得动,出了问题也没法定位是哪一步引入的。
我见过不少项目死在"大重写"上。真的,重写的诱惑是巨大的,因为旧代码太烂,让人感觉"推倒重来比较省力"。但事实是,你如果没有建立起新的规范,重写的结果只会是第二坨烂代码,只是烂得比较新而已。更好的策略是"绞杀者模式"——用新代码逐步替换旧模块,每次替换一块,替换完立刻收获一块干净的地盘。
4.3 文档与注释:写给别人也写给未来的自己
我一直认为,代码中最常见的注释错误不是"没写注释",而是写了描述"是什么"而不是"为什么"的注释。
看这两行:
// 将订单状态设为已支付 order.status = 'paid';这种注释就是废话。代码本身已经说明了order.status = 'paid'。真正该注释的是这种:
// 先设为paid再发通知,防止通知回调时查到旧状态 order.status = 'paid'; await notifyUser(order.id);前者是噪音,后者是信息。t3code对注释和文档的要求就两点:
- 注释解释为什么,而不是解释什么
- 文档描述行为约定,而不是复述代码
至于文档,我不推荐那种事无巨细的"架构设计文档"——那种文档通常写完就过期。真正有效的文档是:
- README 说明项目是什么、怎么跑起来、目录结构是怎么划分的
- 每个模块的 README 说明这个模块负责什么业务、关键流程是什么
- 代码中的注释负责解释那些"不读代码就不知道的约定"
还有一点我要单独强调:文档跟着代码走,不要独立存在。如果文档在Wiki里、代码在Git里,那文档必死无疑。把文档放进Git仓库,和代码一起变更、一起review,doc就是代码的一部分,而不是代码的影子。
5. T3Code落地过程中的关键经验与常见反例
5.1 最容易毁掉整个项目的坑:过度设计
写规范容易,落到真实项目里就难,难在所有规则都有适合的边界。t3code推广过程中最大的阻力不是"没人遵守",而是"有人过度遵守"——为了抽象而抽象,为了设计模式而设计模式。
举个例子,一个只有两个字段的消息格式转换,有些人因为看了某本书,非要搞一个TransformerFactory、AbstractMessageConverter、MessageStrategy三件套。看起来"架构清晰",实际上任何接手的人都会一脸问号:我一个JSON.parse就能解决的问题,为什么要浏览五个文件?
过度设计的技术特征,我总结下来就一条:抽象层级超过实际复杂度一个数量级。解决它的办法是T2那节说的:抽离通用逻辑的时机判断,按重复次数来,不要按想象力来。你觉得"未来可能会复用",和"现在真的复用了",是两回事。写成业务代码,等信号出现,再抽离也不迟。
5.2 团队协作与Code Review的配合
个人写代码是一回事,团队协作是另一回事。t3code要落地,光有人写规范没用,review环节必须跟上。我们的review流程经历了三个演进阶段:
- 阶段一:review只看"有没有bug",不注重"代码是否可持续维护"。
- 阶段二:开始把可维护性作为review要点,但review全凭个人经验,没有统一标准,经常引入争论。
- 阶段三:以
t3code的三级标准为checklist,review从"主观评价"变成"按标准检查"。
阶段三视角下,reviewer对提交的代码会按T1、T2、T3三个维度问问题:
- T1:命名够不够准确?函数边界清不清楚?静态检查有没有过?
- T2:这里是否重复出现两次?该不该抽公共层?依赖方向对不对?
- T3:这个模块将来需求变化时,改动范围会不会失控?日志上下文够不够定位问题?
有了这套checklist,review效率反而高了,因为争议从"我觉得"变成了"标准是"。标准不是限制创造力的枷锁,恰恰是减少无意义争论的润滑剂。
另外,我发现一个微妙但很重要的事:用t3code的标准做review,代码质量问题会提前暴露在合入前,而不是上线后。以前三天两头在生产环境排查那些"当时看着没问题,后来发现是坑"的代码,现在这类问题明显少了。
5.3 存量代码改造:不要试图一夜之间推翻一切
刚接触t3code的团队很容易有一个冲动:把老代码全部重构一遍。我强烈建议别这么做。存量代码的体量往往比想象中大得多,而且老代码通常还承载着复杂的业务逻辑,贸然动刀很容易引入隐性bug。
我的建议是把改造分成几步,稳扎稳打:
- 先给存量项目配置好格式化工具和静态检查,把"机器能自动发现的问题"先清零。
- 从团队当前最痛的模块入手,选定一个边界清晰、改动可控的模块做T1到T2级别的重构。
- 建立"绞杀者"计划,在新增代码中强制按
t3code的标准走,老代码保持冻结逐步替换。 - 每次重构必须带着测试,没有测试的模块先补关键路径的测试,再动手改。
我还记得我们第一次试点,选了一个体量小但非常重要的支付回调模块。第一次重构改动量并不大,主要是把原来五百行的函数按职责拆开,把散落的日志和数据访问统一收拢。结果上线后出了个问题,由于我们保留了原有行为不变,定位问题时比对老代码日志反而更清晰——因为新代码的日志上下文更完整,队友第一次体会到"代码规范立刻转化为排障效率"的正反馈。
从那之后,推t3code就不再是我一个人喊口号了,而是变成大家主动想要的方向。
4. 最后想说的
t3code不是什么高深莫测的理论,说穿了就是把很多优秀工程师已经在做的事情,系统化、可执行化,并且拆成了"先能懂、再复用、再演进"三个阶段。如果你从头建立一个新项目,直接从第一级的标准开始,成本几乎为零,收益却会随着代码量增长而指数级放大。如果你手里已经有一堆存量代码,也别焦虑,从格式化工具和静态检查入手,挑一个试点模块逐步改造,体会"小步重构"带来的稳定感之后,自然就知道怎么继续了。
最后再分享一个小技巧:把t3code的三级标准做成一张A4纸的checklist,贴在工位上,或者放进项目的CONTRIBUTING.md里。写代码之前扫一眼,提交之前再扫一眼,不出两周就会形成肌肉记忆。等哪天你看到一个命名精准、边界清晰、上线半年没出过问题的模块,你会觉得当初定这套规矩花的时间,真的太值了。