☰
t3code:用Type、Trace、Test打造可演进的代码质量体系
2026/10/13 17:13:47 网站建设 项目流程

做技术重构这半年,我一直在琢磨一件事:怎么用一句话告诉组里的新人,什么代码算“好代码”。

前几天翻团队沉淀的文档,看到“t3code”这个代号,突然想写点东西聊聊它。它不是一个框架,也不是某个开源库,而是我们内部对“第三级代码”的统称:代码质量从“能跑”到“能改”再到“能演进”,分三档,t3code 就是最高那一档的约定。以前我们总说“代码乱”“不好维护”,但每个人对“乱”的理解都不一样。后来我把这套标准拆成了三条明确的约束,组里新同学照着做,上手快了不少。这篇就讲讲它到底是什么、怎么落地、以及我踩过的几个坑。


1. t3code的整体设计与拆分思路

1.1 名字的由来:为什么叫 t3code

t3code 不是拍脑袋起的名字。它背后对应三个英文字母开头的单词,恰好都是 T:

  • Type:类型是否是明确的。变量、函数参数、返回值的类型,有没有被说清楚。
  • Trace:运行链路是否可追踪。一个请求进来,一个任务跑完,中间发生了什么,能不能从日志或数据里还原出来。
  • Test:关键行为是否被测试覆盖。改动一个函数之后,能不能通过一条命令确认“原来好的功能没有被改坏”。

把这三个词的首字母连起来,就是 T3。再加上 code,就成了 t3code。

这和我以前写的代码完全是两套路子。早些年我追求“代码少”,一个函数恨不得塞下所有逻辑,类型能不写就不写,日志能省就省。表面上看文件很短,真出了事故,查日志查半天也定位不到根因。后来我把“少写代码”调整为“写容易推理的代码”,t3code 就是从那次转变里沉淀出来的。

之所以把 Type、Trace、Test 拧成一股绳,是因为它们三个其实是互相成就的。类型写清楚了,调用方就不需要猜测“这个参数到底是字符串还是数字”,出错的概率降低;链路日志打完整了,线上问题能快速定位到具体分支,修 bug 的时间从几小时压缩到十几分钟;测试覆盖到位了,重构时才敢大胆改内部实现,因为回归有兜底。单看任何一条都不稀奇,但把它们当作一个整体来要求,代码的“可维护性”就变成可以检查、可以打分的东西了。

1.2 三档代码等级:能跑、能改、能演进

我习惯把代码分为三个等级,和 t3code 名字里的“3”刚好对应:

等级名称特征典型表现
C1可运行功能跑得通,逻辑正确本地功能测试通过,但只有写代码的人敢动它
C2可维护别人也敢改,改了不慌变量命名清晰,函数边界明确,改动后能快速验证
C3可演进新增需求时不需要推翻重来类型约束、链路追踪、测试覆盖三者齐全,新功能是“叠加”而不是“重写”

t3code 的目标不是停留在 C1,而是把人带到 C2,再慢慢往 C3 推。

说实话,很多生产环境的代码长期停留在 C1。功能验证过了,交付了,上线了,但下一次迭代时发现,想加一个字段,要连带改七个地方;想拆一个函数,发现它的日志和业务逻辑缠在一起;想跑一下测试,发现测试依赖外部的真实环境,没法在本地稳定通过。这些问题不会在“能跑”阶段暴露,全都在“要改”的时候爆发。t3code 想解决的,就是这种爆发发生的概率。

1.3 它不解决什么问题

任何方法都有适用边界,t3code 也不是银弹。

  • 不适合一次性脚本。如果是临时跑一遍的数据清理、环境排查,类型写得太严反而是负担,这类场景“能跑”就够了。
  • 不适合纯算法实验。还没想清楚方案时,快速验证思路比约束风格更重要;等思路固定再补类型和测试,效率更高。
  • 不适合交接前的突击美化。有人会在项目快交接时花一周时间给代码做“表面整容”,变量名改顺眼,加几个注释。t3code 强调的是日常习惯,不是最后一刻的粉饰。靠补出来的规范,往往在第一次迭代时就会被打破。

明确了这些边界之后,再来看 t3code 的三个核心细节,就有抓手了。


