资讯详情

资讯详情

t3code三层代码组织法:从概念到落地的工程实践指南

1. 从“t3code”这个代号说起它到底指什么第一次看到“t3code”这个词很多人会一头雾水。它不像“Python入门”“Docker实战”那样自带说明更像是一个圈内人才懂的暗号。我最初接触这个代号是在一个做前端组件库的朋友那里他管自己那套“三层代码组织法”叫 t3code意思是tier-3 code也就是把代码按三个层级来切分和管理。后来我又陆续在几个不同场景里听到类似叫法有人用它指代“TypeScript 三层架构代码”有人用它命名自己的一套“模板三层生成器”还有人把它当成一个内部项目的代号。所以这里必须先说清楚一件事“t3code”并不是一个官方标准术语它更像是一个约定俗成的代号具体含义取决于你所在的团队和上下文。但不管哪种用法它背后都指向同一个核心诉求——把复杂代码拆成清晰的三层让结构可读、可维护、可复用。这也是我这篇内容要展开的主线。如果你正在被“代码越写越乱、改一处崩三处、新人接手看不懂”这些问题困扰那这篇内容就是写给你的。我会从三层代码的本质讲起拆解每一层该放什么、不该放什么再给出可直接抄作业的目录结构和落地步骤最后分享几个我踩过的坑。不管你是刚入行的新手还是带过几个项目的熟手都能从中找到能立刻用上的东西。提示下文提到的“三层”是一种通用的代码组织思路不绑定任何特定框架或语言。你可以把它套用到前端、后端、脚本工具等各类项目里。2. 三层代码的本质不是分层是分责任很多人一听“三层”第一反应是 MVC、是 Controller-Service-DAO。这没错但 t3code 强调的三层比传统分层更轻、更贴近实际写代码时的思考方式。它的核心不是“层”这个物理概念而是责任边界。换句话说你要回答的问题是这段代码到底在替谁干活2.1 第一层入口层只负责“接活和派活”入口层是整个系统的门面。用户点击、接口请求、命令行输入最先到达的就是这一层。它的职责非常单一接收输入、做最基础的校验、把活派给下一层、把结果返回。注意是“派活”不是“干活”。我见过太多项目把业务逻辑直接写在入口层里一个接口函数几百行校验、计算、查库、拼装响应全塞在一起。这种代码刚开始跑得挺欢等到需求一变你就得在几百行里大海捞针。t3code 的第一条铁律就是入口层不写业务逻辑。举个生活化的类比。入口层就像公司前台访客来了前台登记一下、确认找谁、把人领到对应工位完事。前台不会替技术部写代码也不会替财务部算账。如果前台开始干这些公司就乱套了。入口层具体该做什么我整理了一张表该做不该做解析请求参数复杂的业务计算基础格式校验非空、类型数据库查询调用下一层方法拼装复杂响应结构统一异常捕获与返回直接操作文件或网络日志埋点入口权限的深层判断逻辑这张表看着简单但真正落地时很多人会在“基础校验”和“业务校验”之间犯迷糊。我的经验是格式对不对归入口层业务允不允许归第二层。比如“手机号是不是11位”是格式入口层管“这个手机号有没有被注册”是业务交给第二层。2.2 第二层业务层系统的真正大脑第二层是 t3code 的灵魂所有“这件事该怎么做”的判断都在这里。它不关心数据从哪来、存到哪去只关心规则和流程。比如下单要先校验库存、再锁库存、再生成订单、再扣减这一串流程编排就是业务层的活。业务层最大的价值在于可测试。因为它不依赖具体的数据库、不依赖具体的网络请求你完全可以脱离运行环境用纯函数的方式去验证逻辑对不对。我做过一个统计在一个结构清晰的项目里业务层的单元测试覆盖率能做到 80% 以上而在逻辑散落的项目里这个数字往往不到 20%。业务层还有一个容易被忽视的点它应该对“数据长什么样”有明确的定义。也就是说业务层接收和返回的数据结构应该是清晰的、有类型的。这也是为什么很多团队会把 t3code 和 TypeScript 绑在一起——类型系统能帮你在编译期就发现“传错字段”这类低级错误。2.3 第三层数据层只跟存储打交道第三层负责和外部世界的数据打交道数据库、缓存、文件、第三方接口。它的职责是把数据取出来、存进去并屏蔽底层差异。业务层说“我要一个用户”数据层就去查库、组装、返回业务层说“把这个订单存起来”数据层就写库、处理事务。数据层最忌讳的是“夹带私货”。什么叫私带私货就是在数据层里写业务判断。比如“如果用户是VIP就打八折”这明显是业务规则不该出现在数据层。数据层应该像一个老实巴交的仓库管理员你说存什么就存什么你说取什么就取什么不替你做决定。三层各司其职整个系统的脉络就清楚了。下面这张图用文字描述能帮你快速记住它们的关系入口层 → 业务层 → 数据层正向调用数据层 → 业务层 → 入口层结果返回每一层只和相邻层说话不跨层调用注意跨层调用是 t3code 里最常见的破坏结构的行为。入口层直接调数据层短期看省事长期看就是灾难。3. 目录结构怎么摆把三层落到文件夹上光说概念没用代码最终要落到文件上。t3code 的目录结构有一个基本原则让文件夹的名字直接暴露责任。你打开一个项目不用看代码光看目录就知道哪层在哪。3.1 一个可直接抄的目录模板下面这个结构是我在多个项目里反复调整后沉淀下来的适配大多数中小型项目src/ ├── entry/ # 第一层入口层 │ ├── http/ # HTTP 接口入口 │ │ ├── user.ts │ │ └── order.ts │ ├── cli/ # 命令行入口 │ └── index.ts # 统一导出 ├── domain/ # 第二层业务层 │ ├── user/ │ │ ├── service.ts # 业务逻辑 │ │ ├── rules.ts # 业务规则 │ │ └── types.ts # 业务数据结构 │ └── order/ │ ├── service.ts │ ├── rules.ts │ └── types.ts ├── data/ # 第三层数据层 │ ├── user/ │ │ ├── repository.ts # 数据存取 │ │ └── mapper.ts # 数据转换 │ └── order/ │ ├── repository.ts │ └── mapper.ts └── shared/ # 公共工具谨慎使用 ├── logger.ts └── errors.ts这个结构有几个讲究。第一按业务域切分而不是按技术类型切分。很多项目喜欢建controllers/、services/、models/三个大文件夹结果每个文件夹里塞了几十个不相关的文件。t3code 更推荐按user、order这样的业务域来分每个域内部再分三层。这样你改“用户”相关的需求就只动user文件夹影响面可控。第二shared文件夹要克制。我见过太多项目把什么都往shared里扔最后它变成了一个垃圾桶。我的建议是只有真正跨三个以上业务域、且逻辑稳定的工具才放进去。放之前问自己一句这东西删了会不会有三个地方报错如果只有一个那就别放。3.2 命名规范让文件名自己说话文件名是给人看的不是给机器看的。t3code 在命名上有几条我一直在用的规则业务层的文件用service.ts、rules.ts一看就知道是业务逻辑。数据层的文件用repository.ts、mapper.ts一看就知道是存取和转换。入口层的文件用具体协议命名如http、cli、ws。类型定义单独放types.ts不要和逻辑混在一起。这些命名看起来是小事但实际影响很大。新人接手时看到user/service.ts和user/repository.ts基本能猜到哪个是业务、哪个是数据。如果叫userHelper.ts和userUtil.ts那就只能靠猜了。3.3 依赖方向一条不能破的红线目录摆好了还得管住依赖方向。t3code 的红线是依赖只能从外向内不能反向。入口层可以依赖业务层。业务层可以依赖数据层。数据层不能依赖业务层更不能依赖入口层。为什么这条线这么重要因为一旦反向依赖出现你的代码就变成了“你中有我、我中有你”的意大利面。数据层如果依赖了业务层那你想单独测试数据层就做不到想换一个数据库实现也会牵一发动全身。怎么检查有没有破线一个简单办法是看 import 语句。如果data/目录下的文件 import 了domain/或entry/的东西那就是破线了。我习惯在项目里加一条 lint 规则自动拦截这种反向依赖比人工 review 靠谱得多。4. 落地实操从零搭一个 t3code 项目概念和结构都清楚了接下来是动手环节。我以一个“用户注册登录”的小功能为例带你走一遍完整流程。这个例子足够小能看清三层怎么配合又足够真实能覆盖校验、业务规则、数据存取这些典型场景。4.1 第一步先定义业务层的数据结构很多人写代码习惯从入口层开始先写接口。t3code 建议反过来先从业务层的数据结构开始。因为数据结构是三层之间的“合同”合同定好了上下游才好对接。在domain/user/types.ts里我们定义export interface User { id: string; phone: string; nickname: string; createdAt: Date; } export interface RegisterInput { phone: string; password: string; nickname: string; }这里的关键是业务层的类型不掺杂任何存储细节。比如不要在这里出现row_id、db_index这种数据库字段。业务层只关心“一个用户应该有哪些属性”不关心它在库里怎么存。4.2 第二步写业务规则把判断集中起来业务规则单独放rules.ts好处是规则集中、容易测试、容易复用。注册场景的规则大概有这些export function isValidPhone(phone: string): boolean { return /^1[3-9]\d{9}$/.test(phone); } export function isValidPassword(password: string): boolean { return password.length 8 /[a-zA-Z]/.test(password) /\d/.test(password); } export function canRegister(existingUser: User | null): boolean { return existingUser null; }注意canRegister这个函数它接收的是“已存在的用户”而不是自己去查库。这就是业务层和数据层解耦的体现业务层只做判断不负责取数。取数的事交给数据层业务层拿到结果再判断。4.3 第三步业务服务编排流程有了类型和规则业务服务就可以编排流程了。在domain/user/service.ts里export class UserService { constructor(private userRepo: UserRepository) {} async register(input: RegisterInput): PromiseUser { if (!isValidPhone(input.phone)) { throw new BusinessError(手机号格式不正确); } if (!isValidPassword(input.password)) { throw new BusinessError(密码强度不足); } const existing await this.userRepo.findByPhone(input.phone); if (!canRegister(existing)) { throw new BusinessError(该手机号已注册); } const user await this.userRepo.create({ phone: input.phone, nickname: input.nickname, passwordHash: hashPassword(input.password), }); return user; } }这段代码有几个值得说的点。第一业务服务通过构造函数注入数据层依赖而不是自己 new 一个。这样测试时可以传入 mock 的 repository非常灵活。第二所有业务异常都用统一的 BusinessError方便入口层统一处理。第三密码哈希这种操作放在业务层因为它属于业务规则的一部分不属于数据存取。4.4 第四步数据层实现存取数据层在data/user/repository.ts里实现export class UserRepository { async findByPhone(phone: string): PromiseUser | null { const row await db.query(SELECT * FROM users WHERE phone ?, [phone]); return row ? toUser(row) : null; } async create(data: CreateUserData): PromiseUser { const id generateId(); await db.query( INSERT INTO users (id, phone, nickname, password_hash) VALUES (?, ?, ?, ?), [id, data.phone, data.nickname, data.passwordHash] ); return { id, phone: data.phone, nickname: data.nickname, createdAt: new Date() }; } }这里的toUser是一个 mapper负责把数据库的行对象转成业务层的 User 对象。mapper 的存在就是为了隔离存储结构和业务结构。哪天数据库字段改名了你只需要改 mapper业务层完全不受影响。4.5 第五步入口层收尾最后是入口层在entry/http/user.ts里export async function registerHandler(req: Request, res: Response) { try { const input parseRegisterInput(req.body); const user await userService.register(input); res.json({ code: 0, data: user }); } catch (err) { if (err instanceof BusinessError) { res.json({ code: 1, message: err.message }); } else { logger.error(err); res.json({ code: 500, message: 服务异常 }); } } }入口层只做三件事解析输入、调用业务、返回结果。异常处理也在这里统一收口。整个流程走下来每一层的职责都清清楚楚。5. 那些年我踩过的 t3code 坑结构讲完了但真正落地时坑往往出在细节上。下面这几个坑我几乎在每个项目里都见过有的还反复踩过。5.1 坑一业务层偷偷依赖了框架这是最隐蔽的坑。比如你在业务层里直接用了某个 Web 框架的装饰器或者直接读了req对象。表面上看代码能跑但业务层已经和框架绑死了。哪天你想把这套业务逻辑复用到命令行工具里就发现根本搬不动。我的做法是业务层的 import 里不允许出现任何框架相关的包。业务层只依赖纯 TypeScript 和自定义类型。这条规则用 lint 可以强制检查效果很好。5.2 坑二数据层返回了“半成品”有些数据层为了省事直接返回数据库的原始行对象让业务层自己去取字段。这看起来省了几行代码实际上把存储细节泄露给了业务层。业务层一旦开始写row.phone_number这种字段就说明 mapper 没做好。正确的做法是数据层返回的永远是业务层定义的类型。数据库字段和业务字段的映射全部在 mapper 里完成。多写一个 mapper 文件换来的是业务层的干净和可测试。5.3 坑三入口层做了太多“顺手”的事入口层最容易“顺手”加逻辑。比如“反正这里已经拿到用户了顺便判断一下是不是VIP吧”。这一顺手业务逻辑就散到了入口层。下次另一个入口比如定时任务也要这个判断你就得复制一遍。我的经验是入口层每多写一行业务判断未来就多一处要同步修改的地方。判断逻辑一律下沉到业务层入口层只负责搬运。5.4 坑四shared 变成了垃圾场前面提过shared要克制。我见过一个项目shared里有 40 多个文件从日期格式化到权限判断全都有。结果就是谁都不敢删谁都不敢改因为不知道哪里在用。我的建议是新建 shared 文件前先问三个问题。第一这东西是不是三个以上业务域都在用第二它的逻辑是不是足够稳定、不会频繁变第三它是不是真的没有业务含义三个都答“是”才放进去。否则就放在使用它的业务域里。5.5 坑五为了分层而分层最后一个坑最要命把简单的事情搞复杂。一个只有三个接口的小工具非要拆成三层十几个文件维护成本反而更高。t3code 是手段不是目的。当项目小到一层就能写清楚时就别硬拆。等它长大了、变乱了再重构也不迟。我的一般判断标准是当入口层文件超过 200 行或者同一个业务逻辑在两个入口里重复出现时就该考虑拆三层了。6. 怎么验证你的 t3code 拆对了拆完之后怎么知道拆得好不好我总结了几个自检问题你可以拿自己的项目对照一下。6.1 三个自检问题问题一能不能脱离运行环境测试业务逻辑如果你的业务层测试需要启动数据库、需要发 HTTP 请求那说明业务层和数据层、入口层还没解耦干净。理想的业务层测试应该只需要传入 mock 的 repository 就能跑。问题二换一个数据库实现要改几个文件如果只改data/目录下的文件说明隔离得好。如果业务层、入口层都要跟着改那说明存储细节泄露了。问题三新人能不能在半小时内找到“注册”逻辑在哪如果他能顺着entry/http/user.ts→domain/user/service.ts→data/user/repository.ts一路找下去说明结构清晰。如果他找了半天还在猜那目录结构就有问题。6.2 一个实用的重构顺序如果你手上的项目已经乱了想往 t3code 上靠别想着一次性重构完。我的建议是按业务域逐个迁移顺序如下先挑一个最简单、最独立的业务域比如“配置管理”。把这个域的业务逻辑从入口层抽出来放到domain/下。把数据存取抽到data/下补上 mapper。入口层改成只调用业务层。跑通测试确认行为不变。再挑下一个业务域重复以上步骤。这样每次改动范围可控出问题也容易回滚。我一般一个业务域花半天到一天几个迭代下来整个项目就清爽了。6.3 关于测试的一点心得t3code 的结构天然适合测试。业务层用 mock repository 测规则数据层用测试库测存取入口层用集成测试测端到端。三层测试各管一段互不干扰。我特别想强调的是业务层的测试要写得像文档。每个测试用例的名字应该直接描述一条业务规则比如“手机号已注册时应该拒绝注册”。这样测试跑一遍业务规则就过了一遍比看代码还直观。7. 关于 t3code 的一点个人体会写了这么多最后说点掏心窝的话。t3code 这套三层思路本质上不是什么高深技术它就是一种把代码放对位置的习惯。难的不是理解是坚持。项目赶进度的时候最容易“顺手”把逻辑写在入口层需求变的时候最容易“临时”在数据层加个判断。每一次妥协都是在给未来的自己挖坑。我自己带团队时会把 t3code 的几条红线写进代码规范用 lint 工具自动检查。刚开始大家会觉得麻烦但跑上一两个月等新人接手时能快速上手、等改需求时能精准定位大家就体会到好处了。结构这东西平时看不出价值一到项目变大、人员变动的时候差距就出来了。如果你现在手上的项目还小别急着上三层先把入口层和业务逻辑分开就行。如果你手上的项目已经乱了也别慌按业务域一个个迁慢慢来。代码结构的改善从来不是一蹴而就的但只要方向对了每改一处都是赚的。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →