ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入理解DDD四层领域模型:职责划分、依赖倒置与落地实战

深入理解DDD四层领域模型:职责划分、依赖倒置与落地实战 有没有这种体会看了几篇 DDD 文章满屏都是“实体、值对象、聚合、仓储”觉得每个词都认识连到一起就不知道在说什么。尤其“四层领域模型”这个概念不同文章画出来的图还不一样有的叫“四层架构”有的叫“分层架构”再加上“六边形架构”“整洁架构”直接把人绕晕。我先说结论DDD 的四层领域模型是 Eric Evans 在《领域驱动设计》里提出的一套经典分层思路核心是把系统按职责分成接口层、应用层、领域层和基础设施层其中领域层是全部业务逻辑的家其他层都是为它服务的。这套模型的真正价值不是让你照着图建项目而是逼你想清楚“业务规则到底该放在哪”从而避免最常见的腐败业务逻辑被 UI、数据库、框架代码撕碎。这篇文章适合两类人一类是刚接触 DDD概念还没捋顺的开发者另一类是已经在项目里用了几次 DDD但总觉得代码越写越别扭、想回头把底层逻辑理清楚的人。我会把四层的职责边界、领域层里的核心概念、以及它和六边形架构/整洁架构之间的关系都拆开讲最后附上我自己落地时的实操经验和踩坑记录。1. 初识四层领域模型分层结构与定位1.1 四层模型到底分哪四层先别急着背名字我们用一张图在脑子里把位置定下来。四层模型从外到内依次是接口层Interface Layer也叫用户界面层或表现层。它的职责是接收用户的请求、把结果显示给用户。注意这里“用户”不只是浏览器里的人也可能是另一个系统通过 API 调用你。应用层Application Layer编排层。它负责协调用例流程比如“下单”这个用例需要先校验库存、再创建订单、再发事件。应用层不写业务规则只负责把领域层的对象叫起来干活。领域层Domain Layer整栋楼的核心。业务概念、业务规则、业务状态全在这层实体、值对象、聚合、领域服务、领域事件、仓储接口都定义在这里。基础设施层Infrastructure Layer技术细节层。数据库连接、ORM 实体映射、消息队列、文件存储、第三方 SDK 适配……所有和技术相关的实现都放在这里。关键是它要去实现领域层定义的接口而不是反过来。你可以把这四层想成一家餐厅接口层是服务员负责接单和上菜应用层是大堂经理负责协调“先干什么后干什么”领域层是后厨所有菜怎么做、用什么配方、什么顺序出菜都是后厨的事基础设施层是食材供应商、水电气和洗碗机后厨需要什么它提供什么它绝对不决定菜单。1.2 依赖规则由外向内还是由内向外四层模型里最容易画错的就是依赖方向。很多人凭直觉画成了“从上往下调用”也就是接口层 → 应用层 → 领域层 → 基础设施层数据库在最底层。这个直觉在现代 DDD 实践中是反的。正确的依赖规则是外层依赖内层内层绝不依赖外层。也就是说接口层依赖应用层应用层依赖领域层而领域层不依赖任何外层。基础设施层比较特殊它处于“最底层”的位置但它要去依赖领域层定义的接口并实现这些接口。这里的关键是“依赖倒置”。领域层里只放接口比如OrderRepository但没有任何一行 SQL基础设施层里写OrderRepositoryImpl用 MyBatis、JPA 或者其他什么来实现。这样领域层完全不知道数据库存在它只知道自己需要一个仓储来存取订单至于仓储是存 MySQL 还是 MongoDB领域层根本不在乎。记住一句话依赖的方向就是代码中 import 的方向。检查项目时直接看领域层有没有 import 基础设施层的类有就说明依赖倒置没做好。1.3 为什么要分层不分会怎样我见过不少“没有分层”的遗留系统表象非常统一Service 层几百上千行里面既有校验逻辑、又有 SQL、又塞各种 if 判断Controller 里直接调 Repository改一个业务规则要同时动表结构、改实体、改Mapper、改前端字段。这种代码不是说不能运行而是每一次变更都像扫雷。分层解决的是三件事可测试性。业务规则放在领域层领域层不依赖框架和数据库后可以用纯单元测试快速验证。不需要启动 Spring 容器不需要连测试库跑一次只要几毫秒。可维护性。依赖方向清晰后改 UI 不会碰业务换数据库不会碰业务。我曾经把一个项目从 MySQL 迁到 PostgreSQL业务代码一行没改只改了基础设施层的实现和部分 SQL。可理解性。新人入职读代码先读领域层就能知道“这个系统是做什么的、规则是什么”而不是在 Controller 和 SQL 之间考古。不分层当然也能跑但项目一旦超过两三个人维护、生命周期超过半年分层的收益就会非常明显。DDD 的四层模型不是在增加复杂度它是在用结构化的方式管理复杂度。2. 领域层核心概念拆解2.1 实体与值对象两种完全不同的建模对象这是 DDD 里最早让新手困惑的一对概念困惑的来源是“在数据库里它们都是表/字段凭什么要分开”实体Entity的核心特征是“有唯一标识且标识在其生命周期内不变”。比如一个人即使换了名字、换了住址他还是他因为身份证号没变。对应到系统里订单有订单号用户有用户 ID账户有账户号。实体的特征是可变它的属性会随着业务行为发生变化订单状态从“待支付”变成“已支付”这就是实体。值对象Value Object的核心特征是“没有唯一标识由属性值本身定义”。比如金额Money(100, CNY)两个金额只要币种和数值相同就是同一个金额不需要给每个金额分配 ID。地址、电话号码、坐标、颜色也都是典型值对象。为什么要在建模时区分它们因为现实业务中很多我们以为的实体其实是值对象。比如“商品地址”如果被建模成每个地址都有 ID 的实体就会出现两个完全相同的地址因为 ID 不同而被视为不同数据还要额外维护一张地址表。正确做法是把“地址”建模成值对象直接嵌在订单实体里整个订单下的地址变了就整体替换。实操时的判断口诀一个对象有两个属性完全一样能否认为它们就是同一个东西能就是值对象不能就是实体。这个对象需要被单独查询、单独修改、单独追踪历史吗不需要值对象需要实体。2.2 聚合与聚合根给实体分组定边界如果说实体和值对象是零件聚合就是把零件组装成机器的规则。聚合Aggregate是一组相关对象的集合聚合根Aggregate Root是这个集合的入口和唯一访问点。举个例子经典的下单模型订单 Order 是聚合根订单项 OrderItem 是聚合子实体收件地址 Address 是值对象。你操作订单时不能绕过 Order 直接改某个 OrderItem 的数量、不能直接改订单下的地址。所有修改都要通过 Order 的方法order.changeItemQuantity(itemId, newQty)、order.updateAddress(newAddress)。这样能保证不变量订单总金额始终等于所有订单项金额之和新增订单项时自动重算总额地址变更时若有已发货状态则禁止操作。聚合的意义在于划清一致性和事务边界一个聚合内保证强一致跨聚合用最终一致。我见过很多团队把“订单”和“订单项”分成两个独立 Aggregate然后为了统计订单项总额跨聚合查询最后不得不在 Service 里拼数据——这就是聚合边界划错了。聚合设计三条经验聚合要小。优先把聚合设计得足够小减少锁范围和一致性成本。通过聚合根访问内部对象禁止外部直接修改子对象。跨聚合引用用 ID不用对象引用。比如订单里保存“用户ID”字段就够了不要放整个 User 对象。新手最容易犯的错一上来就设计一个巨大的 Customer 聚合包含订单、地址、优惠券、积分最后这个聚合根的代码膨胀到没法维护。聚合边界不是越大越好是越清晰越好。2.3 领域服务与领域事件处理“不属于某个实体的逻辑”不是所有业务逻辑都能安放到实体或聚合上。比如“转账”从 A 账户扣款、往 B 账户加钱还要保证这事要么都成功要么都不成功。这笔逻辑放在 A 账户实体里不合适放在 B 账户里也不合适它横跨了两个聚合。这时候就要用领域服务Domain Service。领域服务是领域层的一部分它包含真正的业务规则但没有自己的状态。命名上习惯用动词或动宾结构TransferService、PriceCalculationService。需要注意的是领域服务不是万能收纳层。如果一个逻辑能放到实体方法里优先放实体只有纯粹因为技术限制比如跨聚合事务、需要调用外部接口才放领域服务。另一个容易混淆的概念是领域事件Domain Event。领域事件表示“领域里发生了一件值得关注的事”比如OrderPlaced、PaymentReceived。它不是技术层面的消息通知而是领域层用来表达“这件事已经发生”的事实并表示后续动作不应该再继续塞进同一个事务里。典型场景下单成功后需要发邮件、给推荐系统发数据、更新统计。如果不使用领域事件这些动作会全部堆在应用层下单方法越来越长。正确做法是Order 聚合根在placeOrder()方法里产生一个OrderPlacedEvent应用层捕获这个事件后异步发给下游。领域层只负责表达“发生了 What”不负责知道“谁会关心”从而降低耦合。实现上领域事件通常是不可变对象包含事件 ID、发生时间、相关聚合 ID 和业务数据。事件命名用过去时态。2.4 仓储与工厂让领域层不碰数据库仓储Repository是领域层的接口语义非常像集合Save(order)、FindById(orderId)、Remove(order)。领域层定义仓储接口基础设施层实现它。这样领域层在执行业务逻辑时可以很自然地说“给我一个订单”而不需要说“用哪些表 join 出来一个订单”。仓储应该只针对聚合根设计不为每个实体都建仓储。如果一个子实体没有聚合根就不能单独存在那也不需要单独仓储。我在项目里见过有人给OrderItem也建了个OrderItemRepository结果业务代码里可以绕过 Order 直接修改订单项聚合的边界形同虚设这就是明显的设计问题。工厂Factory用来封装创建复杂对象/聚合的细节。如果一个聚合初始化时要设置多个子实体、要校验多个不变量、要生成 ID这些逻辑放在构造方法里会让调用方很难看懂。工厂可以是独立的类比如OrderFactory.Create(orderId, customerId, items)也可以用聚合根的静态工厂方法Order.create(...)。现在我更推荐后者语义更清晰也更符合现代 DDD 社区的实践。需要强调的是“存储”与“创建”这两个概念在四层模型里都归领域层管但实现一定在基础设施层或应用层调用侧。领域层只拥有它们的接口和契约这在后面第三部分还会展开。3. 应用层与基础设施层的职责边界3.1 应用层编排而不是塞逻辑应用层是很多人最容易写坏的一层。常见的坏味道有两种一种是应用层没有任何逻辑控制器直接调仓储另一种是应用层承载了所有逻辑领域层只剩一堆 POJO。正确的位置是中间应用层负责协调用例流程、事务边界、权限检查、调用外部系统但不写业务规则。拿“下单”场景举例应用层的CreateOrderUseCase大概长这样从认证上下文获取用户 ID。从仓储加载购物车可能是另一个聚合。调用订单工厂创建 Order传入购物车中的商品、数量、地址。校验是否满足该用户的限购规则规则本身在领域层应用层只触发校验。保存 Order。发布 OrderPlacedEvent。你观察这里面应用层做的全是“安排谁做、什么时候做”而“订单创建后总价怎么算”“库存是否足够”这些规则都在领域层对象的方法里定。很多团队的分层之所以乱就是因为应用层把手伸进领域层内部直接 get 出聚合内的 List 然后在外层算总价。一旦这么写领域层的封装就破了。应用层的类一般按用例划分一个用例一个类命名用 UseCase 后缀或者带动词的服务类。这样用例列表就是系统功能列表新人看代码能快速建立全局认知。3.2 基础设施层把技术细节藏起来基础设施层是四层里最容易被误解的一层很多人把“基础设施层在最底部”理解成“所有层都可以依赖它”于是 Controller 里直接调 RepositoryImpl、Service 里直接拼 SQL、领域层里用 JPA 注解——全乱套。正确的关系我再说一遍基础设施层的实现类实现领域层接口所以基础设施层要依赖领域层但反向依赖是不允许的。你可以这样想基础设施层就像电源插座领域层定义了插头的形状接口电源插座必须按这个形状去造而不是让插头去迁就插座。实际项目中基础设施层包含仓储实现JPA/Mapper/Redis。外部服务适配器比如调用支付网关、短信服务的客户端。消息队列的生产者/消费者适配。领域事件的发布实现。配置、加密、文件存储等通用技术组件。基础设施层是否用 Spring、是否用 MyBatis 还是 JPA都不会影响领域层。换一个说法领域层的代码里不应该出现Transactional、Table、Service这类框架注解个别团队会允许在实体上少量使用 JPA 注解但我个人不建议理由后面讲。3.3 接口层用户界面或 API 层怎么做接口层的职责是“翻译”。HTTP 请求进来它把 DTO 转换成应用层/领域层能理解的对象领域层的返回结果再转成 JSON 输出给前端。它不做业务判断不直接操作仓储。这里有一个常见问题DTO 能不能直接复用领域实体我的答案是分情况。如果是简单的 CRUD 系统可以适当简化如果是业务流程复杂、领域模型丰富的系统我强烈建议在接口层使用独立 DTO。因为 DTO 是给外部消费的契约领域实体是内部实现细节两者变化频率不同强行绑定会让外部接口被内部重构拖累。接口层的典型职责清单参数校验字段格式、必填项但复杂业务校验不在这里。身份认证与权限粗粒度拦截。调用应用层的方法获取结果。把领域层的异常翻译成 HTTP 状态码和错误结构。常见的烂代码特征Controller 里写 if 判断业务规则或者直接注入仓储。出现这种情况说明接口层“上头了”。4. 四层模型与六边形架构、整洁架构的关系4.1 三种架构的共性都是依赖倒置的实践网上经常出现“DDD 四层模型过时了现在都用六边形架构”的说法。说白了这些架构画法不同但核心思想完全一致把业务规则放在最中间让技术和外部依赖都在外面通过端口接口与适配器实现通信。四层模型强调“分层”从上到下接口、应用、领域、基础设施最核心的依赖规则是内层不依赖外层。六边形架构也叫端口-适配器架构把系统画成一个正六边形内部是应用和领域边界上的端口是入站/出站接口外围适配器是 REST Controller、消息监听器、仓储实现等。它的优点是不再区分“上下”而是“内外”所有外部都通过端口接进来。整洁架构Clean Architecture更抽象从内到外是实体、用例、接口适配器、框架与驱动最外层是框架。它强调的依赖规则同样是“由外向内依赖”越往里越纯净。DDD 的四层领域模型和六边形架构不是互斥关系而是视角不同。四层模型适合理解“每一层该放什么”六边形架构适合理解“如何把系统与外部世界隔离”。在我实际项目里两者是结合用的用四层的职责分工来组织代码结构用端口-适配器的思想来设计基础设施层的挂接方式。4.2 实际落地怎么选当你面对一个全新项目考虑用哪种架构时我的建议是不要纠结画法先确定你的业务复杂度。如果业务很简单基本都是增删改查CRUD 为主上四层模型很容易显得“重”。此时我甚至建议只分两个模块应用服务模块和数据访问模块先把业务跑起来等业务规则真的复杂了再引入领域层。如果业务规则中等偏上有几个核心聚合需要多个系统协作那就直接上四层模型。包结构我都给你参考com.company.project ├── interfaces │ ├── controller │ ├── dto │ └── assembler ├── application │ ├── usecase │ ├── dto │ └── service ├── domain │ ├── model │ │ ├── entity │ │ ├── valueobject │ │ ├── aggregate │ │ └── events │ ├── service │ └── repository └── infrastructure ├── persistence ├── mq ├── client └── config如果系统有大量外部集成消息队列、第三方 API、定时任务、事件流那就重点参考六边形架构把每个外部系统都当成一个端口适配器来处理。此时四层模型中的接口层和基础设施层会被拆成多个独立的适配器模块而应用层和领域层保持纯净。这种做法在微服务环境下尤其好使每个服务都能独立演进。5. 从“搞不懂”到“能落地”实操要点与常见坑5.1 落地步骤别一步到位分四步演进我见过太多团队引入 DDD 失败的共性第一步就让全员学概念第二步就要求所有新代码按 DDD 写第三步就开始重构老系统结果一个月后发现没法收场。我建议按下面四步走第一步画清业务边界。先不写代码和产品、业务方一起梳理核心业务概念。找出哪些是实体、哪些是值对象画出最初的聚合边界。这一步的重点是统一语言Ubiquitous Language让产品叫的“订单”和研发代码里的 Order 是同一个概念。第二步从单个用例试点。找一个业务规律明确、又不算太复杂的核心用例比如“创建订单”“提交审批”用四层结构完整走通一版。这版不必覆盖所有异常边界先验证分层结构和依赖倒置在团队里能跑通。第三步建立代码规范。用 ArchUnit 或类似工具把依赖规则固化成测试。比如写一个测试保证 domain 包不依赖 infrastructure 包application 不依赖 interfaces 包。这样后续团队任何人提交代码CI 上就能拦住依赖倒置问题不需要靠人工 review 去“劝说”。第四步渐进式重构老系统。老系统不是一天改完的按聚合边界一个一个拆每次只动一个用例。先让领域层接口出现基础设施层实现新接口应用层从调用旧 DAO 改成调用仓储最后再删除旧代码。5.2 常见问题与排查技巧实录下面这些问题都是我实际项目里反复出现过的整理成速查表症状根因排查思路领域层 import 了 Spring 注解或 MyBatis 类依赖倒置被打破打开领域层包搜索外部框架 import全部移除应用层太肥方法超过两三百行业务规则被放到了应用层看逻辑是否涉及业务不变量是则下沉到领域层聚合方法实体被大量 getter/setter 暴露聚合封装失效把修改行为封装为有意义的方法如changeStatus()禁止直接 setController 里出现仓储调用分层混乱调整代码Controller 只调应用层用例数据库改一个字段前端接口也跟着变DTO 和领域实体混用接口层引入独立 DTO做字段映射一个事务里操作了多个聚合聚合边界可能太大或太小优先缩小事务范围跨聚合改用领域事件或最终一致性Repository 接口定义在基础设施层依赖方向反了把接口提到领域层实现留在基础设施层实体用 JPA 注解领域层和表结构强绑定基础设施污染领域层去掉注解用独立持久化模型或轻量映射排查的顺序也有讲究先看依赖方向再看事务边界最后看聚合封装。依赖方向是根这个错了其他都会连锁反应。5.3 我对 DDD 落地的一句真心话最后说点软件工程之外的事。DDD 这十几年在中国开发者圈子里热度起起伏伏有人奉为圭臬有人说“过度设计”。我自己的体会是DDD 从来不是银弹它是一套“逼你想清楚业务再做技术决策”的方法论。它解决的是业务复杂度带来的维护成本而不是技术复杂度带来的性能问题。如果一个团队连需求都讲不清楚、连基本的单元测试都没有引入那先不要碰 DDD先回去把工程基础打好。真正能落地的 DDD 项目通常不是架构师一个人画出来的而是业务专家、产品经理、开发一起把语言对齐后长出来的。我见过最好的 DDD 代码库它的领域层代码甚至能让不懂技术的产品经理看懂大半——因为里面只有业务规则没有框架噪声。那种“读代码像读业务说明书”的感觉才是四层领域模型真正想带给我们的东西。你在自己项目里如果现在正被“ddd搞不懂”困扰我的建议很简单不要追求一开始就把四层模型设计得完美。先拿一个核心用例把接口层、应用层、领域层、基础设施层这四个包建出来遵循一条硬规则“只允许内层方向的依赖”剩下的业务会教你怎么走。
返回列表