☰
t3code 三层代码结构实践:从目录划分到层间通信的工程指南
2026/10/28 16:03:00 网站建设 项目流程

1. 从“t3code”这个代号说起:它到底指什么

第一次看到“t3code”这个词,很多人会下意识地把它当成某个开源库的名字,或者某个内部项目的代号。我最初也是这么理解的,直到后来在几个不同的技术社群里反复看到有人拿它当话题,才意识到这个词的指向其实比想象中要模糊得多。它不像“React”“Redis”那样有明确的官方定义,也不像某些框架有清晰的版本号和维护团队。换句话说,t3code更像是一个被社区口口相传、逐渐沉淀出共识的“实践标签”,而不是一个可以一行命令装好的具体软件。

那它到底指什么?从我接触到的语境来看,t3code通常被用来描述一种以“三层结构”为核心组织逻辑的代码实践方式。这里的“t3”不是某个单词的缩写,而是“tier 3”或者“three-tier”的口语化写法,而“code”则强调它最终要落到具体的代码组织上。合在一起,它描述的是一套关于“代码应该怎么分层、层与层之间怎么通信、边界怎么划定”的工程习惯。你可以把它理解成一种轻量级的架构约定,而不是一个重型框架。

为什么这个词会流行起来?我的判断是,它恰好踩中了很多团队在项目从“能跑”走向“能维护”这个阶段的痛点。项目初期,大家往往把所有逻辑塞进几个文件里,改起来快、跑起来也快。但当代码量涨到几千行、参与的人从两个变成五个,问题就来了:改一个接口,三个地方跟着崩;加一个字段,要翻遍整个仓库。这时候团队需要的不是一套复杂的微服务拆分,而是一个足够轻、足够直观、能立刻上手的代码组织方式,t3code就是在这个缝隙里被反复提及的。

它适合谁?我认为有三类人最值得花时间了解它。第一类是刚接手一个中型项目、被混乱的代码结构折磨得想重写的人,你需要一个不激进但有效的整理思路。第二类是带小团队的技术负责人,你需要一套能让新人快速看懂、又不至于过度设计的约定。第三类是自己写 side project 的独立开发者,你不想引入重型框架,但又希望代码在半年后自己还能看懂。这三类人的共同点是:要的是秩序,不是仪式。

需要提前说明的是,t3code并没有一个官方规范文档,我下面讲的所有内容,都是基于我在多个项目里反复实践、踩坑、调整之后总结出来的。它不保证是“唯一正确”的做法,但可以保证是“在真实项目里跑通过”的做法。如果你正在找一个能直接抄作业的代码组织方案,接下来的内容应该对你有用。

2. 三层结构到底怎么切:边界比层数更重要

2.1 为什么是三层,而不是两层或四层

很多人一听到“分层”,第一反应是“层数越多越专业”。我见过有人把项目切成六七层,结果一个简单的查询请求要在六个文件之间跳转,改一行代码要开八个标签页。这种分层不是秩序,是负担。t3code选择三层,核心逻辑是:三层刚好能覆盖“输入处理、业务决策、数据落地”这三个不可合并的职责,同时又不至于让调用链路长到难以追踪。

具体来说,这三层分别是:

  • 接入层:负责接收外部请求、做参数校验、组装返回格式。它不关心业务规则,只关心“进来的东西长什么样、出去的东西长什么样”。
  • 逻辑层:负责真正的业务判断、流程编排、规则计算。它是整个系统的“大脑”,所有“如果……就……”的判断都应该在这里发生。
  • 数据层:负责和存储打交道,包括数据库读写、缓存操作、外部接口调用。它不关心业务对错,只关心“怎么把数据拿回来、怎么把数据存进去”。

为什么不是两层?因为如果把接入和逻辑合并,参数校验和业务规则会混在一起,改一个校验规则可能影响业务判断。为什么不是四层?因为再加一层“服务层”或“编排层”,在中小项目里往往是过度设计,逻辑层本身就能承担编排职责。三层的价值不在于数字本身,而在于它强制你把“格式问题”和“规则问题”分开,这是后续所有可维护性的基础。

2.2 每一层的职责边界怎么划

划边界这件事,说起来简单,做起来最容易走样。我见过太多项目,名义上分了层,实际上接入层里写着业务判断,数据层里藏着格式转换。要避免这种情况,我建议用一条硬标准来检验:如果这一层的代码需要知道“业务上什么是对的”,那它就不属于这一层。

