在开发这个圈子里泡久了,你会发现一个很有意思的现象:很多人写代码,功能是能跑的,测试也能过,但一旦要接手别人留下的模块,或者让别人来接手自己的模块,那种“说不清哪里不对劲,但就是浑身难受”的感觉就会出现。这几年我一直在琢磨一件事——抛开具体的语言和框架,一套代码到底凭什么能被称为“好代码”?后来我把自己的答案收敛成了一个模型,起名叫 t3code。
这个名字不玄乎,拆开来看就是 T3 × code,意思是把代码质量分成三个层级来审视:Tier 1 能跑、Tier 2 能读、Tier 3 能交付。它不是某个框架,也不是某个工具,而是一套我在实际项目里反复验证过的自我检查方法。解决的问题很具体:为什么你的代码逻辑全对,却总被 review 打回?为什么半年后回看自己的代码,连自己都要查半天?为什么别人写的库用起来很顺手,轮到自己写公共模块就总是差口气?
适合谁看?我觉得 1 到 3 年的后端、前端、客户端开发者都能从中找到对应阶段的痛点,中高级开发者也能拿这套分层去校准自己带人时的评审标准。下面我把这套方法背后的思路、实操细节、踩过的坑一次讲清楚。
1. 为什么是 T3:三层编码模型的拆解思路
1.1 T1 能跑:功能正确远没有你想的那么简单
绝大多数人写代码,停在了 T1 的及格线上。这里的“能跑”不只是说程序不报错,而是指在预期输入下能给出正确输出,在异常输入下不至于崩得很难看。
但“能跑”这个词其实很有迷惑性。我见过很多同学觉得,if-else 分支写全了、接口调通了、页面能渲染了,就是 T1 完成了。实际上,T1 隐含的要求往往被忽略:输入校验、缓存击穿、重试策略、并发边界、超时处理。这些不会在正常路径上报错,但一定会在某个半夜的报警电话里找你算账。
我自己的判断标准很简单:如果一个新同事拿到你的模块,不改任何业务逻辑,只用正常参数和极端参数各调一遍,能得出符合预期的结果,那么 T1 算过关。注意,极端参数这一条就能筛掉至少一半代码。
T1 的价值在于它是信任的地基。地基不稳,上面谈什么可读性、工程化都是空中楼阁。一个真实例子:之前做过一个订单导出的功能,初版代码只跑了 happy path,CSV 里一旦出现包含逗号的字段,导出的文件就会错列。功能在演示的时候看起来无比顺畅,但真要交付给运营用,第一周就会出现一堆数据错乱的工单。这就是典型的还没跨过 T1。
1.2 T2 能读:代码是写给“下一次修改”的人看的
T2 是我认为区分“能干活”和“有职业素养”的分水岭。这里的“能读”不是指语法上能看懂,而是指一个不熟悉上下文的人,能通过代码本身理解它的意图、边界和变更理由。
我一直记着一句话:写代码的时候,你的读者是三个月后的自己。三个月后的你早就忘了当时的业务细节,如果代码没有清晰的结构、命名和注释,那你和陌生人没有区别。很多人在 review 里发的那些“这段逻辑看不懂”“这个命名是什么意思”“这个函数为什么这么长”,本质上全是 T2 的问题,而不是功能正确性的问题。
T2 怎么做?其实标准非常朴素:函数一眼能看出它做什么,变量名不产生歧义,控制流是线性的而不是一团乱麻,注释写的是“为什么”而不是“是什么”。我一般要求自己做到一个效果——把代码打印出来,不聊天不讲解,直接递给同事看,他能在我离开座位的情况下复述出这段代码的业务流程。做不到,就是 T2 没达标。
1.3 T3 能交付:工程级代码的隐形门槛
T3 是我这两年才真正重视起来的。很多个人项目或者短期项目里,代码能跑、能读就已经够用了,但一旦进入团队协作和长期迭代,T3 才是决定项目能不能持续走下去的关键。
T3 的全称应该是“能交付给生产环境长期运行的代码”。它包含的维度非常广:可测试性、可观测性、异常兜底、性能余量、依赖管理、兼容性策略。换句话说,T3 关注的不是“这个功能今天能不能上线”,而是“这个功能上线之后,半年内出了任何问题,我们能不能快速定位和修复”。
举个例子,一个接口刚上线时每天调用量只有几万,T1 到 T2 完全够用。但一旦业务量增长到每天几百万,日志没打全、慢查询没发现、熔断没做、容量预估缺失,这些问题就会集中爆炸。T3 的核心价值是提前把这层风险垫平,它做的很多工作看起来像“过度设计”,实际上是为了给未来留出可操作的空间。
2. 核心细节解析与实操要点:三个层级分别怎么抠
2.1 T1 阶段必须守住的四个基线
把 T1 做好,不需要花哨的技巧,只需要守住四个基线。
第一,输入输出要明确。函数的入参和返回值要有明确定义,能窄就不要宽。你用Object当参数接收一切,等于告诉调用方“猜去吧”,这不是灵活,这是埋雷。
第二,逻辑分支要完整。if-else 不只是写完正常路径,else 覆盖不到的场景要考虑。写 switch 记得给 default,写策略模式记得给兜底实现,写正则记得处理不匹配的情况。
第三,异常要兜底但不要吞掉。最忌讳的是catch (Exception e) { },空异常等于把故障埋进了深渊。正确做法是记录日志、返回可理解的提示、并且抛给上层合适的错误模型。这里我有个习惯:凡是 catch 里没有日志输出的代码,review 一律打回。
第四,不要隐藏错误。有些人为了防止“程序崩溃”,宁可返回一个null或者空对象,也不愿意把错误抛出来。短期看界面不红了、接口不报 500 了,实际上是把错误从“显性”变成了“隐形”,问题没有消失,只是更难被发现了。这个心态要改。
我建议把上面四条变成团队 review 的固定检查项。不要每次都展开讲道理,直接对照清单打钩就行,效率高很多。
2.2 T2 阶段的可读性自检清单
T2 的落地可以细化成一张可执行的自检清单,每次写完代码对着过一遍,能解决大部分可读性问题。
函数长度控制在 5 到 20 行之间。少于 5 行通常是过度封装,多于 20 行通常意味着里面塞了不止一件事。如果你发现自己经常写 50 行甚至 100 行的函数,先别急着重构,试着把它按动作拆开,你会发现原来的很多局部变量其实可以变成参数,很多嵌套 if 其实可以用卫语句提前返回。
命名是最值得花时间的地方。boolean 类型的变量不要用flag,要用isXXX、hasXXX、shouldXXX;集合类型的变量名带上复数意识;工具函数用动词开头。我自己有个土办法:如果一个变量名需要超过 10 秒才能想出来,说明这个变量的作用还不够清晰,往往需要重新划分数值或提炼概念。
参数数量控制在 3 个以内。超过 3 个参数的时候,调用方会迷失在参数顺序里,出错率直线上升。这时候应该把相关的参数封装成一个结构体或者配置对象,语义反而更清楚。
注释只写“为什么”。代码本身已经能表达“是什么”,注释再去复述一遍就是噪音。真正值得注释的是:为什么这里要兼容老数据?为什么不能直接改这个字段?为什么这里的排序规则比较奇怪?这些“为什么”背后全是业务约束和历史包袱,是后来人最容易踩坑的地方。
2.3 T3 阶段真正拉开差距的工程化手段
T3 不是靠写代码那一刻的灵感,而是靠把“上线后怎么活得好”前置到开发过程中。
测试是第一个硬指标。不是说覆盖率一定要到多少,而是核心流程必须有用例守护。写测试最直接的作用是逼着你把代码拆成可测的结构,副作用是你发现很多 T1 的边界问题在写测试的时候就暴露了,这比上线后暴露要好一百倍。
日志是第二个硬指标。每个关键路径至少要保证“进、出、错”三个节点有日志,且日志要带上上下文标识。以前踩过一个坑,线上出了问题,日志一直在报错,但报错里没有任何订单号或者用户 ID,根本没法定位是哪个请求出了问题。后来把所有日志都要求带上 requestId,问题平均定位时间从小时级降到了分钟级。
性能和依赖也不能松懈。接口的响应时间基线要在开发环境测出来,超过 500ms 的接口必须能说清楚耗在哪里;第三方依赖要统一管理版本,避免传递依赖冲突;定时任务要考虑执行时间和重入问题,避免重复数据。
这些事单看都不难,难的是形成习惯。我目前的节奏是:功能开发占 60% 的精力,剩下 40% 固定在测试、日志、边界检查上,这套比例帮我躲过了很多线上事故。
3. 实操过程:把一个真实功能按 t3code 三层走一遍
3.1 第一步:先给现状打分
拿到一个需求,我习惯先按 t3code 的框架给自己做一个现状盘点,目的不是追求形式上好看,而是明确自己现在处于哪一个层级。
我拿一个真实的“批量导出订单”功能来演示。需求很简单:根据时间范围导出订单 CSV,包含订单号、金额、商品名、状态。第一版我很快写完了核心逻辑,功能看起来很正常,在本地跑也没有任何问题。但如果用 t3code 的标准去打分:
- T1 边界:时间范围传反了会怎样?订单商品名包含逗号或换行符会怎样?数据量为 0 会怎样?
- T2 表达:核心函数
exportOrders()有 80 行,中间还有 6 层嵌套,变量名是d1、d2、tmp。 - T3 工程:没有日志、没有测试、没有考虑大数据量下的内存占用,导出的 CSV 一次性全加载到内存里。
这一盘点下来,第一版其实只是“演示版”,连合格的 T1 都需要补强。这个过程非常重要,因为大多数时候我们不是写不出来好代码,而是根本没意识到自己写出来的东西离标准有多远。
3.2 第二步:逐层补强 T1、T2、T3
T1 层面,我先把边界补齐。时间范围做参数校验,开始时间不能晚于结束时间;商品名字段按 CSV 规范,如果包含逗号、换行符或引号,必须包裹转义;空数据时不生成文件而是返回明确提示;导出量级超过 5 万条时切换成分批查询,避免一次性加载所有数据导致内存溢出。这一步做完,功能才算真正“能跑”。
T2 层面,我把那个 80 行的函数拆成三个:buildExportQuery()负责查询条件组装,formatOrderRow()负责单行格式化,writeCsv()负责流式输出。拆完之后每个函数都能一眼看出职责。参数从原来的 6 个收成了 2 个,剩余参数封装成了ExportRequest结构体。
T3 层面,我补了三件事。第一件,给导出过程加上进度日志,记录每个批次处理了多少条、耗时多少、失败了多少;第二件,针对核心的格式化逻辑写单元测试,专门覆盖逗号、换行、超大文本等边界;第三件,把导出的数据量上限做成可配置项,超出时明确提示用户拆分导出。这些改动不会让功能本身更“炫”,但会让这个功能具备交付的品质。
改造完再打分,这套代码就从“本地能跑”跨到了“团队里任何一个人接手,一个月后都能放心维护”的水平。整个过程大概多花了两三个小时,换来的却是后面上线后省下的无数个排查之夜。
3.3 第三步:把这套检查变成肌肉记忆
t3code 不能只靠某一次的认真,它必须内化成写代码的默认姿势。
我现在写完任何一个模块,都会主动问自己三个问题:第一,如果我不在这个公司了,同事能不能通过代码直接理解这个模块的意图?第二,上线后第一个月最容易出问题的点,我是不是已经提前打了日志?第三,核心逻辑有没有测试兜底,改坏了马上能发现?
这三个问题听起来很简单,但真正坚持下来并不容易。以前写代码总觉得“后面有时间再补测试”“先上线再说”,然后就没有然后了。现在我把补测试、补日志、补边界当成功能的一部分,不完成就不算开发完,Review 也不通过。半年下来,最大的变化不是代码变好看了,而是线上问题的数量肉眼可见地在下降,心理压力小了很多。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
整理一下我在实践 t3code 过程中遇到频率最高的几类问题,给同样在往这个方向努力的同学做个参考。
| 症状 | 典型现象 | 排查思路 | 处理方式 |
|---|---|---|---|
| 代码逻辑正确但没人敢改 | 改动一个小功能要翻半天调用链 | 大概率是 T2 不合格,函数过长、命名含糊、依赖混乱 | 按动作拆函数,把长调用链的中间状态提成明确的领域对象 |
| 边界问题集中暴露 | 用户输入稍微特殊点就报错 | T1 输入校验和分支覆盖不完整 | 补全参数校验,列出所有极端输入逐条测试 |
| 线上问题定位慢 | 报错日志有但找不到是哪一笔业务 | T3 可观测性不足,日志缺少上下文 ID | 全链路日志加上 requestId、userId 等业务标识 |
| 测试补不上 | 代码耦合太紧,mock 成本太高 | 写用例时发现结构不合理 | 先从拆分纯函数和固定输入输出开始,把 IO 操作隔离到边缘 |
| Review 总在争论风格 | 每个人有自己的代码习惯 | 缺少统一标准,评价全靠感觉 | 把 t3code 三层的检查项固化成团队 checklist |
这张表是我实际排查问题时的路径总结,大部分“看起来很难搞”的代码问题,回溯到根因都逃不出 T1、T2、T3 这三层。
4.2 为什么代码 Review 总在吵架
很多团队 Review 效率很低,一半时间耗在风格争论上,比如“你觉得这个变量叫 data 好还是叫 info 好”“这里应该用 map 还是用 switch”。表面看是技术分歧,实质是大家手里没有一把共同的尺子。
尝试引入 t3code 之后,我要求团队在 review 时只聊分层问题:T1 的边界漏没漏?T2 的命名和结构清不清楚?T3 的日志和测试有没有补?风格类问题除非严重影响表达,否则不做硬性要求。
这一招非常管用。Review 的场面从“我觉得”“我认为”变成了“这里 T1 不完整,导出的内容没做转义”“这里 T2 有问题,函数命名和实际行为不符”。一旦标准统一,讨论就立刻从主观偏好转移到客观标准上,效率提升是肉眼可见的。
4.3 关于通宵改 Bug 的真相
我见过不少团队把通宵改 Bug 当成“团队奋斗”的勋章,但我个人的感觉是,这个姿势本身就是 t3code 三层的系统性失败。
通宵改的 Bug 大多是这几类:一是数据边界没考虑,某些历史数据格式特殊,线上跑到就挂了;二是并发场景没覆盖,压测没测出来,深夜流量低谷反而出问题;三是日志缺失,现场信息不够,靠瞎猜定位。其实每一类都能在开发阶段用 T1 的边界校验、T3 的日志和测试来拦截掉。
所以现在每次遇到有人炫耀通宵经历,我都会默默想一下:如果当时在 T1、T2、T3 上多花两小时,今天这个夜是不是本来不用通?排除确实极其罕见的线上疑难杂症,至少七成线上故障,都能靠把编码层级抬高一档来规避。
4.4 团队落地 t3code 的节奏和坑
最后说一下怎么在团队里推行这套方法,因为一个人用是习惯,一群人用才是制度。
我的建议是不要一步到位,那样阻力非常大。先找一两个核心模块,在代码评审时引入三层检查,其他模块看得见的改,看不见的暂时不动。先跑两到四周,让团队感受到明显的质量变化,有了正向反馈再去扩大到全部模块。
落地过程中踩过最大的坑,是在一开始把标准定得太高。团队里有些老代码本身就是历史债务,不可能一次性全部按 T3 重构。我的做法是:存量代码只补最关键的风险点,新增代码严格要求,用增量慢慢替代存量。半年下来,核心模块基本都被迭代过一遍,质量水位自然就上来了。
另外一定要有实物产出,比如把三层检查清单打印出来贴在工位旁边,或者在 MR 模板里内置这几个检查项。单纯口头宣讲是记不住的,只有把抽象的编码理念转成具体的流程节点,团队才能真正执行下去。
我个人在实际操作中的体会是,t3code 听起来像一套评判标准,但用久了更像一种思维习惯。它不会直接帮你写出更炫的技术方案,但会让你在拿到任何一个需求时,自然而然地把“能跑、能读、能交付”这三层过一遍。很多时候,代码质量的提升不是靠某个惊天动地的重构,而是靠这些看似平凡的默认动作。最后再分享一个小技巧:从今天开始,每次写完代码提交之前,给自己留十分钟,只做一件事——以“三个月后的陌生同事”的视角读一遍自己的 diff。就这一个习惯,坚持一个月,你收到的 Review 打回意见会肉眼可见地变少。