3. 领域驱动设计基础:DDD核心概念与战略/战术设计
领域驱动设计(Domain-Driven Design,DDD)是一套应对复杂业务系统的建模与设计方法论。在GoBFF架构中,DDD帮助我们精确划分业务接口的领域边界,避免业务逻辑泄漏到基础设施层。本节将系统讲解DDD的核心概念、战略设计与战术设计,为后续的BFF领域建模打下理论基础。
3.1 DDD核心概念
DDD的核心概念分为两类:战术设计中的建模元素(实体、值对象、聚合、领域服务)和战略设计中的边界划分工具(限界上下文、上下文映射)。我们先从战术设计的基础元素开始。
3.1.1 实体(Entity)
实体是具有唯一标识且生命周期持续变化的对象。在GoBFF中,实体通常对应业务中的核心概念,如用户、订单、商品。
- 唯一标识:每个实体通过ID(如UUID、自增ID)区分,即使属性完全相同,不同ID代表不同实体。
- 可变性:实体的属性可以随时间变化,但标识保持不变。
- 业务行为:实体封装与其身份相关的业务逻辑,而非仅作为数据容器。
// Order 是典型的实体,具有唯一标识和业务行为
type Order struct {
ID string // 唯一标识
CustomerID string
Items []OrderItem
Status OrderStatus
CreatedAt time.Time
}
// 实体封装业务行为
func (o *Order) Cancel() error {
if o.Status == OrderStatusShipped {
return ErrCannotCancelShippedOrder
}
o.Status = OrderStatusCancelled
return nil
}
3.1.2 值对象(Value Object)
值对象通过属性值来定义,没有唯一标识,且通常是不可变的。在GoBFF中,值对象用于描述实体的属性或业务概念,如地址、金额、时间段。
- 无标识:两个值对象如果所有属性相同,则视为相等。
- 不可变性:值对象创建后不应修改,修改应返回新实例。
- 自包含:值对象可以包含验证逻辑和业务方法。
// Money 是值对象,不可变且通过属性定义相等性
type Money struct {
amount decimal.Decimal
currency string
}
func NewMoney(amount decimal.Decimal, currency string) (Money, error) {
if amount.IsNegative() {
return Money{}, ErrNegativeAmount
}
if currency == "" {
return Money{}, ErrEmptyCurrency
}
return Money{amount: amount, currency: currency}, nil
}
// 值对象方法返回新实例,而非修改自身
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, ErrCurrencyMismatch
}
return NewMoney(m.amount.Add(other.amount), m.currency)
}
| 特性 | 实体(Entity) | 值对象(Value Object) |
|---|---|---|
| 唯一标识 | 有(ID) | 无 |
| 可变性 | 可变 | 不可变 |
| 相等性判断 | 通过ID | 通过所有属性 |
| 生命周期 | 持续变化 | 无独立生命周期 |
| 典型例子 | 用户、订单、商品 | 金额、地址、时间段 |
3.1.3 聚合(Aggregate)
聚合是一组相关实体和值对象的集合,通过聚合根(Aggregate Root)作为外部访问的唯一入口。聚合保证了内部数据的一致性和不变性。
- 聚合根:聚合中唯一的实体,外部只能通过聚合根操作聚合内的对象。
- 一致性边界:聚合内的所有修改必须满足业务不变量,聚合根负责校验。
- 原子性:每次操作聚合时,整个聚合作为一个整体持久化。
// Order 作为聚合根,管理内部的 OrderItem 实体和 Money 值对象
type Order struct {
ID string
CustomerID string
Items []OrderItem // 聚合内的实体
Total Money // 聚合内的值对象
Status OrderStatus
}
// 外部只能通过聚合根操作
func (o *Order) AddItem(productID string, quantity int, price Money) error {
if o.Status != OrderStatusPending {
return ErrCannotModifyNonPendingOrder
}
item := NewOrderItem(productID, quantity, price)
o.Items = append(o.Items, item)
o.Total = o.calculateTotal() // 重新计算总额,保证一致性
return nil
}
// 聚合内的实体,外部不可直接访问
type OrderItem struct {
ProductID string
Quantity int
Price Money
}
3.1.4 领域服务(Domain Service)
当某个业务操作不属于任何实体或值对象,或者涉及多个聚合的协作时,应将其建模为领域服务。领域服务是无状态的,只包含业务逻辑。
- 无状态:领域服务不持有自身状态,所有状态通过参数传入。
- 业务操作:封装跨聚合的业务流程或计算。
- 命名规范:通常以动词命名,如
TransferService、PaymentService。
// TransferService 是领域服务,处理跨聚合的转账操作
type TransferService struct {
accountRepo AccountRepository
}
func (s *TransferService) Transfer(fromID, toID string, amount Money) error {
from := s.accountRepo.FindByID(fromID)
to := s.accountRepo.FindByID(toID)
// 领域服务协调多个聚合
if err := from.Withdraw(amount); err != nil {
return err
}
if err := to.Deposit(amount); err != nil {
// 回滚操作,保证一致性
from.Deposit(amount) // 补偿操作
return err
}
return nil
}
3.2 DDD战略设计
战略设计关注宏观的领域划分和上下文边界,帮助团队在大型系统中确定每个子领域的职责和交互方式。
3.2.1 限界上下文(Bounded Context)
限界上下文是DDD战略设计的核心,它定义了模型应用的边界。在每个限界上下文内部,模型是统一且自洽的;不同上下文之间通过上下文映射进行集成。
- 明确边界:每个上下文有独立的领域模型、数据库、团队和部署单元。
- 统一语言:团队在上下文内使用相同的业务术语,避免歧义。
- 独立演化:上下文之间通过接口通信,内部实现可以独立变化。
3.2.2 上下文映射(Context Map)
上下文映射描述了限界上下文之间的协作关系。常见的映射模式包括:
| 模式 | 描述 | 适用场景 |
|---|---|---|
| 合作关系(Partnership) | 两个团队协同演进,共同维护集成接口 | 紧密协作的子系统 |
| 共享内核(Shared Kernel) | 共享部分领域模型,保持同步 | 高度依赖的核心模块 |
| 客户-供应商(Customer-Supplier) | 上游团队提供接口,下游团队按需消费 | API提供方与消费方 |
| 防腐层(Anti-Corruption Layer) | 在下游上下文建立转换层,隔离上游模型变化 | 集成遗留系统或第三方服务 |
| 开放主机服务(Open Host Service) | 上游以协议形式公开服务,下游通过协议集成 | 微服务间的REST/gRPC通信 |
3.3 DDD战术设计
战术设计关注微观的模型实现,将战略设计中的限界上下文细化为具体的代码结构。在GoBFF中,战术设计指导我们如何组织包结构、定义领域对象和实现业务逻辑。
3.3.1 分层架构
DDD推荐使用分层架构来隔离关注点,典型的分层包括:
- 用户接口层(Interface):处理HTTP请求、gRPC调用、消息队列等外部输入。
- 应用层(Application):编排领域对象,处理事务和安全校验。
- 领域层(Domain):核心业务逻辑,包含实体、值对象、聚合、领域服务。
- 基础设施层(Infrastructure):提供持久化、消息、缓存等技术实现。
3.3.2 仓储模式(Repository)
仓储为聚合提供集合式的访问接口,隐藏底层持久化细节。在GoBFF中,仓储通常定义为接口,由基础设施层实现。
// 仓储接口定义在领域层
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, id string) (*Order, error)
FindByCustomerID(ctx context.Context, customerID string) ([]*Order, error)
Delete(ctx context.Context, id string) error
}
// 基础设施层实现仓储接口
type PostgresOrderRepository struct {
db *sql.DB
}
func (r *PostgresOrderRepository) Save(ctx context.Context, order *Order) error {
// 实现持久化逻辑
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
// 保存聚合根和内部对象
// ...
return tx.Commit()
}
3.3.3 领域事件(Domain Event)
领域事件记录领域中发生的业务事件,用于解耦聚合之间的通信。在GoBFF中,领域事件常用于跨上下文的异步通知。
// 领域事件定义
type OrderCreatedEvent struct {
OrderID string
CustomerID string
Total Money
CreatedAt time.Time
}
// 聚合根发布事件
func (o *Order) Create() error {
if o.Status != OrderStatusPending {
return ErrInvalidStatus
}
o.Status = OrderStatusCreated
o.AddEvent(OrderCreatedEvent{
OrderID: o.ID,
CustomerID: o.CustomerID,
Total: o.Total,
CreatedAt: time.Now(),
})
return nil
}
3.4 战略设计与战术设计的关系
战略设计和战术设计是DDD的两个互补维度,共同构成完整的领域建模体系。
| 维度 | 关注点 | 产出物 | 在GoBFF中的应用 |
|---|---|---|---|
| 战略设计 | 宏观边界、团队协作、系统集成 | 限界上下文、上下文映射、统一语言 | 划分BFF与后端服务的边界,确定接口协议 |
| 战术设计 | 微观模型、代码实现、业务逻辑 | 实体、值对象、聚合、领域服务、仓储 | 实现BFF中的业务接口逻辑,封装领域规则 |
3.5 本章小结
DDD的核心概念为GoBFF业务接口的领域边界设计提供了坚实的理论基础:
- 实体和值对象是领域模型的基本构建块,分别处理有标识和无需标识的业务概念。
- 聚合通过聚合根保证数据一致性和业务不变量,是事务操作的基本单元。
- 领域服务处理跨聚合的业务逻辑,保持领域层的纯净。
- 战略设计通过限界上下文和上下文映射划分系统边界,指导团队协作和系统集成。
- 战术设计通过分层架构、仓储模式和领域事件,将战略设计落地为可执行的代码结构。