接入层的代码,应该只做三件事:解析输入、调用逻辑层、包装输出。它不应该出现“如果用户是VIP就打折”这种判断,因为那是业务规则。它也不应该直接查数据库,因为那是数据层的活。一个合格的接入层函数,读起来应该像一份“翻译说明书”:把HTTP请求翻译成逻辑层能理解的参数,再把逻辑层的返回翻译成HTTP响应。

逻辑层的代码,应该只做一件事:根据输入决定做什么。它不关心输入是从HTTP来的还是从命令行来的,也不关心结果要存到MySQL还是Redis。它只关心“这个操作在业务上意味着什么”。比如“下单”这个动作,逻辑层要判断库存够不够、用户有没有权限、要不要触发优惠,但它不负责把订单写进数据库——那是数据层的事。

数据层的代码,应该只做一件事:执行存取操作。它不应该包含“如果库存不足就返回错误”这种判断,因为那是逻辑层的职责。它只负责“把这条记录写进去”或“把那条记录读出来”,至于读出来之后怎么用,不归它管。

注意:边界划得越清楚,层与层之间的“接口”就越重要。我建议每一层对外暴露的方法都尽量少,最好控制在个位数。方法越少,别人越容易理解这一层能做什么,也越不容易被误用。

2.3 层与层之间怎么通信才不别扭

分层之后,下一个问题就是:层与层之间怎么传数据?我见过两种极端做法。一种是所有层之间都用同一个“万能对象”传递,结果这个对象字段越来越多,最后没人知道哪些字段在哪些层有效。另一种是每层都定义自己的数据结构,结果层与层之间要写大量转换代码,改一个字段要动三个地方。

t3code的实践建议是:接入层和逻辑层之间用“参数对象”,逻辑层和数据层之间用“领域对象”。参数对象只包含这次请求需要的信息,字段少、含义明确。领域对象则承载业务概念,比如“订单”“用户”“商品”,它的字段和业务规则对齐。数据层拿到领域对象后,自己决定怎么映射到数据库表结构。

这样做的好处是:接入层的改动不会直接影响数据层,数据层的表结构变化也不会直接冲击接入层。中间的逻辑层像一个缓冲带,把两边的变化隔离开。代价是逻辑层需要做一些字段映射的工作,但这个代价是值得的,因为它换来的是两边可以独立演进。

3. 把t3code落到目录结构上:文件怎么摆比怎么命名更重要

3.1 目录划分的两种思路与取舍

知道了三层结构,接下来要把它落到实际的目录上。这里有两种常见思路。第一种是按层分目录:根目录下建三个文件夹,分别叫handlers、services、repositories,所有接入层代码放第一个,逻辑层放第二个,数据层放第三个。第二种是按业务分目录:根目录下按业务模块建文件夹,每个模块内部再分三层。

这两种思路我都用过,我的结论是:项目初期按层分,项目中期按业务分。为什么?因为项目初期业务模块少,按层分能让新人一眼看懂“这个项目有三层”,快速建立整体认知。但当业务模块涨到五六个以上,按层分会导致一个业务改动要同时打开三个文件夹,改起来很累。这时候按业务分更合适,每个模块自成体系,改一个业务只在一个文件夹里操作。

如果你不确定选哪种,我建议先用按层分,等感觉到“改一个功能要跳来跳去”的时候再切到按业务分。切换成本并不高,因为三层结构本身没变,只是文件夹的嵌套方式变了。

3.2 一个可直接抄的目录模板

下面是我在多个项目里反复调整后固定下来的目录结构,按业务分目录的版本,你可以直接拿去用:

src/ modules/ order/ order.handler.js # 接入层:处理请求、校验参数 order.service.js # 逻辑层:业务规则、流程编排 order.repo.js # 数据层:数据库读写 order.model.js # 领域对象定义 user/ user.handler.js user.service.js user.repo.js user.model.js shared/ db.js # 数据库连接 errors.js # 统一错误定义 logger.js # 日志工具 app.js # 应用入口

这个结构有几个细节值得说明。第一,每个业务模块内部的文件命名统一用模块名.层名.js,这样在编辑器里搜索时,输入order.就能看到这个模块的所有文件。第二,shared目录放跨模块共用的东西,但不要什么都往里塞,只有真正被两个以上模块使用的代码才放这里,否则它会变成一个垃圾堆。第三,model.js单独存在,是因为领域对象可能被逻辑层和数据层同时引用,放在模块根目录最方便。

