前言
领域驱动设计(DDD)是一种软件开发方法论,其核心思想是将业务领域的知识和业务逻辑融入到软件设计和开发中,以实现更加符合业务需求和更易于维护的软件系统。
在传统的软件开发方法中,开发人员往往关注的是技术实现而非业务领域本身。而在DDD的方法论中,开发人员需要与业务专家紧密合作,深入了解业务领域,将业务领域的知识和业务逻辑转化为软件设计和开发中的概念和实现。
DDD的核心概念包括:
- 领域模型:对业务领域进行建模,将业务领域中的概念和规则转化为软件系统中的对象和方法。
- 实体:具有唯一标识和生命周期的领域对象。
- 值对象:没有唯一标识和生命周期的领域对象。
- 聚合:一组具有内聚性的实体和值对象的集合。
- 领域服务:对领域模型的操作和行为进行抽象和封装的服务。
- 限界上下文:领域模型的上下文边界,规定了领域模型中的概念和规则的适用范围。
通过将业务领域的知识和业务逻辑融入到软件设计和开发中,DDD可以帮助开发人员实现更加符合业务需求的软件系统,并提高软件系统的可维护性、可扩展性和可测试性。
一、从六边形架构谈起
六边形架构是一种软件架构,用于为每种外部类型提供一个适配器。它可以帮助我们从新的角度来看待整个系统,并将系统分为外部区域和内部区域两个部分。
外部区域是指处理不同客户端的输入请求的部分。它包含了各种适配器,用于将不同类型的客户输入转化为程序内部API所理解的输入。在六边形架构中,每种类型的客户都有自己的适配器。其中,每种适配器对应着一个不同种类型的端口,端口要么处理输入,要么处理输出。无论采用哪种方式对端口进行划分,当客户请求到达时,都应该有相应的适配器对输入进行转化,然后适配器将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给内部区域。
内部区域是指负责处理持久化数据并对程序输出进行存储和转发的部分。它包含了应用层,领域层和基础设施层。
- 应用层 是整个系统的业务逻辑层,它负责接收用户的请求,调用领域层模型和服务完成业务逻辑,然后将结果返回给用户接口层。应用层可以包含如下内容:service、command、query、dto和mq等。
- 领域层 是整个系统的核心,它包含了系统的业务规则和业务逻辑。领域层的核心是领域模型,它是对业务领域的建模。领域层可以包含如下内容:model、service、repository、event和facade等。
- 基础设施层 是整个系统的基础设施,它包含了与具体技术相关的代码和逻辑。基础设施层可以包含如下内容:dal、mapper和factory等。
六边形架构是一种非常灵活和通用的架构,可以帮助我们更好地组织和管理大型软件系统。
二、依赖倒置
依赖倒置原则(DIP)由Robert C. Martin提出,其核心定义如下:
- 高层模块不应该依赖于底层模块,两者都应该依赖于抽象。
- 抽象不应该依赖于实现细节,实现细节应该依赖于接口。
根据DIP原则,领域层可以不再依赖于基础设施层,基础设施层通过注入持久化实现完成对领域层的解耦。采用依赖注入原则的新分层架构模型如下:
采用依赖注入方式后,我们可以发现实际上已经没有分层概念了。无论是高层还是底层,都只依赖于抽象,整个分层结构被推平了。
三、聚合
在DDD中,聚合是指一组具有内聚性的实体和值对象的集合。它们共同形成了一个有边界的上下文,这个上下文可以看作是一个单个的单元。这个单元可以通过聚合根(Aggregate Root)进行访问和修改,聚合根是聚合中的唯一访问点。聚合根担任了保护聚合内部完整性和一致性的角色。
聚合的设计目的是保持领域模型的内聚性和一致性。将相关实体和值对象聚合在一起,可以更好地保护和管理领域模型,减少了并发冲突的可能性,提高了系统的可维护性。
在聚合内部,实体和值对象的访问应该受到限制,只能通过聚合根进行访问。这个限制可以通过使用封装和访问控制等技术来实现。聚合根应该提供足够的方法来支持聚合的业务需求,同时也应该避免暴露过多的实现细节。
在设计聚合时,需要注意以下几个原则:
- 通过一致性边界内建模真正的不变条件来封装实体的不变性,实现对象数据的一致性。该原则保证了聚合的业务高内聚性。
- 采用设计小聚合的方式来降低实体之间管理的复杂性,避免高并发操作带来的冲突和数据库锁等问题。小聚合设计也提高了领域模型的适应性,以适应业务变化。
- 通过唯一标识引用其它聚合,避免直接对象引用的方式。该原则减少了聚合之间的耦合度,避免聚合边界不清晰的问题。
- 在边界之外使用最终一致性,保证聚合内部数据的强一致性,而聚合之间数据的最终一致性。该原则通过异步修改相关聚合的领域事件来实现聚合之间的解耦。
- 通过应用层实现跨聚合的服务调用,避免跨聚合的领域服务调用和跨聚合的数据库表关联。该原则实现了微服务内聚合之间的解耦,支持未来以聚合为单位的微服务组合和拆分。
聚合根、实体、值对象
在DDD中,聚合是一组相关的对象的集合,它们具有内在的一致性和完整性规则,并且共享一个边界。
- 聚合根:
ProductAggregateRoot
- 聚合根是聚合中的唯一访问点,担任了保护聚合内部完整性和一致性的角色。在本示例中,
ProductAggregateRoot
是整个聚合的入口,提供了增删改查等操作。 - 聚合根是聚合中最重要的对象,因为它是聚合的边界,也是聚合内部所有对象之间的协调者。
- 聚合根可以包含多个实体和值对象。实体和值对象都是聚合根的子对象。
- 聚合根是聚合中的唯一访问点,担任了保护聚合内部完整性和一致性的角色。在本示例中,
- 实体:
Product
- 实体是具有唯一标识和生命周期的领域对象。在本示例中,
Product
表示商品对象。 - 实体是聚合中最重要的对象之一,因为它们是聚合的核心和主要参与者。
- 实体可以包含多个值对象。值对象通常作为实体的属性存在,用于描述实体的某个方面。
- 实体之间可以相互引用。这种引用通常是通过聚合根进行的,以确保聚合根在协调实体之间的关系时发挥其作用。
- 实体是具有唯一标识和生命周期的领域对象。在本示例中,
- 值对象:
ProductSpec
- 值对象是没有唯一标识和生命周期的领域对象。在本示例中,
ProductSpec
表示商品规格对象。 - 值对象通常用于描述实体的某个方面,例如商品的重量、颜色、尺寸等等。
- 值对象不能单独存在,它们总是作为实体的属性存在。
- 值对象的相等性仅由其属性值决定,不同值对象之间可以相等。这意味着如果两个值对象的属性值相同,它们被认为是相等的,即使它们不是同一个对象。
- 值对象是没有唯一标识和生命周期的领域对象。在本示例中,
四、DDD 分层
整体架构图
整体代码结构
ddd-domin
├── pom.xml
└── src
└── main
└── java
└── org
└── example
├── App.java
├── adapter
│ ├── market
│ └── product
│ ├── kafka
│ │ └── ProductConsumer.java
│ ├── socket
│ │ └── ProductSocket.java
│ └── web
│ └── ProductController.java
├── application
│ ├── event
│ │ ├── EventManager.java
│ │ └── IEvent.java
│ ├── market
│ └── product
│ ├── ProductFactory.java
│ ├── ProductService.java
│ ├── command
│ │ ├── AddCountProductCommand.java
│ │ └── CreateProductCommand.java
│ ├── dto
│ │ └── ProductDTO.java
│ ├── event
│ │ ├── AbstractProductEvent.java
│ │ ├── AddNumProjectEvent.java
│ │ └── CreateProjectEvent.java
│ └── mapstruct
│ └── ProductStruct.java
├── common
│ └── exception
│ └── BusException.java
├── domain
│ ├── market
│ ├── product
│ │ └── Product.java
│ └── repository
│ ├── IProductRepository.java
│ └── entity
│ └── ProductEntity.java
└── infrastructure
├── cache
├── repository
│ └── impl
│ └── ProductRepositoryImpl.java
└── sqlmapper
3.1 用户接口层
用户接口层作为对外的门户,将网络协议与业务逻辑解耦。它是整个系统的重要组成部分,可以包含如下内容:
- 鉴权:验证用户的身份和权限,保证系统的安全性。
- Session管理:管理用户的会话,保证用户操作的一致性。
- 限流:限制用户的访问频率,保证系统的稳定性。
- 异常处理:处理用户请求过程中可能出现的异常,保证系统的健壮性。
在项目应用中,用户接口层仅负责封装协议请求的处理,鉴权、Session管理和限流等其他任务则由独立的应用程序负责。这种设计方法确保了用户接口层不会负担过多的额外职责,使其可以专注于处理请求和响应。通过将鉴权、Session管理和限流等任务委托给独立的应用程序,整个系统可以更好地组织和更加灵活,从而更易于维护和扩展。
3.2 应用层
应用层是整个系统的业务逻辑层,是领域层和用户接口层之间的桥梁。应用层接收用户的请求,调用领域层模型和服务完成业务逻辑,然后将结果返回给用户接口层。应用层可以包含如下内容:
- service:应用层服务接口定义。它定义了应用层的服务接口,包括对外提供的方法和参数等。
- command:命令定义,例如订单创建、商品更新等。它定义了应用层的命令类型,包括命令的名称、参数等。
- query:查询定义,例如商品详情查询、订单列表查询等。它定义了应用层的查询类型,包括查询的名称、参数等。
- dto:数据传输对象定义。它定义了应用层的数据传输对象,包括数据的类型、属性等。
- mq:消息队列定义。它定义了应用层的消息队列,包括消息的类型、属性等。
3.3 领域层
领域层是整个系统的核心,它包含了系统的业务规则和业务逻辑。领域模型是领域层的核心,它是对业务领域的建模。领域层可以包含如下内容:
- model:领域模型定义。它定义了领域模型的类型、属性等。
- service:领域服务接口定义。它定义了领域层的服务接口,包括对外提供的方法和参数等。
- repository:领域仓储接口定义。它定义了领域层的仓储接口,包括对数据的读取、写入等操作。
- event:领域事件定义。它定义了领域层的事件类型,包括事件的名称、参数等。
- facade:领域门面定义,负责领域模型和应用层服务的协调。它定义了领域层的门面类型,包括门面的名称、方法等。
在我们的项目中,我们遵循领域驱动设计(DDD)的方法,并在不同层之间进行了明确的关注点分离。Repository 层仅负责处理聚合根相关的操作,而不是查询。因此,我们决定将查询从应用层直接连接到基础设施层。通过这样做,我们确保领域层可以更专注于实体操作,而不会被查询所干扰。
将查询移到基础设施层中允许我们在实现和可扩展性方面具有更大的灵活性。我们可以选择最适合处理查询的技术,并优化系统的性能。
总的来说,这个设计决策通过分离职责并确保每一层都有明确的目的,提供了一个更健壮和可维护的系统。
3.4 基础设施层
基础设施层是整个系统的基础设施,它包含了与具体技术相关的代码和逻辑。基础设施层可以包含如下内容:
- dal:数据访问层,包括DO和DAO。它定义了数据访问层的类型、属性等。
- mapper:数据映射定义。它定义了数据映射的类型、属性等。
- factory:工厂定义,例如对象工厂、配置工厂等。它定义了工厂的类型、属性等。