本节摘要:实体、服务、仓库各自归位之后,还需要一层骨架把它们组织成可导航的整体。包结构是架构的第一张脸:按技术层分包,目录是数据流的形状;按领域模块分包,目录就是 2.2 上下文地图的形状。本节给出青柚商城交易上下文的实际包结构、模块内部分层的三条依赖铁规,并解释为什么"先模块化单体、再谈拆服务"的顺序在 3.4 这一层就要打好地基。
拿到一个陌生代码库,工程师做的第一件事是展开目录树。目录告诉他的第一件事不是代码质量,而是这个团队如何理解系统。两种分包对应两种世界观。
按技术层分包:顶层目录是 controller、service、dao、model。所有业务搅在同一层——service 目录下交易的下单、仓储的拣货、财务的对账挤在一起,想找"退款逻辑在哪",要在上千个文件里按名字捞。这种形状是数据流的形状:请求从上往下穿层而过。它隐含的世界观是"系统 = 一条条流经技术环节的数据"。
按领域模块分包:顶层目录是上下文与聚合——order、inventory、promotion。想找退款逻辑,打开 order 模块,它就在那里;想改营销规则,根本不用打开 inventory。这种形状是 2.2 上下文地图的形状。它隐含的世界观才是 DDD 的世界观:系统 = 一组各自内聚的领域,技术细节是每个领域的私事。
青柚商城动工时的第一版骨架就吃过按层分包的亏:交易与仓储的 Service 混在同一个目录,一个新同事改拣货逻辑时顺手 import 了交易的订单工具类,编译通过、测试通过,上线后交易与仓储之间多了一条谁也说不清的隐性依赖。迁到按模块分包并加上依赖检查后,这类 import 在编译期就过不去——包结构的价值不在美观,在于它能把架构意图变成编译器可执行的约束。
// 交易上下文的包结构(单体时期,一个模块即一个上下文) com.qingyou.trade ├── order // order 聚合:3.1 到 3.3 所有产出的家 │ ├── domain // 领域层:系统的重心所在 │ │ ├── Order.java // 聚合根:状态迁移与不变量 │ │ ├── OrderLine.java // 实体 │ │ ├── Money.java // 值对象 │ │ ├── OrderPaid.java // 领域事件 │ │ ├── OrderRepository.java // 仓库接口(不是实现!) │ │ └── AllocationService.java // 跨行分摊的领域服务 │ ├── application // 应用层:用例编排,薄 │ │ └── PlaceOrderUseCase.java │ └── infrastructure // 基础设施:领域层的粉丝,不是领导 │ ├── OrderRepositoryImpl.java │ └── OrderSnapshotDao.java ├── refund // refund 聚合:同款结构,独立目录 └── shared // 本上下文内的共享内核:极薄 └── TradeOrderId.java
这棵树上能读出三条结构性信息。第一,模块按聚合切,不按层切:order 与 refund 互不串门,各自的 domain、application、infrastructure 自成一体——层在模块内部,模块之间只有领域语义的往来。第二,domain 目录最重:聚合、事件、仓库接口都在里面,打开目录先看到业务词汇表;映射工具的注解、框架的 import 被隔离在 infrastructure。第三,shared 极薄:只有被两个聚合都用到的 id 类型;它一旦长出业务方法,就说明有聚合的边界画错了,回头查 3.5 的四条判据。
模块内部的依赖铁规同样值得明文写下,三条都是编译器或架构测试工具可以逐条检查的:
| 铁规 | 内容 | 违反的典型症状 |
|---|---|---|
| 单向依赖 | application 可以依赖 domain;infrastructure 依赖 domain 的接口;domain 不依赖任何人 | 领域对象里出现框架注解与数据访问调用 |
| 聚合隔离 | 跨聚合只经 id 引用或事件往来,不得直接持有对方聚合对象 | order 聚合里嵌着 inventory 的库存对象 |
| 上下文封锁 | 跨上下文只走防腐层与发布语言(2.4),禁止 import 对方内部类 | trade 模块里出现 marketing 内部的优惠实体 |
第三条铁规是 2.4 的代码化:防腐层翻译类就放在调用方的 infrastructure 目录,成为对方模型在本上下文内唯一合法的出现地点。三条规矩立住之后,"架构腐化"这个抽象名词变成了三台可自动运行的检查——模块化不是目录美学,是给编译器写的宪法。
其一,模块化单体是拆服务的预演。 青柚商城至今没有拆微服务,但它的模块边界按第 6 章微服务的标准来立:模块间不共享数据库连接、不共享事务管理器、只走接口与事件。这层预演的成本几乎为零,收益在拆分那天兑现——把模块抽成独立服务时,搬走的是代码,不用改设计。反过来,按层分包的单体想拆微服务,等于在没有房间隔断的房子里装门,每一堵墙都要现砌。
其二,domain 不依赖框架,是纪律不是教条。 有团队把"领域层纯净"执行成清规戒律,连工具注解都不敢用,为此手写大量映射代码。务实版本是:领域层可以依赖语言特性与极少的通用工具库,凡是会把业务语义挤走的依赖(对象关系映射、Web 框架、消息客户端的 API)一律隔离在 infrastructure。判断的尺子还是那句老话——打开 domain 目录,读到的应该是业务,不是技术选型清单。
骨架齐了,本章只剩那块最硬的骨头:聚合边界到底怎么划。3.5 用一道真实争议题做完整推演——白板上那三行字,该给答案了。