01.DDD领域驱动设计
# 01.DDD领域驱动设计
# 1、DDD概述
- 领域驱动设计是一种
软件开发方法
,它强调专注于业务领域的核心概念和逻辑
- DDD 的目标是通过更好地
理解业务领域来创建高度可维护、灵活和可扩展的软件系统
- 领域驱动设计的核心思想包括
领域模型、限界上下文、实体、值对象、聚合根
等概念
领域模型
- 业务的抽象表示,包括业务规则、实体和流程,帮助团队理解和建模业务领域
- 例:描述了
购物车、订单和支付等业务概念
实体和值对象
Order
(订单)具有唯一标识是实体
Product
(商品)没有唯一标识 是值对象
聚合和聚合根
Order
是一个聚合根,管理订单和其商品项
限界上下文
不同子系统
有不同的领域模型
,通过限界上下
文来确定某一领域模型的边界
订单管理
和支付管理
是不同的限界上下文,各自有独立的领域模型
领域服务
- 处理特定业务逻辑的服务,通常不属于实体或值对象
- 例:
PaymentService
(支付服务)负责处理支付逻辑
事件驱动架构
- 通过事件传递系统中的重要事务,支持松耦合、异步、分布式系统
- 例:
OrderPaid
事件触发库存更新、发货等操作
# 2、贫血和充血模型
# 1) 贫血模型
贫血模型中
业务逻辑被分散到外部的服务类中,实体对象仅作为数据容器,没有行为这样会导致
实体的行为和状态变得不一致
,无法很好地维护复杂的业务规则
例:
Order实体
:只存储数据(OrderID
,Status
,TotalAmount
)。OrderService
:- 包含所有业务逻辑(支付和发货)
Order
只是数据容器,所有行为被移到OrderService
中
贫血模型 - 订单实体
// 贫血模型 - 订单实体 type Order struct { OrderID string Status string TotalAmount float64 }
1
2
3
4
5
6
OrderService
包含所有业务逻辑(支付和发货)// 贫血模型 - 订单服务 type OrderService struct{} func (s *OrderService) PayOrder(order *Order) error { if order.Status != "CREATED" { return errors.New("order cannot be paid") } order.Status = "PAID" fmt.Println("Order Paid:", order.OrderID) return nil } func (s *OrderService) ShipOrder(order *Order) error { if order.Status != "PAID" { return errors.New("order cannot be shipped") } order.Status = "SHIPPED" fmt.Println("Order Shipped:", order.OrderID) return nil }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2) 充血模型
充血模型
中实体不仅包含数据
,还包含与数据相关的业务逻辑
通过
将行为封装到实体中
,充血模型让领域模型更加完整
,并且更加符合DDD的设计原则例:
Order实体
:除了包含数据外,Order
实体还包含了支付(Pay
)和发货(Ship
)的方法所有业务逻辑都在
Order
实体中,订单的支付和发货由Order
本身来管理,而不是分散到外部的服务类中
说明:
- 订单领域可能需要与支付领域交互,可以通过一个
PaymentService
来处理支付逻辑 - 支付领域可能通过
事件监听
来响应订单的支付,而不是由订单直接调用支付方法
- 订单领域可能需要与支付领域交互,可以通过一个
// 充血模型 - 订单实体
type Order struct {
OrderID string
Status string
TotalAmount float64
}
func (o *Order) Pay() error {
if o.Status != "CREATED" {
return errors.New("order cannot be paid")
}
o.Status = "PAID"
fmt.Println("Order Paid:", o.OrderID)
return nil
}
func (o *Order) Ship() error {
if o.Status != "PAID" {
return errors.New("order cannot be shipped")
}
o.Status = "SHIPPED"
fmt.Println("Order Shipped:", o.OrderID)
return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 3、DDD项目结构
server
├── interfaces # 第一层:接口层
│ ├── assembler # 实现 DTO 数据传输对象与 Domain Entity 之间的相互转换和数据交换
│ ├── controller # 控制器路由函数
│ └── dto/vo # 可包含多个领域对象的属性:DTO(Data Transfer Object)主要关注数据的传输
├── application # 第二层:业务调度层
│ ├── event # 微服务事件推送或订阅
│ │ ├── publish # 事件发布
│ │ └── subscribe # 事件订阅
│ └── service # 用于连接 Controller 和 Domain,进行三方接口调用等其他操作
├── domain # 第三层:领域服务层(领域逻辑和领域对象,主要的业务逻辑,采用充血模型)
│ ├── aggregate01 # Aggregate 聚合根目录
│ │ ├── entity # entity 实体、VO 值对象以及工厂模式(Factory)相关
│ │ ├── event # 事件发布和订阅
│ │ ├── repository # 仓储:持久化领域对象
│ │ └── service # 领域服务代码
│ ├── aggregate02
│ └── ...
├── infrastructu # 第四层:基础设施层
│ ├── api # 第三方 API/SDK
│ ├── configs # 配置参数变量
│ ├── database # 初始化数据库
│ ├── mq # 消息队列连接和配置
│ ├── persistence # 数据持久化(Domain 层 repository 的具体实现,数据库 CRUD 操作)
│ └── pkg # 工具函数
│ ├── common # 与业务相关包
│ └── utils # 公共基础包
└── main.go # 主入口
├── go.mod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 4、DDD凤城架构图
# 02.DDD模型举例
# 0、事例
├── domain // 领域层
│ └── demo
│ ├── agg // 聚合
│ │ ├── agg1.go
│ │ ├── aggroot.go
│ │ └── readme.md
│ ├── entity // 实体
│ │ ├── entity.go
│ │ └── readme.md
│ ├── repo // 仓储
│ │ ├── readme.md
│ │ └── repo.go
│ ├── service // 领域服务
│ │ ├── readme.md
│ │ └── svc.go
│ └── vo // 值对象
│ ├── readme.md
│ └── vo.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1、领域事件(Domain Event)
- 领域事件是领域驱动设计中的一个重要概念,它用于表示系统中发生的重要业务变化或状态转换
- 领域事件是对业务领域中某一事实或情境的描述,它通常是由业务专家或领域内的相关人员定义的,而不是由技术团队来确定
订单创建事件
描述:当客户成功下单时,系统会发布一个“订单创建”事件。
示例:
OrderCreatedEvent
,包含订单号、客户信息、商品列表等关键信息。
库存不足事件
描述:当系统检测到某个商品库存不足时,触发“库存不足”事件。
示例:
InventoryInsufficientEvent
,包含商品ID、当前库存数量、需要的最小库存量等信息。
# 2、聚合(Aggregate)
# 1、聚合定义
此目录下存放聚合(Aggregate)与聚合根(Aggregate Root)
聚合根
聚合根
通常是一个包含业务逻辑和数据的结构体或接口
- 它代表了整个聚合的入口点,并负责维护聚合内部对象之间的一致性
- 在该项目实践中,我们将
聚合根定义为一个接口
,该接口包含了聚合的所有业务方法,以及聚合内部对象的访问方法
聚合
聚合是一组相关的对象的集合
,它们一起执行一个特定的业务功能
- 聚合根是聚合的入口点,通过聚合根可以访问聚合内部的所有对象
- 在该项目实践中,我们将
聚合定义为一个结构体
,该结构体包含了聚合内部的所有对象,以及聚合内部对象的访问方法
# 2、domain/demo/agg/aggroot.go
package agg
import (
"rms/domain/demo/entity"
"rms/domain/demo/repo"
"rms/domain/demo/vo"
)
// BookAggregateRoot 书籍聚合根
type BookAggregateRoot struct {
Book *entity.Book
Inventory *vo.InventoryValueObject
Repository repo.BookRepository
}
// OrderAggregateRoot 订单聚合根
type OrderAggregateRoot struct {
Order *entity.Order
Repository repo.OrderRepository
}
// GetBook 获取书籍
func (bar *BookAggregateRoot) GetBook() *entity.Book {
return bar.Book
}
// GetInventory 获取库存
func (bar *BookAggregateRoot) GetInventory() *vo.InventoryValueObject {
return bar.Inventory
}
func (bar *BookAggregateRoot) GetTest() string {
return "arr root test"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 3、实体(Entity)
# 1、实体定义
- 实体是具有唯一标识的对象,它们的标识是通过唯一标识符来实现的。
- 实体的状态可能随着时间的推移而改变,但是实体的标识是不变的。
- 在该项目实践中,
我们将实体定义为一个结构体
,该结构体包含了实体的所有属性,以及实体的所有方法
例子:
- 以图书为例,每本书都有唯一的标识(如ISBN),即使两本书的其他属性相同,它们仍然是不同的实体。
# 2、domain/demo/entity/entity.go
package entity
import "time"
type EntityA struct {
}
func (e *EntityA) NewA() *EntityA {
return &EntityA{}
}
func (e *EntityA) GetTest() string {
return "entity a test"
}
type Book struct {
ID int
Title string
Author string
Price float64
}
// OrderItem 表示订单中的一项书籍
type OrderItem struct {
BookID int
Quantity int
}
// Order 实体
type Order struct {
ID int
UserID int
OrderDate time.Time
Items []OrderItem
Total float64
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 4、值对象(Value Object)
# 1、实体定义
值对象是没有唯一标识的对象,其相等性是根据其所有属性的值来判断的
即,如果两个值对象的所有属性值都相等,那么这两个值对象就是相等的
值对象通常是不可变的,一旦创建就不能修改,如果需要修改值对象的某些属性,应该创建一个新的值对象
例子:
以货币金额为例,一个表示货币金额的值对象可以包含两个属性,金额数值和货币类型
两个值对象如果金额和货币类型相同,就被认为是相等的
type Money struct { Amount float64 Currency string }
1
2
3
4
# 2、domain/demo/vo/vo.go
package vo
type InventoryValueObject struct {
BookID int
InStock int
}
1
2
3
4
5
6
2
3
4
5
6
# 5、领域服务(Domain Service)
# 1、领域服务定义
- 特点:
- 领域服务是一种协作的概念,它不是实体或值对象,而是执行某个特定领域操作或任务的服务。
领域服务通常涉及多个实体和值对象,用于执行业务规则和操作,而不属于任何单一的实体或值对象
。
- 例子:
- 在一个电子商务系统中,计算订单总金额的过程可能涉及多个商品(实体)和它们的数量以及价格(值对象)
- 为了执行这个计算,你可能会创建一个领域服务,如
OrderCalculationService
# 2、domain/demo/service/svc.go
- 这个例子中,
OrderService
是一个领域服务,用于保存订单和计算订单价格。
package service
import (
"time"
"rms/domain/demo/entity"
"rms/domain/demo/repo"
)
// OrderService 订单领域服务
type OrderService struct {
BookRepo repo.BookRepository
OrderRepo repo.OrderRepository
}
// PlaceOrder 是一个领域服务方法,用于创建并保存订单
func (s *OrderService) PlaceOrder(userID int, items []entity.OrderItem) (*entity.Order, error) {
// 创建订单实例
order := &entity.Order{
UserID: userID,
OrderDate: time.Now(),
Items: items,
// 计算订单总价
Total: calculateTotalPrice(items, s.BookRepo),
}
// 保存订单到仓储
err := s.OrderRepo.Save(order)
if err != nil {
return nil, err
}
return order, nil
}
// 计算订单总价
func calculateTotalPrice(items []entity.OrderItem, bookRepo repo.BookRepository) float64 {
var total float64
for _, item := range items {
// 查询书籍价格并累加
book, err := bookRepo.FindByID(item.BookID)
if err != nil {
// 处理错误
}
total += book.Price * float64(item.Quantity)
}
return total
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 6、仓储(Repository)
# 1、仓储定义
仓储是一种模式或接口
,用于封装对领域对象的持久化和检索操作
- 仓储的
主要责任是将领域对象持久化到数据存储(如数据库、文件系统等)中
- 它
提供了一种抽象的方式来访问领域对象,而不暴露底层的数据存储细节
- 从数据存储中检索领域对象,并将其重新构造为领域对象的实例
# 2、domain/demo/repo/repo.go
package repo
import "rms/domain/demo/entity"
type BookRepository interface {
FindByID(id int) (*Book, error)
Save(book *Book) error
}
type Book struct{
ID int
Name string
// 其他属性...
}
func (BookRepo) FindByID(id int) (*Book, error) {
//TODO implement me
panic("implement me")
}
func (Book) Save(book *Book) error {
//TODO implement me
panic("implement me")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24