1. 先从“搞不懂”说起:DDD到底卡在哪
1.1 战术与战略,不是两套知识,而是同一个认知的上下游
很多人搜“DDD搞不懂”,其实不是智商问题,也不是代码写得太少,而是大多数人把DDD当成了一堆名词的集合:实体、值对象、聚合、仓储、领域事件、CQRS、事件溯源……每个词单独看都能查到定义,拼在一起却不知道先干哪件事。我当年也是这样,拿着Eric Evans那本书翻了三遍,脑子里只有一句话:“嗯,很有道理。”然后回到项目里,还是Controller-Service-DAO那老三层。
后来我才想明白一件事:DDD本质上分两个层面,战术设计和战略设计。战术设计解决的是“一个限界上下文内部怎么写代码”,战略设计解决的是“多个限界上下文之间怎么划边界、怎么通信”。这两个层面不是割裂的,而是同一个认知的上下游。战术设计做得好不好,直接决定你有没有能力把战略层的边界落到真实代码里;而战略层想不清楚,战术层写得再漂亮,也只是在一个烂地基上盖精装房。
我建议初学者换个学习路径:先从单服务战术落地开始,把一个业务模块真正用DDD的方式写干净,再去接触分布式中台和战略设计。因为战略层如果不落到代码上,很容易变成PPT架构——画了一堆限界上下文,代码里却是意大利面。反过来,如果战术层练扎实了,你自然能体会到为什么需要战略层来管理边界和协作,就像练拳先练站桩,站桩不稳的人去打散打,只会被打得更惨。
1.2 一个最容易被忽略的前提:模型必须能被代码表达
DDD的全称是Domain-Driven Design,核心词在Domain,但真正让DDD区别于一堆分析文档的,是Design——领域模型必须能被代码直接表达。这句话听起来像废话,但我见过太多团队把DDD做成了“领域名词收集大会”。
举一个我实际辅导过的例子。团队要做订单系统,业务专家说“订单有状态,待支付、已支付、已发货、已完成”,开发同学就在数据库里建了一张order表,带一个status字段,然后在service层写一堆if else判断状态迁移。这算不算DDD?严格来说不算,因为状态被压扁成了一个字段,所有业务规则都藏在service里,领域模型根本不存在。真正的做法是把状态机的规则放到聚合根里,由订单自己决定能不能从“待支付”变成“已发货”,而不是让任何service层随便改status。
这里涉及到一个老生常谈的词:贫血模型与充血模型。传统三层的service层,本质上是把业务逻辑全部放在“上帝服务”里,领域对象只剩一堆getter和setter,这就是贫血模型。而DDD要求领域模型中不仅要有数据,还要有行为,让业务规则内聚在它该归属的地方。你可以把这个区别理解为自助餐和私房菜:自助餐把所有菜摆出来,你自己夹;私房菜是主厨根据你的需求配菜,吃什么、什么顺序,由主厨决定。贫血模型就是自助餐,点什么都往Controller和Service里加;充血模型是私房菜,聚合根就是那个主厨。
所以,你在单服务里写DDD时,第一个要问自己的问题是:我的业务规则到底写在哪儿?如果答案还是“写在Service里”,那你写再多的聚合、值对象、仓储,也只是换了一层皮。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 单服务战术落地:先让一个限界上下文内部变干净
2.1 六边形架构:把领域模型放到太阳底下
单服务的战术落地,我推荐从六边形架构入手。六边形架构也叫端口与适配器架构,它的核心思想是让领域模型处于系统的正中心,业务对它外部的一切都不知情:不知道数据库,不知道HTTP,不知道消息队列,甚至不知道Spring或者.NET这类框架的存在。
可能有读者会问:这不就是分层架构吗?Controller算适配器,Service算应用服务,Repository算端口,Infrastructure算适配器实现,好像没差多少。但是六边形架构和经典分层有一个本质区别:依赖的方向。经典分层是从上往下依赖,Controller依赖Service,Service依赖Mapper;六边形架构是依赖倒置,领域层定义端口(比如仓储接口、消息发布端口),外部适配器来实现这些端口,依赖方向始终指向最里层的领域模型。
用一个生活类比:插座标准。你的手机充电器是领域层,它只认USB-C这个端口。墙上那个220V插座是什么电网公司、什么发电方式,充电器完全不用关心。你只需要一个适配器把交流电转成USB-C——六边形架构就是这个适配器层。把适配器拔下来换个新的,领域层一行代码都不用改。
实际动手时,我建议把一个旧工程重构成六边形架构,按照以下步骤走,效果很好:
- 先确认限界上下文,把这个上下文涉及的业务功能圈进来,不相关的功能一律不碰。
- 梳理领域模型:找出聚合根、实体、值对象、领域服务、领域事件,这一步是重中之重。
- 定义端口:把所有对外依赖抽象成接口,包括仓储、外部API调用、消息发送等。
- 实现适配器:把现有的Controller、Repository、FeignClient、KafkaProducer等填到端口外。
- 最后调整目录结构,让依赖方向从外向内,禁止反向依赖。
我可以给你一个参考目录,这只是最小结构,具体到项目可以再调整:
text复制order-context/
├── interfaces/ # 适配器层:Controller、Consumer、Job
│ ├── rest/
│ └── mq/
├── application/ # 应用服务层:事务、命令分发、查询处理
│ ├── command/
│ └── query/
├── domain/ # 领域层:聚合、实体、值对象、端口
│ ├── model/
│ ├── service/
│ └── repository/ # 仓储接口(端口)
└── infrastructure/ #
