初始DDD

Mr.LR2023年5月1日
大约 8 分钟

初识DDD

查阅相关资料,DDD的专业名词特别多,在不结合代码和业务的场景下,理解起来很困难。本文从代码的角度出发,简述了DDD 四层架构的具体职责

服务分层

image-20231009224751474

这些目录的职能和代码形态是这样的。

  • Interfaces(用户接口层): 它主要存放用户接口层与前端交互、展现数据相关的代码。

    和前端直接交互的逻辑在这里,例如:controller DTO 等相关代码放在这里。

  • Application(应用层): 它主要存放应用层服务组合和编排相关的代码。

    应用服务向下基于微服务内的领域服务或外部微服务的应用服务完成服务的编排和组合,向上为用户接口层提供各种应用数据展现支持服务。应用服务和事件等代码会放在这一层目录里。应用层就是为了快速的承接需求迭代,一般需求变更,个性化的逻辑放到应用层。

    编排业务用例的执行流程。调用领域层处理业务逻辑。管理跨领域的事务,例如启动和提交事务。

  • Domain(领域层): 它主要存放领域层核心业务逻辑相关的代码。

    领域层是整个系统的核心,承载了主要的业务逻辑,所有的业务规则都应该集中在这一层。

    领域层可以包含多个聚合代码包,它们共同实现领域模型的核心业务逻辑。聚合以及聚合内的实体、方法、领域服务和事件等代码会放在这一层目录里。

  • Infrastructure(基础层): 它主要存放基础资源服务相关的代码

    为其它各层提供的通用技术能力、三方软件包、数据库服务、配置和基础资源服务的代码都会放在这一层目录里。

用户接口层

Interfaces 的代码目录结构有:assembler、dto 和 façade 三类

image-20231009225106824

  • Assembler: 实现 DTO 与领域对象之间的相互转换和数据交换。一般来说 Assembler 与 DTO 总是一同出现。
  • Dto: 它是数据传输的载体,内部不存在任何业务逻辑,我们可以通过 DTO 把内部的领域对象与外界隔离。
  • Facade: 提供较粗粒度的调用接口,将用户请求委派给一个或多个应用服务进行处理。

常见问题

  • 错误的职责归属:将业务逻辑放入 Controller,这是错误的,逻辑应在领域层实现。

应用层

Application 的代码目录结构有:event 和 service

image-20231009225304144

  • **Event(事件):**这层目录主要存放事件相关的代码。

    它包括两个子目录:publishsubscribe。前者主要存放事件发布相关代码(生产者),后者主要存放事件订阅相关代码(消费者)(事件处理相关的核心业务逻辑在领域层实现)。

    这里提示一下:虽然应用层和领域层都可以进行事件的发布和处理,但为了实现事件的统一管理,我建议你将微服务内所有事件的发布和订阅的处理都统一放到应用层,事件相关的核心业务逻辑实现放在领域层。通过应用层调用领域层服务,来实现完整的事件发布和订阅处理流程。

    事件主要和消息队列等相关

  • **Service(应用服务):**这层的服务是应用服务。

    应用服务会对多个领域服务或外部应用服务进行封装、编排和组合,对外提供粗粒度的服务。应用服务主要实现服务组合和编排,是一段独立的业务逻辑。你可以将所有应用服务放在一个应用服务类里,也可以把一个应用服务设计为一个应用服务类,以防应用服务类代码量过大。

    关键概念

    • 应用服务:定义和实现用例相关的操作,每个服务通常对应一个用例。
    • 事务管理:确保用例中的所有操作要么成功,要么全部回滚。
    • DTO 映射:将领域对象转换为界面层可用的 DTO。

    常见问题

    • 过于复杂的逻辑:应将业务逻辑委托给领域层,应用层只负责调用和协调。
    • 事务边界模糊:建议在应用服务层管理事务,保持领域层的纯净。

领域层

Domain 是由一个或多个聚合包构成,共同实现领域模型的核心业务逻辑。聚合内的代码模型是标准和统一的,包括:entity、event、repository 和 service 四个子目录。

image-20231009225558152

而领域层聚合内部的代码目录结构是这样的。

  • Aggregate(聚合): 它是聚合软件包的根目录,可以根据实际项目的聚合名称命名,比如权限聚合。

    在聚合内定义聚合根、实体和值对象以及领域服务之间的关系和边界。聚合内实现高内聚的业务逻辑,它的代码可以独立拆分为微服务。

    以聚合为单位的代码放在一个包里的主要目的是为了业务内聚,而更大的目的是为了以后微服务之间聚合的重组。聚合之间清晰的代码边界,可以让你轻松地实现以聚合为单位的微服务重组,在微服务架构演进中有着很重要的作用。

  • Entity(实体): 它存放聚合根、实体、值对象以及工厂模式(Factory)相关代码。

    实体类采用 充血模型,同一实体相关的业务逻辑都在实体类代码中实现。跨实体的业务逻辑代码在领域服务中实现。

  • Event(事件): 它存放事件实体以及与事件活动相关的业务逻辑代码。

  • Service(领域服务): 它存放领域服务代码。

    一个领域服务是多个实体组合出来的一段业务逻辑。你可以将聚合内所有领域服务都放在一个领域服务类中,你也可以把每一个领域服务设计为一个类。如果领域服务内的业务逻辑相对复杂,我建议你将一个领域服务设计为一个领域服务类,避免由于所有领域服务代码都放在一个领域服务类中,而出现代码臃肿的问题。领域服务封装多个实体或方法后向上层提供应用服务调用。

  • Repository(仓储): 它存放所在聚合的查询或持久化领域对象的代码,通常包括仓储接口和仓储实现方法。为了方便聚合的拆分和组合,我们设定了一个原则:一个聚合对应一个仓储。

特别说明:按照 DDD 分层架构,仓储实现本应该属于基础层代码,但为了在微服务架构演进时,保证代码拆分和重组的便利性,我是把聚合仓储实现的代码放到了聚合包内。这样,如果需求或者设计发生变化导致聚合需要拆分或重组时,我们就可以将包括核心业务逻辑和仓储代码的聚合包整体迁移,轻松实现微服务架构演进。

常见问题

  • 贫血模型:将领域层简化为数据结构,逻辑被分散到其他层,领域模型失去了价值。
  • 越权访问:直接操作聚合内部对象,破坏聚合的封装性。

基础层

Infrastructure 的代码目录结构有:config 和 util 两个子目录。

image-20231009225828975

  • Config: 主要存放配置相关代码。

  • Util: 主要存放平台、开发框架、消息、数据库、缓存、文件、总线、网关、第三方类库、通用算法等基础代码,你可以为不同的资源类别建立不同的子目录

  • 防腐层(Anti-Corruption Layer):适配外部系统,避免外部系统的影响渗透到领域层。

常见问题

  • 基础设施层污染领域层:应通过接口隔离,避免技术细节泄漏到领域层。
  • 过度依赖外部框架:框架应服务于领域层,而不是领域层依赖于框架。

总结

层次职责典型角色
用户接口层处理用户交互,调用应用层,并返回结果Controller, ViewModel, DTO
应用层用例驱动,协调领域对象完成业务逻辑Application Service, DTO
领域层核心业务逻辑与规则,表达领域模型,处理持久化Entity, Value Object, Service,Repository
基础设施层提供技术支持,外部系统交互等Adapter, EventBus

通过遵循四层架构设计,可以实现职责清晰解耦良好领域驱动的业务系统,大幅提高系统的可维护性和扩展性。