2. t3code三大核心细节解析

2.1 T1 Type:用类型把“隐式约定”变成“显式约束”

类型是最容易被忽略、又最能降低理解成本的东西。

我接手过一个模块,里面大量使用data这个变量名,有时它是字符串,有时是对象,有时又是数组。调用方靠读代码猜类型,猜错了就出 bug。后来我用 TypeScript 重写,把整个模块的数据流重新梳理一遍,定义出清晰的接口,这类问题直接消灭了。

举一个最简单的对比。

重构前,一个查询函数可能是这样的:

function getOrderDetail(id) { if (!id) return null; const data = fetch(`/api/order/${id}`); if (data && data.status === 'success') { return data.result; } return null; }

这段代码有三个隐患:id到底是字符串还是数字,没人知道;data的形态不固定,前一步是响应包装,后一步是业务数据;status是魔法值,拼错一个字母就会静默走失败分支。

用 t3code 的思路改一下:

interface ApiResponse<T> { status: 'success' | 'fail'; result: T; } interface OrderDetail { orderId: string; amount: number; createTime: string; } function getOrderDetail(id: string): OrderDetail | null { if (!id.trim()) return null; const response = await fetchOrderApi(id); if (response.status !== 'success') return null; return response.result; }

改动点不多,但效果很明显:id被限定为字符串,调用方传数字会直接报编译错误;ApiResponse<T>把响应包装和业务数据分开;status只有'success' | 'fail'两种取值,不会再出现拼错字符串的问题。

这条约束背后的逻辑是:人脑的记忆是有限的。三个月的代码,自己都要重新读一遍;如果类型信息写在代码里,读代码时不需要从几十行之外往回翻找赋值逻辑。类型就是一份“看得到的数据字典”。

2.2 T2 Trace:让每一次状态变化都有迹可循

代码出 bug 不可怕,可怕的是不知道它在哪个环节出的问题。

t3code 对链路追踪的要求不是“多打日志”,而是“在关键节点留下可拼接的痕迹”。我见过一种反面案例:所有日志都用console.log('data is ', data),没有请求 ID,没有时间戳,没有函数名,出问题之后翻日志等于在沙子里找针。

一个可追踪的系统至少要有三个层次:

  1. 请求入口生成唯一标识。无论是 HTTP 请求、消息队列消息还是定时任务,每次执行都生成一个 traceId,并把它往下游传递。
  2. 每个关键步骤打印结构化的上下文。包括当前函数名、入参摘要、耗时、结果状态。
  3. 错误链路上保留原始错误信息。catch到错误后,先记录再重新抛出或处理,不要吞掉异常。

举例来说,我习惯这样写日志:

async function handleRefund(orderId: string, amount: number, traceId: string) { logger.info('refund_start', { traceId, orderId, amount }); try { const result = await refundService.apply(orderId, amount); logger.info('refund_success', { traceId, orderId, refundId: result.refundId }); return result; } catch (error) { logger.error('refund_failed', { traceId, orderId, error: error.message }); throw error; } }

每一条日志都带 traceId,线上排查时按这个 ID 一搜,整个调用链就串起来了。哪里开始、哪里结束、哪一步出错,一目了然。

我还给组里定过一条规矩:不允许出现空 catch。以前有人写:

