t3code:用三层编码规范根治项目混乱与代码治理

发布时间:2026/10/9 18:49:01
t3code:用三层编码规范根治项目混乱与代码治理 t3code 这个词第一次出现在我工作笔记里的时候其实就是个随手起的内部代号三层、三个字母、一套编码约定。真正让我决定把它叫出来的原因是团队仓库已经乱到“找文件比写代码还累”脚本到处是临时目录接口逻辑散落一地每个人都在用自己的习惯命名。后来我把这套以三层结构为核心的代码治理实践整体命名为 t3code用来指代一套明确的工程边界和技术规范。这篇文章就是我对 t3code 的完整复盘它解决什么问题、目录怎么切、命名怎么定、最小链路怎么跑通、落地时哪些坑躲不掉。无论你是在单人项目里整理代码还是接手一个老仓库想立规矩这套思路基本都能直接用。1. 为什么给项目代号 t3code三层编码思路的由来先聊清楚“为什么”。t3code 不是一个框架也不是某个第三方库它是我给项目组定的一套编码组织方式。核心逻辑非常简单任何一份代码你都得能在三秒钟内说清楚“它属于哪一层、它是什么类型、它服务哪个业务模块”。如果说不清楚那这份代码就不该出现在这个位置。我见过太多项目失控的过程。最开始只是多放一个工具函数没人觉得有问题后来有人在 controller 里写 SQL在 service 里拼 HTML在 utils 目录下堆了三屏没人敢删的“公共方法”。等你想重构的时候最大的阻力不是技术债而是没人能说清楚哪些代码可以动、动了会影响什么。t3code 的第一层意义就是解决这个“归属感”的问题。1.1 把 T3 拆成三把尺子而不是三个层级名词很多人听到“三层架构”第一反应是 controller、service、dao 三个词。但 t3code 里的 T3我更多是把它理解成三把检查尺层Tier、类型Type、归属Tag。这样拆完以后判断一段代码怎么写就变成三个连续问题这段代码属于哪一层接入层、业务层还是数据层这个文件承担什么类型路由、控制器、服务、仓储还是领域对象它服务哪个业务模块用户、订单、支付还是系统公共基础这三个问题依次回答完文件和目录的摆放就自然出来了。比如“用户模块的登录接口”它的文件名里面需要能体现这是用户业务它是控制器类型它属于接入层。你不需要记住三条规范只需要记住这三个问题。这套思路对单人项目可能显得有点“重”但它的好处是当项目从 3 个文件变成 300 个文件时你不需要靠记忆认出每个文件是干什么的。路径和命名本身就是坐标系。1.2 它想解决的三类编码失控我在实际落地过程中发现绝大多数项目失控都可以归成三类t3code 也就是冲着这三类去的。第一类是文件失控。今天加一个utils.ts明天加一个utils2.ts后天再来一个final/目录。文件本身没有位置感最后就是谁的代码都找不到。第二类是依赖失控。跨层调用、循环引用、一个 service 里塞了五层调用代码编译能过但一启动就报 undefined排查要花掉半天。第三类是协作失控。提交信息乱写、分支命名随意、review 的时候只知道 diff 很大但不知道改的是什么。这三类失控不是靠纪律能长期压住的必须靠一套可视化的边界去兜底。2. 骨架先行三层目录结构与模块边界怎么定无论你从零搭建仓库还是给老项目做治理我都建议先立骨架再写业务。骨架就是一套明显的目录结构它是后续所有规则的基础。下面这套结构以 Node TypeScript 为例我用 t3code 组织一个典型的后端仓库。如果你是 Java、Go甚至前端项目核心思想都一样只是命名可以换成自己熟悉的 controller、service、repository。t3code/ ├─ src/ │ ├─ interfaces/ # 接入层路由、控制器、参数校验 │ │ ├─ routers/ │ │ ├─ controllers/ │ │ └─ validators/ │ ├─ applications/ # 业务层用例、服务、领域逻辑 │ │ ├─ services/ │ │ └─ domain/ │ └─ infrastructures/ # 数据层仓储、模型、数据库访问、配置 │ ├─ repositories/ │ ├─ models/ │ ├─ db/ │ └─ config/ ├─ tests/ ├─ .eslintrc.cjs ├─ package.json └─ README.md你可能会问为什么不用 controller、service、dao 这种最常见的命名我的理由是dao 这个词很容易让新人误解成“所有跟数据有关的东西都塞里面”而 t3code 想强调的层级边界是接入层只负责请求和响应业务层只负责流程和规则数据层只负责存取和映射。interfaces、applications、infrastructures 这三个词会逼迫你思考当前代码的职责。2.1 一个示例骨架的说明为什么只切三大块很多项目一上来就分几十个目录每个模块再套一层 M V C结果新同学刚到团队光是理解目录都要讲一小时。t3code 只切三大块是因为分层是对“依赖方向”的约束而模块拆分是对“代码归属”的约束两者不要混在一起。拿用户模块举例。用户模块不是一个目录而是散在三层里interfaces/controllers/user、applications/services/user、infrastructures/repositories/user。这样切以后你看到一个问题先能判断它属于哪一层。比如“登录接口返回 500”问题可能出在参数校验层、业务逻辑层或者数据库映射层排查范围一下子小了一半。2.2 边界规则可以向下调用但严禁绕过上层骨架只是形式边界规则才是灵魂。t3code 只定一条硬性规则依赖方向必须由上往下同层之间允许调用但绝不能跳过中间层。具体来说controller 可以调用 service也可以调用同层的另一个 controller很少用。service 可以调用 repository也可以调用同层另一个 service。repository 不允许反向调用 controller。任何一层都不能直接操作数据库连接池除了 infrastructure 层。听起来是很基础的规则但我在真实代码里见过太多破例controller 直接发 SQL、service 里直接写 HTTP 响应、repository 里做金额计算。破例一次不一定会马上出问题但它会让后续的人觉得规则可以随意打破等破例积累到一定程度边界就彻底失效了。3. 命名与协作规范让路径和提交记录自带坐标系有了目录骨架之后下一步就是命名。我从经验里得出一个教训如果文件名不能代表职责那唯一能代表职责的就是代码注释而代码注释是不可信的。所以 t3code 的第二层规范抓的就是命名。3.1 文件与接口命名一眼辨认层和职责先说文件命名。接入层文件通常用资源名加后缀user.controller.ts、user.router.ts、user.validator.ts。业务层文件通常用行为动词create-order.service.ts、cancel-order.service.ts。数据层文件通常用实体名加仓储类型user.repository.ts、order.mapper.ts。这样做的好处是你在文件树里扫一眼马上就知道这个文件是干什么的。然后是接口或类命名。我建议保留一套一致的前后缀不要混着用。比如 controller 类统一以Controller结尾service 类统一以Service结尾repository 类统一以Repository结尾。别一会儿叫UserRepo一会儿叫UserRepository这些细节看着小却是真正影响代码检索效率的地方。还有一个容易被忽略的点DTO 的命名要带上场景。不要出现一个叫UserData的东西它到底用在接入层还是数据层我建议接入层入参用UserLoginRequest返回用UserLoginResponse数据层的实体就叫UserEntity或UserModel。命名一旦做到“场景可见”整个团队的沟通成本会明显下降。3.2 提交信息、分支和评审的 t3 写法代码层面的规范只是第一部分。t3code 里最容易被低估、但我实际用下来收益最大的一部分是 Git 协作规范。如果代码是仓库的骨架那提交历史就是这具骨架的病历本。病历本写不清楚后面任何人排查问题都得考古。分支命名我推荐统一成t3-业务模块-行为-变更摘要比如t3-order-fix-amount-precision、t3-user-add-register。一眼就知道这个分支在干嘛。提交信息我也做了统一格式t3-fix(order): 修复订单金额精度丢失 t3-feat(user): 新增用户注册接口 t3-refactor(infrastructure): 抽取统一数据库连接“t3”前缀代表这是经过边界规则确认的变更后面带层级和模块标签。这样你执行git log --grept3-fix(order)的时候就能快速筛出该模块所有修复记录。这个习惯在复盘线上事故时特别管用。4. 从骨架到可跑链路一个最小请求的完整落底说完了骨架和命名接下来我带你完整走一遍从零启动一个 t3code 项目并且把一个“查询用户资料”的最小请求跑通。你不需要完全照抄技术栈它更像一个流程模板重点看请求是怎么在层里流动的。4.1 初始化目录并安装基础依赖假设你已经装好了 Node 和 pnpm先建立空目录并初始化mkdir t3code cd t3code pnpm init -y mkdir -p src/interfaces/routers src/interfaces/controllers src/applications/services src/infrastructures/repositories src/infrastructures/models这是我习惯的第一步先建目录再装依赖。原因很简单目录一旦固定后面新增文件的位置就不用思考了。接着安装一个最小 Web 框架和数据库驱动这里我以 Express 和 mysql2 为例pnpm add express mysql2 pnpm add -D typescript types/express ts-node如果你用的是别的语言这一步可以换成 Spring Initializr 或者 Go 的 project layout后面的代码示例在思想上一样。4.2 最小示例用户资料查询的三个文件怎么分工我写一个极简的用户资料查询链路。接入层只做两件事接收 HTTP 请求把业务结果返回给客户端。// src/interfaces/routers/user.router.ts import { Router } from express import { UserController } from ../controllers/user.controller const router Router() router.get(/users/:id, UserController.getProfile) export default router// src/interfaces/controllers/user.controller.ts import { Request, Response } from express import { UserService } from ../../applications/services/user.service export const UserController { async getProfile(req: Request, res: Response) { const userId req.params.id const profile await UserService.getProfile(userId) res.json(profile) }, }业务层只负责流程判断不感知 HTTP 细节也不直接写 SQL。// src/applications/services/user.service.ts import { UserRepository } from ../../infrastructures/repositories/user.repository export const UserService { async getProfile(userId: string) { const profile await UserRepository.findProfileById(userId) if (!profile) throw new Error(用户不存在) return profile }, }数据层只做数据映射和存取。// src/infrastructures/repositories/user.repository.ts import { db } from ../db export const UserRepository { async findProfileById(id: string) { const rows await db.query( SELECT nickname, bio FROM user_profiles WHERE user_id ?, [id], ) return rows[0] }, }这段代码本身很简单真正重要的不是代码量而是依赖方向。controller 引用 serviceservice 引用 repository没有任何一跳是反向的。这就是 t3code 希望在你的项目里看到的基本形态代码能跑通依赖也能看清。4.3 提交前自查清单每次提交前我会让自己和团队成员过一遍四个问题可以说是 t3code 的“最小检查铃”这个文件是不是放到了它所属的层文件名是否体现了职责有没有跨层调用或者反向依赖提交信息里是否带了模块和层级标签这四个问题全部回答“是”代码再丑也不会给团队带来灾难性的维护成本。回答里任意一个“否”那就先整改再提交。5. 实际落地时最常撞到的 5 个故障与排查技巧再好的规范落到真实项目里都会遇见各种意外。下面这五个问题是我在推 t3code 过程中真实踩过、也帮团队成员排查过的你可以把它们当成一份便宜买的经验清单。5.1 故障速查与对应解法症状原因解法项目启动时报 undefinedcontroller 反向依赖 repository或者存在循环引用锁定依赖方向打破循环引用优先让三层链条保持单向工具方法找不到地方放没有公共归属层越放越乱建立shared/utils或common层但严格禁止业务逻辑落地提交历史无法回溯提交信息随意没有模块和层级标签统一提交格式分支命名也带上模块同一个实体命名不一致没有命名规范各写各的固定后缀统一DTO 按请求/响应分场景命名controller 越来越肥违背边界规则把业务逻辑塞进接入层把判断逻辑下沉到 servicecontroller 只做转发和包装这张表基本覆盖了我见过的大部分团队混乱场景。你不需要一开始就把所有规范都铺开只需要先挑表格里的第一行去治根把循环依赖和反向依赖打掉很多症状会自行消失。5.2 两个一直有用的排查命令在实际排查过程中有两组命令是我长期在用的建议你直接收藏。第一组是检查依赖方向。在 TypeScript 项目里我会用madge扫描循环依赖npx madge --circular --extensions ts src/如果输出非空说明你的目录结构里存在循环引用逐条去拆。拆的时候先定方向依赖只能从上往下如果两个模块确实互相需要那多半是边界切错了。第二组是检查提交信息。回查历史记录时我会配合git log的搜索功能git log --grept3-fix(order) git log --grept3-feat(user)这个命令帮我做过的线上事故复盘次数数不清。看到一个订单金额不对先搜订单模块的所有修复记录再去看具体 diff效率比逐屏翻日志高得多。还要提醒一个老生长谈的点不要把“先跑通再治理”变成“先跑通就不治理”。初期你可以允许少量破例但要么当时就补一张 tech-debt 卡片要么在下次提交时顺手修掉。破例一旦没人跟踪规范就会变成一句口号。6. 影响范围复盘从代码层到团队协作层的连锁变化最后说说 t3code 实际落地以后影响范围到底蔓延到了哪里。我原本以为它只是改动代码目录但后来发现它把团队协作的节奏也一起带顺了。6.1 这些变化会落到哪些可观察的地方结构上最直观的变化是新成员上手时间变短了。以前一个新同学进组要看一周代码才能找到“订单金额在哪里计算”。有了 t3code 之后他看到路径里写着order再去applications/services/order找业务逻辑几乎不用人带。代码评审也变了。以前 reviewer 遇到一个大 PR 只能从头硬看现在可以先看变更路径属于哪一层、提交信息带了什么标签再决定重点看哪里。如果一批 commit 里混了三层马上就能判断这不是一次“整洁变更”可以打回。线上排障效率也受到直接正向影响。事故复盘时第一件事就是看这个接口经过了哪几层。哪一层抛异常哪一层去修边界清晰之后责任范围自然清晰。这是一个组织结构问题而不只是代码问题。6.2 踩过几轮坑之后我对三层结构的最终想法如果让我浓缩成一句话我会说t3code 不是要求你把所有项目都做成教科书式的三层而是要求你在下笔之前先回答“这段代码归谁管”。我早期犯过一个错为了“架构干净”把 service 又拆了五层结果同事改一个字段要翻五个文件那比没有分层更痛苦。所以我现在执行 t3code 时给自己定了一个很朴素的底线层数可以少边界必须清晰规范可以小执行必须一致。真正能帮你长期维护下去的不是最先进的设计方法而是团队愿意共同遵守的一套“最小约定”。我现在的习惯是每次往仓库里提交代码前先默念一遍 t3code 三个问题它属于哪一层它是什么类型它服务哪个模块三个问题回答清楚了代码就能放提交也敢发。这个习惯我在个人项目和团队项目里都试过节省下来的花在“找代码”“猜意图”“考古提交”上的时间远远超过当初理解这套规则所花的时间。