3.3 文件命名和函数命名的约定

目录结构定了,接下来是命名。我见过太多项目,文件名叫utils.js、helpers.js、common.js,打开一看里面什么都有。这种命名等于没命名。t3code的建议是:文件名要能回答“这是什么层的什么模块”,函数名要能回答“这个函数做什么”。

具体约定如下:

  • 接入层函数用动词开头,比如createOrder、getUserProfile,因为它处理的是一个动作。
  • 逻辑层函数用业务动作命名,比如placeOrder、calculateDiscount,它描述的是业务上发生了什么。
  • 数据层函数用存取动作命名,比如insertOrder、findUserById,它描述的是对存储做了什么。

这样命名之后,你看到一个函数调用orderService.placeOrder(),立刻就知道它在逻辑层,做的是下单这个业务动作。看到一个函数叫orderRepo.insertOrder(),立刻就知道它在数据层,做的是插入操作。命名清晰带来的最大好处不是好看,而是减少沟通成本——新人看代码时不需要问“这个函数在哪一层”,看名字就知道了。

4. 数据在层间怎么流动:一次请求的完整追踪

4.1 从请求进来到响应出去的全链路

光说结构有点抽象,我们用一个具体场景把三层串起来。假设有一个“用户下单”的请求,从进入系统到返回结果,完整链路是这样的:

  1. 接入层的orderHandler.createOrder收到请求,解析出userId、productId、quantity三个参数,做基本校验(比如数量不能为负),然后调用orderService.placeOrder。
  2. 逻辑层的placeOrder收到参数后,先调用userRepo.findUserById确认用户存在,再调用productRepo.findProductById确认商品存在且库存充足,然后计算价格、生成订单号,最后调用orderRepo.insertOrder把订单写进去。
  3. 数据层的insertOrder收到领域对象后,把它映射成数据库表的字段,执行插入语句,返回插入结果。
  4. 逻辑层拿到插入结果后,决定返回什么给接入层,比如返回订单号和总价。
  5. 接入层拿到逻辑层的返回后,包装成HTTP响应格式,返回给调用方。

这条链路里,每一层只做自己该做的事。接入层不判断库存,逻辑层不写SQL,数据层不关心HTTP状态码。这种清晰的分工,是后续排查问题和修改功能的基础。

4.2 参数对象和领域对象的转换时机

上面这条链路里有一个容易被忽略的细节:参数对象和领域对象在哪里转换?我的做法是在逻辑层的入口处转换。接入层传进来的是参数对象,逻辑层收到后第一件事就是把它转换成领域对象,然后后续所有操作都基于领域对象进行。

为什么不在接入层转换?因为接入层不应该知道领域对象长什么样,那是业务概念,属于逻辑层的知识。为什么不在数据层转换?因为数据层拿到领域对象后只需要做映射,不需要反向转换。转换点放在逻辑层入口,能让逻辑层成为唯一同时了解两种对象的地方,这样两边变化时只需要改逻辑层一处。

4.3 错误怎么在层间传递

错误处理是分层设计里最容易乱的地方。我见过接入层直接捕获数据库异常然后返回500的,也见过逻辑层把业务错误包装成HTTP状态码的。这些做法都会让层与层之间的边界模糊。

t3code的建议是:每一层只抛出自己这一层能理解的错误,由上一层决定怎么处理。数据层遇到数据库连接失败,抛出一个“存储不可用”的错误;逻辑层收到后,判断这是系统错误还是业务错误,如果是业务错误(比如库存不足),就抛出一个业务异常;接入层收到业务异常后,决定返回什么状态码和什么提示信息。

这样做的好处是,业务错误和系统错误被清晰地区分开。库存不足是业务错误,应该返回给用户看;数据库连接失败是系统错误,应该记录日志并返回通用错误提示。如果混在一起,要么用户看到一堆技术术语,要么运维看不到真正的故障信息。

5. 实测中容易走样的几个地方

5.1 逻辑层膨胀成“上帝层”

三层结构跑一段时间后,最常见的问题是逻辑层越来越胖。因为所有业务判断都往逻辑层塞,最后它变成了一个什么都管的“上帝层”。我见过一个逻辑层文件超过两千行,里面既有订单计算,又有用户权限,还有消息推送。