try { sync(); } catch (e) { // ignore }

看起来安全,实际上把问题全藏起来了。t3code 要求至少要有一条logger.warn,哪怕不加修饰,也要把“这里吞了一个错误”这个事实说出来。很多线上事故,最初都是从一个空 catch 开始的。

2.3 T3 Test:为关键行为织一张回归网

t3code 里的 Test,不是追求行覆盖率 100%,而是要求“关键路径必须有测试”。

什么算关键路径?用户支付成功后的回调流程、库存扣减的并发逻辑、状态机流转、金额计算的分支,这些地方一个逻辑错位就是事故。至于工具函数、纯展示组件,优先级可以往后放。

一份测试设计可以这么组织:

测试对象关注点测试类型
状态机流转每个合法状态迁移是否到达目标状态单元测试
金额计算精度、四舍五入、负数与零值单元测试
超时与重试第三方调用超时是否走 fallback集成测试
异常路径重复请求、幂等处理是否符合预期集成测试

另外,测试要尽量做到“本地可跑通”。以前我们团队测试依赖测试环境的数据库,环境一挂,全组没法回归。后来把依赖 mock 掉,测试 3 分钟跑完,每次提交前都能刷一遍。


3. t3code实操过程:落地到一个订单模块

3.1 起点:一份典型的“能跑但难改”代码

前阵子重构老订单模块,当时我挑了一个典型页面,功能很简单:根据订单号展示订单详情。原代码长这样:

function detail(id) { const r = request('/api/order/' + id) if (r.state == 1) { render({ title: r.info.title, price: r.info.price / 100, name: r.info.name || 'anonymous', }) } else if (r.state == 2) { showError('已取消') } else { showError('加载失败') } }

问题一目了然:state == 1、state == 2是魔法数字;price / 100的换算逻辑埋在渲染函数里;name字段为空的默认值处理散落在多个位置。功能是好的,但维护文档为零。

3.2 重构步骤:按 t3code 的节奏来

我没有一次性把所有地方改完,而是分了六步走:

第一步:确认数据契约。

把后端返回的字段画出来,整理成一个 TypeScript 接口。订单状态不再是数字,而是明确的枚举值。

enum OrderStatus { Pending = 'pending', Paid = 'paid', Cancelled = 'cancelled', TimedOut = 'timed_out', } interface OrderDetail { id: string; title: string; priceInFen: number; buyerName?: string; status: OrderStatus; }

这里注意到我把金额字段起名为priceInFen,单位直接暴露在名字里,谁看到都不会再犯“把分当元”的错。这是从一次线上事故换来的教训,后面会细说。

第二步:抽取纯函数。

状态判断、金额转换都是纯逻辑,不依赖框架,把它们抽出来单独测试。

function formatPrice(fen: number): string { return (fen / 100).toFixed(2); } function getDisplayName(name?: string): string { return name && name.trim() ? name : 'anonymous'; }

第三步:接日志链路。

在获取详情、渲染成功、渲染失败三个节点加上带 traceId 的日志。这一步开始,排查问题有了抓手。

第四步:补测试。

先给纯函数加,再给页面状态切换的渲染逻辑加。测试跑通之前,不进入下一步。

第五步:替换调用方。

把原来页面里的逻辑全部切到新接口,删除旧的detail函数,不让旧代码和新代码并存。

第六步:持续集成。

把测试命令挂到 CI 流水线,每次推代码自动跑一遍,不通过就拦住合并。

六步做完之后,那个页面的代码长成了下面这样:

class OrderDetailPage { constructor(private api: OrderApi, private logger: Logger) {} async renderDetail(orderId: string): Promise<void> { const traceId = generateTraceId(); this.logger.info('render_start', { traceId, orderId }); const order = await this.api.getOrderDetail(orderId); if (!order) { this.logger.warn('order_not_found', { traceId, orderId }); this.showError('订单不存在'); return; } this.logger.info('render_success', { traceId, orderId, status: order.status, }); this.showDetail({ title: order.title, price: formatPrice(order.priceInFen), name: getDisplayName(order.buyerName), }); } }

3.3 落地效果与量化数据

这个模块重构完,我把改动前后做了一次对比:

指标重构前重构后
文件行数86102
状态值表示1、2、3 魔法数字OrderStatus 枚举
日志无每次渲染有完整 traceId 链路
测试023 个用例
新增字段时改动点可能涉及 7 处只需要改接口定义 + UI 映射

行数没有减少,甚至多了 16 行。但这是正常现象:类型定义、日志、枚举声明都需要代码量。很多人用“代码行数少”来衡量代码好坏,在 t3code 里,这反而是一根错误标尺。关注点应该是“下一次改需求时,需要动几个地方”。

一个月后,新需求来了:订单列表要显示优惠后的价格。团队里的同事只改了一行映射,跑了三分钟测试,直接上线了。那一刻我对 t3code 的价值更笃定了一些。


4. 常见问题与排查技巧实录

4.1 五个高频问题与解法

t3code 推行过程中,组里几乎每个人都在相同的地方栽过跟头。我把高频问题整理成了一张表:

问题现象根因解法
类型名太抽象,读不懂data、res、obj满天飞图省事,没认真设计领域概念类型名用业务词汇,宁可写长一点
日志打太多,反而没法查全链路每行都是 info,刷屏没有区分级别info 只留关键节点,错误用 error,详情用 debug
测试太脆,小改动就跑红改了一个文案,测试挂了测试断言了 UI 细节,不是行为断言只锁定业务结果,不锁定展示样式
金额单位混用元/分经常混淆字段名没写明单位字段名带单位,或用专门类型标记
重构成瘾,改动面失控本来只是加字段,结果把模块重写了顺手改一切,导致风险放大每次改动限定范围,不相关代码不动

4.2 用简单命令给代码“体检”

在没上严格检查工具之前,我习惯用几个简单的命令定期扫一遍代码,把明显的问题揪出来:

# 找出代码里的魔法数字(3位以上数字字面量) grep -rEn "\b[0-9]{3,}\b" src --include="*.ts" --include="*.js" | grep -v test # 找出空 catch grep -rEn "catch\s*\([^)]*\)\s*\{\s*\}" src --include="*.ts" --include="*.js" # 找出裸 console.log(无上下文) grep -rEn "console\.log" src --include="*.ts" --include="*.js" | grep -v traceId

这些不能替代类型检查器和 linter,但胜在零成本、随时能跑。每周扫一次,比自己凭感觉 review 靠谱多了。

4.3 推行 t3code 的几个“反直觉”经验

第一条:不要一开始就要求 100% 达标。如果强迫所有代码立刻满足规范,团队会在情绪上反弹。我采取的办法是“新代码必须达标,老代码逐步改造”,改造时优先挑线上事故最多的模块。

第二条:测试不能只求绿。有一次我们的 CI 全绿,但某个测试断言的是数据库里的旧字段,业务代码早就不走了。测试跑过,不等于测试有效。我后来会让每个测试简答一个问题:“我在保护哪个行为?”如果答不上来,就删掉它。

第三条:代码评审时不要揪细节用词。有人把 type 命名为data_info,有人命名为orderInfoData,没必要统一拘泥于某种风格。只要类型清晰,不产生歧义,就放行。评审的重点是 T1、T2、T3 三条主线上是否达标,而不是纠结下划线风格。


5. 把t3code从个人习惯变成团队规范

5.1 用一份 PR 清单代替口头要求

只靠理念没法约束行为。我后来做了一张 Pull Request 自检清单,每次提交代码都对照着过一遍:

  • 我新增的类型名是否表达业务含义?
  • 我的函数参数是“看得懂”的还是“猜得着”的?
  • 这条链路如果出问题,我能否靠日志定位到函数?
  • 我修改的关键逻辑,有没有对应测试用例?
  • CI 是否跑通?本地是否跑得更快?

这份清单不追求华丽,但它把 t3code 从抽象概念变成了可勾选的流程。团队里每个人用同样一把尺子量代码,讨论问题时就不再是“你觉得难看”“我觉得还行”这种主观争论了。

5.2 与 AI 工具协同

最近和 AI 结对编程,我发现也可以把 t3code 写成一段 prompt 前缀。给它规定好输出风格,效果会稳定很多。比如:“请编写函数,参数和返回类型使用显式 interface,关键步骤打印结构化日志,核心逻辑附带单元测试,避免魔法数字和空 catch。”

AI 写出来的初稿,再按 t3code 的清单过一遍,能省不少时间。但要注意:AI 生成的类型名经常是通用的Result、Detail,需要人来改造成业务词汇。工具提供雏形,质量判断还是在我们自己手上。

5.3 最后一个小技巧:用“半年后”视角写代码

写代码的时候多问自己一句:“半年后的我或同事,只看到这一块代码,能不能知道它想干什么?”

这个视角很朴素,但它能逼着我在写类型、日志、字段名时多想一层。我见过很多精致的架构,最终输给了几个月后无人能懂的命名。t3code 不是什么高深的理论,它只是把“替未来的人着想”这件事,变成了三条可执行的检查项。坚持下来,代码的寿命会明显变长。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询