首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
T3Code:用接口契约与依赖规则重构三层架构,终结代码分层混乱
📅 2026/10/26 17:14:15
✍️ 爱科研究院
👁 阅读 3,247
第一次在代码仓库里看到t3code这个名字时我以为它是某个机场三号航站楼的内部业务系统代号。翻完设计文档才发现T3Code 并不是一个机场项目而是一套以三层架构为基底、以代码工程化为目标的后端代码组织方案。这个名字本身藏着三层意思后面我会展开讲。简单说T3Code 解决的是我在大量项目里反复遇到的同一个问题Controller-Service-DAO 写到最后业务层变得越来越臃肿DTO 满天飞事务边界说不清换一个人来改需求代码放哪一层完全看心情。如果你也正在被这些问题困扰或者想让团队的分层代码不再乱成毛线团这篇内容值得看完。1. T3Code 是什么一次对三层架构的重新定义1.1 名字拆解T3 的三重含义T3Code 这个名字我理解下来有三个层面的含义它们并不冲突反而共同构成了这套方案的骨架。第一个含义是 Tier 3也就是标准的三层架构。表现层负责接收请求、返回响应业务层负责用例编排、规则判断、事务控制数据层负责读写数据库。这层含义是 T3Code 的底座它不排斥经典分层反而是在经典分层的肩膀上做精细化约束。第二个含义是 Terminal 3 的联想。把一次业务请求类比成一次航班出行表现层是值机柜台负责校验票据、托运行李、发放登机牌业务层是中转调度中心负责航路规划、节点衔接、异常改签数据层是登机口负责把旅客安全送上飞机、落库。这三者的职责一旦混淆就好比值机柜台直接跑去开飞机系统不乱才怪。第三个含义是 Third-party 的隔离意识。T3Code 特别强调第三方依赖不能穿透架构业务层不能看到 MyBatis 的 SqlSession表现层不能直接操作 Redis 客户端基础设施的细节必须被封装在数据层内部。这一条在实际代码评审中是最容易被忽略、也最值得较真的地方。1.2 它解决的核心问题先说一个我亲眼见过的真实场景。有一个订单系统早期只有两个开发同学维护分层还算清晰。后来团队扩到 12 人需求频率翻倍代码就开始失控了。有人在 Controller 里直接写 JDBC有人在 Service 里拼 SQL还有人为了省事把 Dubbo 的 RpcContext 传到 DAO 层去取用户信息。最后的结果是每次上线都要靠资深程序员人肉排雷新人根本不敢碰核心链路。T3Code 要解决的就是这类“分层名存实亡”的问题。它给出了一套可以落地的规则接口契约要怎么定义、类要怎么命名、依赖方向怎么检查、事务边界画在哪里、代码评审时逐条对照什么清单。它不是银弹但至少能让团队对“这段逻辑应该放哪一层”这个问题有一个统一答案。1.3 与传统三层架构的差异传统三层架构本身没问题问题在于它太抽象了只说了 Controller、Service、DAO却没有给出判断依据。T3Code 把它往工程化方向推进了一步核心差异我用一张表来对照对比维度传统三层架构T3Code 的风格分层依据Controller/Service/DAO 约定依赖方向和职责边界双重约束接口定义往往从实现类反推先定义接口契约实现类后补DTO 使用各层共用复用混乱DTO 只在表现层出现跨层用命令/模型对象事务边界写在 DAO 或 Service 均可只允许在应用服务层开启第三方依赖可以从任意层引用只允许出现在基础设施层内部可测试性Service 依赖具体 DAO 实现业务层依赖仓储接口测试时轻松替换这张表里的每一条后面都会用代码示例展开。咱们先把框架立住再聊细节。2. 核心设计思路与架构拆分逻辑2.1 为什么把“接口契约”提到最前面过去团队协作里最常见的冲突是这样的后端同学先写 Controller前端同学等着接口文档数据库表还没建完两边就开始互相催促。等到联调时发现字段对不上再回头改代码浪费大量时间。T3Code 的做法是先把接口定义拿出来。这个接口不是 REST 接口而是应用服务层的用例接口。比如创建一个订单先写下面这个方法public interface OrderApplicationService { OrderId createOrder(CreateOrderCommand command); }这里有几个讲究。第一CreateOrderCommand是入参命令对象它只包含本次用例需要的数据不携带多余字段第二返回的是OrderId而不是数据表实体也不直接返回一个 Map第三接口只描述“做什么”不描述“怎么做”。定义好之后前端可以按命令对象的字段结构准备数据数据库设计可以并行推进写实现类的同学也不用天天被催因为契约已经锁定了。我把这种前置定义叫“合同先行”。它和现实中的工程发包逻辑一样合同签清楚了再施工才不会边干边扯皮。2.2 三层边界到底怎么划很多人问过我到底什么样的代码该放在哪一层T3Code 给了一个非常简单的判断方式我总结成两句话。第一句话表现层只做“收参数、组响应”。请求校验、数据格式转换、状态码映射都放这里。如果一段代码里出现了 result.setXxx() 这种给前端拼数据的操作它就不该出现在业务层。第二句话应用服务层只做“用例编排”。事务开启、调用仓储接口、组装领域结果都放这里。如果一段代码里出现了具体数据库表名、SQL 片段、缓存 key 前缀它就不该出现在应用服务层。我把这套规则整理成一个依赖方向的图示概念表现层adapter │ 依赖方向 ↓ 应用服务层application/domain │ 依赖方向 ↓接口 数据访问层接口定义在业务层实现下沉到基础设施层这里最容易被误解的是数据层的方向。T3Code 不是让业务层去依赖 MyBatis 的 Mapper而是让业务层定义仓储接口再让基础设施层去实现这个接口。这样依赖方向就反转了业务层是主人数据库实现是仆人。想换数据库、换 ORM 框架、加缓存策略都不会污染业务代码。2.3 一个可落地的目录结构示例理论讲完直接看目录。下面的结构是我在一个订单项目里实际使用的 T3Code 风格布局t3code-order ├── adapter # 表现层 │ ├── controller │ │ └── OrderController.java │ └── dto │ ├── CreateOrderRequest.java │ └── OrderResponse.java ├── application # 业务层 │ ├── command │ │ └── CreateOrderCommand.java │ ├── repository # 仓储接口定义层不写实现 │ │ └── OrderRepository.java │ ├── service │ │ ├── OrderApplicationService.java │ │ └── OrderApplicationServiceImpl.java │ └── domain # 领域模型可选按需引入 │ └── Order.java └── infrastructure # 基础设施层 ├── repository │ └── OrderRepositoryImpl.java └── mapper └── OrderMapper.java这个目录结构看着简单但每条路径都对应一条架构规则。application/repository只放接口不允许出现 SQLinfrastructure里的类可以依赖 Spring、MyBatis、Redis 客户端但application里的类不能反向依赖它们。谁守住了这条线谁就守住了整个架构的命脉。3. 实操落地从零搭一套 T3Code 风格工程3.1 脚手架搭建与依赖选型我用 Java Spring Boot 作为示例来说明落地过程因为这套组合在国内后端团队里覆盖率最高。但思路本身不绑定语言换成 Go、Python、Node.js 同样可以平移关键是保持依赖方向一致。建工程时只引入必要的依赖避免一上来就堆一大堆工具包。基础依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这里刻意没有引入类似hutool、commons-lang3这类大而全的工具库。原因很简单T3Code 强调分层清晰工具类的滥用往往是跨层依赖的温床。项目里的工具方法该放在哪个包、该被哪些层使用得提前想清楚。否则一个静态方法满天飞的工程比不分层的工程还难维护。3.2 一段完整业务代码的层级实现接下来用一个创建订单的用例把四个核心文件串起来。先从仓储接口开始public interface OrderRepository { Order save(Order order); OptionalOrder findById(OrderId id); }这个接口定义在application/repository包下。它只描述了两个动作保存和按 ID 查询完全不知道底层用的是 MySQL 还是 PostgreSQL。接下来是应用服务层的接口和实现public interface OrderApplicationService { OrderId createOrder(CreateOrderCommand command); }Service public class OrderApplicationServiceImpl implements OrderApplicationService { private final OrderRepository orderRepository; private final OrderIdGenerator idGenerator; public OrderApplicationServiceImpl(OrderRepository orderRepository, OrderIdGenerator idGenerator) { this.orderRepository orderRepository; this.idGenerator idGenerator; } Override Transactional(timeout 3) public OrderId createOrder(CreateOrderCommand command) { Order order new Order(idGenerator.next(), command.getTotalAmount()); orderRepository.save(order); return order.getId(); } }注意Transactional是在实现类的方法上而且只在这里出现。为什么不开在仓储实现上因为事务的粒度应该对应一个完整的业务用例而不是一次数据库操作。如果开在save方法上那么“创建订单”过程中如果还有写日志、扣库存、发消息等操作就无法纳入同一事务数据一致性必然出问题。关于timeout 3这个参数我再补充一句。3 秒是本例中订单创建这个用例的合理阈值因为不涉及外部系统调用正常情况 100 毫秒内就能完成。如果这个用例要同步调用第三方支付接口那么建议把外部调用拆到事务之外或者适当调大超时时间避免长事务锁表。这里想强调的是超时参数要按用例的实际行为去反推不能全工程统一拍脑袋。最后看基础设施层的实现Repository public class OrderRepositoryImpl implements OrderRepository { private final OrderMapper orderMapper; public OrderRepositoryImpl(OrderMapper orderMapper) { this.orderMapper orderMapper; } Override public Order save(Order order) { OrderDO orderDO OrderAssembler.toDO(order); orderMapper.insert(orderDO); return order; } Override public OptionalOrder findById(OrderId id) { OrderDO orderDO orderMapper.selectById(id.value()); return Optional.ofNullable(orderDO).map(OrderAssembler::toDomain); } }这段代码里会出现OrderDO、OrderMapper、OrderAssembler这些基础设施层特有的东西都是合理的因为它们都待在infrastructure包内部。而OrderAssembler做了一件很关键的事把数据库对象转换成领域对象转换逻辑被隔离在数据层内部不会泄露到业务层。3.3 这个方案的关键参数与配置除了事务参数还有几处配置值得单独拎出来讲。第一处是 Spring Boot 的SpringBootApplication扫描范围。如果公司规范要求启动类只扫描自己的启动包那么 T3Code 建议所有业务类都放在同一个根包下面比如com.example.t3code。不要出现 Controller 在一个根包、Service 在另一个根包的情况否则组件扫描配置会变成维护噩梦。第二处是 MyBatis 的 mapper 扫描。对应的配置写法是MapperScan(com.example.t3code.infrastructure.mapper) public class T3CodeApplication { // ... }这个配置的作用是把 Mapper 接口的扫描范围限定在 infrastructure 层。如果哪天在application包下也出现了 Mapper 接口项目启动就会直接报错等于在编译期用配置做了一次架构约束。第三处是 json 序列化的时区与空值处理。表现层一旦返回时间字段建议统一格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这类看似和分层无关的配置其实也是 T3Code 的一部分。一个工程的分层再干净如果表现层返回给前端的数据格式不统一联调体验依然会很差。工程化讲究的是全链路整洁不是只看包结构。4. 质量门禁与 Code Review 规则清单4.1 Review 检查表逐条对照不放过我每次做 T3Code 风格项目的代码评审都会在脑内过一遍固定清单。这里整理成表格方便团队直接复制进评审规范里检查项通过标准常见违规示例import 方向application 包不依赖 infrastructure 包Service 里直接 import MyBatis 的 MapperDTO 位置DTO 只出现在 adapter 层把前端字段对象传到 repository 方法里事务位置Transactional 只出现在应用服务实现类在 Controller 或 Mapper 上加事务注解仓储接口定义处接口写在 application实现写在 infrastructure业务层直接注入 Mapper 接口命令对象入参使用 Command 对象禁止裸 MapcreateOrder(MapString, Object params)返回值返回领域对象或 ID禁止返回数据库表实体Service 方法直接返回 OrderDO异常处理基础设施层异常翻译成业务异常再往上抛把 SQLException 直接抛到 Controller命名规范接口与实现类命名清晰可辨接口叫 OrderService实现也叫 OrderService这份清单看起来琐碎但它在评审中能真正做到“对事不对人”。有分歧时不用争论“我觉得放这里更合理”直接看清单不符合规则就回去改。规则是大家提前约定好的比个人权威更好使。4.2 用自动化工具守住架构边界人肉评审总有疏忽的时候所以 T3Code 强烈建议引入自动化架构检查。Java 生态里我用得最多的是 ArchUnit它可以直接写在测试代码里跑一遍单测就等于做一次全量架构体检。下面这条规则就是一堵自动的“围墙”ArchTest static final ArchRule no_infrastructure_dependency_in_application noClasses() .that().resideInAPackage(..application..) .should().dependOnClassesThat() .resideInAPackage(..infrastructure..);把它放到某个基础测试类里每次 CI 都会执行。只要有人不小心在业务层引用了基础设施包的类构建直接失败错误信息会精确指出哪个类依赖了哪个类。这种“把规则写进测试”的玩法比在 Code Review 里反复口头提醒要可靠得多。同样值得做的还有 Checkstyle 规则比如禁止在application包下出现import java.sql.Connection、禁止在adapter包下出现Transactional注解。这些规则的目的是把架构约束变成代码规范让 IDE 在写代码的当下就给出提醒而不是等评审时才发现。5. 常见问题与排查技巧实录5.1 事务不生效接口越改越乱的典型坑第一个高频问题Transactional不生效。常见原因有两个。一是类内部通过this调用加事务的方法Spring 的事务依赖代理对象自调用会绕过代理事务就静默失效了。二是事务方法被定义成了private代理对象根本访问不到它。排查方法很简单在事务方法里故意抛一个RuntimeException看数据库是否回滚用一次小实验就能定位问题。第二个高频问题DTO 误入业务层。团队里总有同学觉得“反正字段都一样直接用 DTO 传下去不是省事吗”。短期看确实省事长期看一旦前端需求改了字段结构DTO 一变业务层和数据层全部跟着变跨层耦合就产生了。T3Code 的解法前面已经说了DTO 只留在表现层业务层的数据模型用 Command 和领域对象独立定义。宁可多一点转换代码也不能让依赖方向倒转。第三个高频问题循环依赖。比如 OrderService 需要调用 UserServiceUserService 又需要调用 OrderServiceSpring 启动时可能兜得住但只要业务调整两个类的方法就会纠缠在一起。更隐蔽的是 Service 和 Repository 之间的循环依赖应用服务注入了仓储接口仓储实现又反向依赖了应用服务里的辅助类这种问题在 ArchUnit 规则下会立刻暴露纯靠人肉发现往往要等到上线前的联调阶段。第四个高频问题为了分层而分层的过度设计。一个只有 20 行的简单查询非要在 adapter、application、infrastructure 里各建一个类这会让新人觉得这套方案“很重”。我个人的建议是如果一个用例简单到不需要事务、不需要跨仓储组装、不需要后续扩展可以只保留 Controller Mapper 的直连并在评审时主动声明“这里不走完整 T3Code”。规则是为人服务的不是反过来绑架人的。5.2 排查思路与工具组合遇到架构问题我推荐按下面的顺序排查。先看依赖方向。用 ArchUnit 跑一遍相关包确认没有反向依赖。如果跑完没问题再排查事务边界写一个临时测试方法制造异常验证回滚行为。接着看 DTO 流向全局搜索某个 DTO 类是否出现在application或infrastructure包的 import 列表中。最后再看命名有没有同一个类被复制成了OrderBean、OrderVO、OrderDTO三个版本这类问题通常是从“命名不统一”开始的。有一组工具组合我现在一直在用IDEA 的Call Hierarchy查看方法调用链ArchUnit 做编译期架构检查SonarQube 做代码异味扫描。三者配合使用能覆盖从“单个方法”到“整个工程”的各个粒度。5.3 我踩过的几个独家坑这里再分享几个常规文档里不会写的细节。第一个坑仓储接口的返回值不要直接使用OptionalOrderDO。DO 是数据层的内部表示把 DAO 的抽象泄漏到业务层等于把数据库表结构的变化传染给了上层。正确做法是让仓储方法返回OptionalOrder领域对象哪怕内部要做一次转换。第二个坑命令对象不要直接复用前端请求体。有些同学觉得CreateOrderRequest和CreateOrderCommand字段一模一样干脆省略命令对象。一旦前端加了sourceChannel字段后端可能顺手把这个字段也透传到事务里最后事务逻辑被迫感知前端展示逻辑。命令对象的意义是定义“这个用例真正需要什么”字段变了以后业务层代码完全不需要动。第三个坑重蹈覆辙的“把 Redis 操作写进应用服务层”。缓存逻辑看起来属于业务规则但从依赖隔离角度来看它属于基础设施能力。T3Code 的做法是定义OrderCachePort之类的接口放在application然后在infrastructure里用 RedisTemplate 实现。这样业务层写单元测试时可以用一个内存版的假端口代替 Redis跑得又快又稳。写在最后的一点个人体会T3Code 不是某个框架也不是一套高深的架构理论它其实是一组把经典分层落到实处的工程习惯。我在实际项目里推广这套方案时最大的阻力往往不是技术水平而是大家习惯性地“凭感觉放代码”。靠 Code Review 一条条盯不现实靠团队自觉更不现实真正有效的方式就是这三板斧接口契约前置、依赖方向规则化、自动化检查兜底。如果你所在团队正被业务代码的混乱分层困扰我建议先别急着把整个团队拉过来推这套方案选一个中小型业务域试点跑两个迭代把目录结构调整好把 ArchUnit 规则加上再拿试点结果去说服团队。先小步跑通再全面铺开这是我能给的最实在的建议了。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/26 17:14:15
2023年AI答辩工具选型与实战指南
2026/10/26 17:09:14
Agent-Reach:面向生产环境的Agent运行时协调层
2026/10/26 17:09:14
WPF可视化大屏开发实战:从布局缩放到动画数据绑定
2026/10/26 18:04:30
Python爬虫神器PyQuery的使用方法
2026/10/26 18:04:30
零售巨头Loblaw数据泄露事件中的PII暴露风险与防御架构重构
2026/10/26 18:04:30
一文看懂MCP(大模型上下文协议)
2026/10/26 18:04:30
破解PS5如何传游戏最快?ps5upload断点续传上传实操详解
2026/10/26 18:04:30
dsh-purge 红队破甲上手指南:装完三步,看清拒绝到底卡在哪
2026/10/26 17:59:30
Design Compiler:触发器的复制(set_register_replication命令)
2026/10/26 0:01:33
热搜量不是文档频率:先把 BM25 的统计口径分开
2026/10/26 0:01:33
Nested Learning:解决AI灾难性遗忘的分层记忆架构
2026/10/26 0:01:33
企业级大模型应用:OCR预处理、Dify编排与DeepSeek落地实战
2026/10/26 11:29:50
全国计算机二级C语言真题PDF高效刷题指南
2026/10/26 0:03:29
requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南
2026/10/26 11:29:49
递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现
2026/10/25 22:15:16
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/26 11:29:48
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/25 12:42:56
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)