要避免这个问题,我的经验是给逻辑层设一个“职责上限”:如果一个逻辑层函数超过一百行,或者一个逻辑层文件超过五百行,就应该考虑拆分。拆分的依据不是代码行数,而是业务概念的独立性。比如订单计算和消息推送是两个独立的业务概念,即使它们都在下单流程里被调用,也应该拆成两个逻辑层模块,由一个编排函数把它们串起来。

5.2 数据层偷偷混入业务判断

另一个常见问题是数据层里出现业务判断。比如在findProductById里加一句“如果库存小于0就返回null”,这看起来方便,实际上是把业务规则藏到了数据层。以后业务规则变了(比如允许负库存),你要去数据层改,而不是逻辑层。

我的做法是数据层只做纯粹的存取,不做任何判断。库存是否充足,由逻辑层拿到数据后自己判断。数据层返回的就是数据库里的原始值,哪怕它是负数。这样业务规则的变更永远只影响逻辑层,数据层保持稳定。

5.3 接入层直接调用数据层

还有一种走样是接入层跳过逻辑层,直接调用数据层。比如一个简单的查询接口,接入层觉得“反正只是查一下,没必要走逻辑层”,就直接调了repo.find。这种做法在项目小的时候没问题,但一旦这个查询需要加权限判断或缓存逻辑,你就得回头改接入层,而接入层本来不应该关心这些。

我的建议是即使是最简单的查询,也要走逻辑层。逻辑层可以只是一个透传函数,但它存在本身就是一种约束:以后加业务规则时,你知道该往哪里加。这个约束的成本很低,但收益在项目变复杂时会非常明显。

6. 从三层结构延伸出的测试和调试习惯

6.1 每一层怎么测才不重复

分层之后,测试也可以分层做。接入层测的是“参数校验对不对、返回格式对不对”,逻辑层测的是“业务规则对不对”,数据层测的是“SQL写得对不对”。三层各测各的,互不重复。

我见过有人只写端到端测试,一个请求打进去,检查数据库里的结果。这种测试覆盖率高,但定位问题很慢——测试失败了,你不知道是接入层参数解析错了,还是逻辑层规则算错了,还是数据层SQL写错了。分层测试的好处是失败时能立刻定位到哪一层出了问题。

6.2 出问题时先看哪一层

线上出问题时,分层结构能帮你快速缩小排查范围。我的排查顺序是:先看接入层的日志,确认请求参数对不对;如果参数没问题,再看逻辑层的日志,确认业务判断走到哪一步;如果逻辑判断也没问题,最后看数据层的日志,确认SQL执行结果。

这个顺序的逻辑是:从外到内,从简单到复杂。参数错误是最容易发现也最容易修复的,SQL问题往往需要更多上下文。按这个顺序排查,大部分问题在前两步就能定位,不需要一上来就翻数据库日志。

6.3 日志应该打在哪一层

日志打在哪一层,决定了你排查问题时能看到什么。我的做法是:接入层打请求和响应日志,逻辑层打关键业务节点日志,数据层打慢查询日志。接入层的日志让你知道“谁在什么时候调了什么接口”,逻辑层的日志让你知道“业务走到了哪一步”,数据层的日志让你知道“哪条SQL慢了”。

三层日志的详细程度可以不同。接入层可以打全量请求参数,逻辑层只打关键判断结果,数据层只打超过阈值的慢查询。这样既不会日志爆炸,又能在需要时找到足够的信息。

7. 关于t3code的一些个人体会

我在多个项目里用过t3code这套三层结构,最大的体会是:它的价值不在于“分了三层”,而在于“逼你想清楚每一层该做什么”。很多项目的问题不是没有分层,而是分了层但边界模糊,结果比不分层还乱。t3code的约束足够简单,简单到你可以在一张白板上画出来,但它又足够严格,严格到你能用它来拒绝一些“看起来方便但会破坏结构”的改动。

另一个体会是,三层结构不是越早用越好。如果项目只有几百行代码、一个人维护,强行分三层反而增加负担。我通常建议在项目代码超过两千行、或者参与人数超过两个时,再引入这套结构。在那之前,怎么快怎么来,不用太纠结结构。

最后分享一个小技巧:如果你不确定某个函数该放哪一层,就问自己一个问题——“这个函数需要知道业务上什么是对的么?”如果需要,它属于逻辑层;如果不需要,只是格式转换或数据存取,那它属于接入层或数据层。这个问题我用了很多次,几乎每次都能帮我快速做出判断。

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

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

立即咨询