一、DDD 核心概念梳理

1.1 什么是领域驱动设计?

领域驱动设计(Domain-Driven Design,缩写DDD)是Eric Evans在2003年提出的一种软件设计方法论。它的核心思想是:将业务领域(Domain)作为软件设计的中心,通过业务专家(Domain Expert)与开发团队的紧密协作,建立统一的领域语言(Ubiquitous Language),并将业务逻辑落地为代码。

为什么需要DDD? Evan的经典图表:当项目规模增长到一定程度,功能逻辑散落在各处,维护成本暴涨,业务认知不一致。DDD通过隔离业务复杂度,让代码结构反映业务逻辑,降低理解成本。

1.2 战略设计 vs 战术设计

维度着眼核心概念
战略设计大范围,生态层面限界上下文(Bounded Context)、上下文映射
战术设计小范围,代码层面聚合根、实体、值对象、领域事件、仓储

二、战略设计:把握大方向

2.1 限界上下文(Bounded Context):在哪里说话

限界上下文是一个明确的输入输出边界,在这个边界内,领域模型和领域语言是统一的。每个限界上下文是一个独立的协作范围,可以独立开发、独立部署。

例子: 电商系统中的多个限界上下文
  • 交易上下文:订单中的"商品"是一个 SKU(最小库存单位),关心价格、效期、详情图片。
  • 配送上下文:订单中的"商品"变成了"装车单",关心体积重量、装单表、距离优先级。
  • 库存上下文:订单中的"商品"变成了"鞋子.1983",关心库存数量、位置。

(同一个"商品"在不同的上下文中有不同的含义,这就是语境),隔离它们能缓解各个上下文之间的对接难度。

2.2 通用语言(Ubiquitous Language):说一样的话

通用语言是开发团队和业务专家共享的语言,它在需求文档、设计讨论、代码实现中统一使用。例如:

  • 不行: "用户下单"
  • 行: "客户提交订单"

同一个有限用词,应该在教科书、代码、对接外区域一致。

2.3 上下文映射(Context Map):怎么对接

不同上下文之间通过上下文映射进行互联,常见的映射方式:

映射关系含义在待
上下游(U/D)上游服务下游,下游依赖上游上游主动通知,下游被动接收
共同抗住(ACL)中间层隔离反向依赖不同上游共享文件/Esb/RPC
共同抗宿多司配同一个一个数据库数据同步难度低,但联系紧

三、战术设计:代码怎么写

3.1 分层架构(Layered Architecture)

核心原则:领域层不依赖于任何其他层

.
├── domain       ← 基音 — 业务逻辑落脚
├── application  ← 政策的地方 — 业务流程落脚
├── infrastructure ← 政策地方(数据库,Redis,消息队列)
└── interfaces   ← 派生者 — HTTP/gRPC/CLI
Go 语言中的分层原则
  • 依赖退一,接口退一domain 包只有接口 (interface),没有实现。
  • 依赖注入:infrastructure 实现domain 中定义的接口,通过"线(wire)"注入到应用层。
  • 接口隔离:接口定义在domain ,实现在infra ,依赖反转。

3.2 核心构建块(Building Blocks)

实体(Entity):有生命周期的对象

package domain

import "time"

// Order 是一个实体,有唯一ID,有生命周期
type Order struct {
 ID        string
 Items     []OrderItem
 Status    OrderStatus
 CreatedAt time.Time
 UpdatedAt time.Time
}

// OrderStatus 是状态机,只能按规则切换
type OrderStatus string

const (
 OrderStatusPending   = "pending"   // 待支付
 OrderStatusPaid      = "paid"      // 已支付
 OrderStatusShipped   = "shipped"   // 已发货
 OrderStatusCompleted = "completed" // 已完成
 OrderStatusCancelled = "cancelled" // 已取消
)

// ChangeStatus 切换状态,检查业务规则
func (o *Order) ChangeStatus(newStatus OrderStatus) error {
 if !o.canTransitionTo(newStatus) {
  return fmt.Errorf("cannot transition from %s to %s", o.Status, newStatus)
 }
 o.Status = newStatus
 o.UpdatedAt = time.Now()
 return nil
}

func (o *Order) canTransitionTo(newStatus OrderStatus) bool {
 transitions := map[OrderStatus][]OrderStatus{
  OrderStatusPending:   {OrderStatusPaid, OrderStatusCancelled},
  OrderStatusPaid:      {OrderStatusShipped, OrderStatusCancelled},
  OrderStatusShipped:   {OrderStatusCompleted},
 }
 allowed, ok := transitions[o.Status]
 if !ok {
  return false
 }
 for _, s := range allowed {
  if s == newStatus {
   return true
  }
 }
 return false
}

type OrderItem struct {
 ProductID string
 Quantity  int
 Price     Money
}

值对象(Value Object):无ID,不变,可替换

package domain

// Money 是值对象,无ID,不变,遵循 x == Money{10,"USD"} == Money{10,"USD"}
type Money struct {
 Amount   float64
 Currency string
}

func (m Money) Add(other Money) (Money, error) {
 if m.Currency != other.Currency {
  return Money{}, fmt.Errorf("cannot add different currencies: %s and %s", m.Currency, other.Currency)
 }
 return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}, nil
}

func (m Money) IsZero() bool {
 return m.Amount == 0 && m.Currency == ""
}

// Email 值对象,本身包含检测规则
type Email struct {
 address string
}

func NewEmail(raw string) (Email, error) {
 if !strings.Contains(raw, "@") {
  return Email{}, fmt.Errorf("invalid email: %s", raw)
 }
 return Email{address: strings.ToLower(strings.TrimSpace(raw))}, nil
}

func (e Email) String() string {
 return e.address
}

值对象的特性:所有字段在构造时确定,时间点和值相等,不能修改(immutable),可通过值比较判断相等。

领域事件(Domain Event):发生了什么

package domain

import "time"

// DomainEvent 是领域事件的基类
type DomainEvent interface {
 EventName() string
 OccurredAt() time.Time
}

// OrderPaidEvent 定义
type OrderPaidEvent struct {
 OrderID  string
 Amount   Money
 Occurred time.Time
}

func (e OrderPaidEvent) EventName() string {
 return "order.paid"
}

func (e OrderPaidEvent) OccurredAt() time.Time {
 return e.Occurred
}

// OrderCancelledEvent 定义
type OrderCancelledEvent struct {
 OrderID    string
 ReasonCode string
 Occurred   time.Time
}

func (e OrderCancelledEvent) EventName() string {
 return "order.cancelled"
}

func (e OrderCancelledEvent) OccurredAt() time.Time {
 return e.Occurred
}
事件名称用过去式:领域事件表示已经发生的事情,所以名称应该用过去式,如 OrderPaidEvent,而不是 PayOrderCommand

聚合根(Aggregate Root):一量恩的入口

package domain

import "time"

// Order 同时是聚合根,控制对所有子实体的访问
// 外部只能通过 Order 修改其子对象
type Order struct {
 id        string
 items     []OrderItem
 status    OrderStatus
 createdAt time.Time
 updatedAt time.Time
 events    []DomainEvent
}

// 构造函数,确保无浮状态
func NewOrder(id string, items []OrderItem) (*Order, error) {
 if len(items) == 0 {
  return nil, errors.New("order must have at least one item")
 }
 o := &Order{
  id:       id,
  items:    items,
  status:   OrderStatusPending,
  createdAt: time.Now(),
 }
 return o, nil
}

// Pay 支付订单,同时产生领域事件
func (o *Order) Pay(amount Money) error {
 if o.status != OrderStatusPending {
  return errors.New("only pending order can be paid")
 }
 total := o.TotalAmount()
 if !amount.Equal(total) {
  return fmt.Errorf("payment amount mismatch: expected %v, got %v", total, amount)
 }
 o.status = OrderStatusPaid
 o.updatedAt = time.Now()
 o.events = append(o.events, OrderPaidEvent{
  OrderID:  o.id,
  Amount:   amount,
  Occurred: time.Now(),
 })
 return nil
}

// TotalAmount 计算订单总金额
func (o *Order) TotalAmount() Money {
 var total Money
 for _, item := range o.items {
  total = total.Add(item.Price.Mul(float64(item.Quantity)))
 }
 return total
}

// ClearEvents 清除已记录的事件(用于事件发布后清理)
func (o *Order) ClearEvents() []DomainEvent {
 events := o.events
 o.events = nil
 return events
}

聚合根要点

  • 将一组相关对象理一为一个教室,同教客对象只通过根对象联系
  • 保证一致性边界:在聚合内部不允许出现误状态,所以所有修改都通过聚合根。
  • 小而精:聚合设计得小,人为组合设计:所以代码要保证紧,脱教客只通过根关联。

仓伦接口(Repository Interface):教客储有的手段

package domain

// OrderRepository 是领域层的接口,并不提供实现
// 通过依赖反转,从而隔离数据库细节
type OrderRepository interface {
 Save(ctx context.Context, order *Order) error
 FindByID(ctx context.Context, id string) (*Order, error)
 Delete(ctx context.Context, id string) error
}

先看接口定义:看是想想数据库,从数据库读,而不是想想电破接口给数据库。

反转依赖:domain 定义接口,infrastructure 实现。

领域服务(Domain Service):不属于任何实体的操作

当某个操作不属于任何一个实体或值对象时,就放在领域服务。

package domain

// PricingService 领域服务:处理入了应属于单个 Order/Product 的定价逻辑
type PricingService struct {
 discountRepo DiscountRepository
}

func NewPricingService(dr DiscountRepository) *PricingService {
 return &PricingService{discountRepo: dr}
}

func (s *PricingService) CalculateTotal(ctx context.Context, items []OrderItem) (Money, error) {
 var subtotal Money
 for _, item := range items {
  discount, err := s.discountRepo.FindByProductID(ctx, item.ProductID)
  if err != nil {
   return Money{}, err
  }
  finalPrice := applyDiscount(item.Price, discount)
  subtotal, _ = subtotal.Add(finalPrice.Mul(float64(item.Quantity)))
 }
 return subtotal, nil
}

3.3 应用层:UseCase 编排

应用层职责:编排流程(Orchestration)

  • 不包含业务逻辑
  • 提取业务流程(如:Q4 是据审批的过程)
  • 处理事务边界
  • 发布领域事件
  • 协调多个聚合根
  • 发送通知(将它作为领域事件发布,而不是发亊事件)

3.3.1 Application 层结构

.
application
├── ports       ← 转为提取,同时是入口
│   ├── in      ← 转为有限,同时合聚内异化
│   └── out     ← 转为也是别事事件,将它作为宏
└── usecase     ← 转为实现

3.3.2 PlaceOrderUseCase 实现

package usecase

type PlaceOrderInput struct {
 Items []OrderItemDTO
}

type OrderItemDTO struct {
 ProductID string
 Quantity  int
 Price     MoneyDTO
}

type MoneyDTO struct {
 Amount   float64
 Currency string
}

type PlaceOrderUseCase struct {
 orderRepo domain.OrderRepository
 eventBus  EventBus
 idGen     IDGenerator
}

func NewPlaceOrderUseCase(or domain.OrderRepository, eb EventBus, ig IDGenerator) *PlaceOrderUseCase {
 return &PlaceOrderUseCase{
  orderRepo: or,
  eventBus:  eb,
  idGen:     ig,
 }
}

func (uc *PlaceOrderUseCase) Execute(ctx context.Context, in PlaceOrderInput) (string, error) {
 // 1. DTO => Domain Object
 items := make([]domain.OrderItem, 0, len(in.Items))
 for _, dto := range in.Items {
  items = append(items, domain.OrderItem{
   ProductID: dto.ProductID,
   Quantity:  dto.Quantity,
   Price:     domain.Money(dto.Price),
  })
 }

 // 2. 创别聚合根
 id := uc.idGen.Generate()
 order, err := domain.NewOrder(id, items)
 if err != nil {
  return "", err
 }

 // 3. 保存(事务)
 if err := uc.orderRepo.Save(ctx, order); err != nil {
  return "", err
 }

 // 4. 发布领域事件
 for _, event := range order.ClearEvents() {
  if err := uc.eventBus.Publish(ctx, event); err != nil {
   log.Printf("failed to publish event %s: %v", event.EventName(), err)
  }
 }

 return id, nil
}

应用层的语呅:没有业务逻辑,只是流程经列(pipeline)

典型的小方法: TransformInput, LoadAggregate, ExecuteBusinessLogic, Save, PublishEvents

3.4 Infrastructure:数据库的实现

package persistence

type OrderRepoImpl struct {
 db *sqlx.DB
}

func (r *OrderRepoImpl) Save(ctx context.Context, order *domain.Order) error {
 // 使用事件源(Event Sourcing)或表同存(State-based),这里用同存
 query := `INSERT INTO orders (id, status, created_at, updated_at) VALUES (:id, :status, :created_at, :updated_at) ON CONFLICT (id) DO UPDATE SET status = :status, updated_at = :updated_at`
 _, err := r.db.NamedExecContext(ctx, query, map[string]interface{}{
  "id":         order.GetID(),
  "status":     order.GetStatus(),
  "created_at": order.GetCreatedAt(),
  "updated_at": order.GetUpdatedAt(),
 })
 return err
}

func (r *OrderRepoImpl) FindByID(ctx context.Context, id string) (*domain.Order, error) {
 var row OrderRow
 query := `SELECT id, status, created_at, updated_at FROM orders WHERE id = ?`
 if err := r.db.GetContext(ctx, &row, query, id); err != nil {
  return nil, err
 }
 return row.ToDomain(), nil
}

3.5 实战:完整项目结构模板

myapp/
├── cmd/
│   └── myapp/
│       └── main.go              # 入口
├── domain/                       # 领域层
│   ├── order.go                  # 聚合根+实体
│   ├── events.go                 # 领域事件
│   ├── order_repo.go             # 接口
│   └── pricing_service.go        # 领域服务
├── application/
│   ├── ports/
│   │   ├── in/
│   │   └── out/
│   └── usecase/
│       └── place_order.go        # 用例实现
├── infrastructure/
│   ├── persistence/
│   │   └── order_repo_impl.go    # MySQL实现
│   ├── messaging/
│   │   └── event_bus_impl.go     # Kafka实现
│   └── redis/
│       └── cache_impl.go         # Redis实现
├── interfaces/
│   ├── http/
│   │   └── handler.go            # HTTP处理器
│   └── grpc/
│       └── server.go             # gRPC服务
└── wire.go                       # 依赖注入

四、DDD 在 Go 中常见误区与最佳实践

4.1 常见误区

误区正确做法
把所有逻辑放应用层业务逻辑应在领域层(实体/值对象/领域服务)
聚合根过大(God Aggregate)小聚合,通过ID引用其他聚合
领域层引用基础设施依赖反转:domain 定义接口,infra 实现
贫血模型(只有getter/setter)充血模型:行为+数据在同一个类型里
过度设计(为DDD而DDD)简单模块用事务脚本即可

4.2 最佳实践

  • 先战略后战术:先划定限界上下文边界,再深入设计聚合
  • 聚合要小:一个事务只修改一个聚合,通过领域事件实现最终一致性
  • 值对象优先:能用值对象就不用实体,确保不变性
  • 构造函数校验:在对象创建时确保有效性,拒绝无效状态
  • 领域事件解耦:跨上下文协作通过事件实现,避免紧耦合

4.3 何时不该用 DDD?

DDD 适合业务复杂度高、领域模型多变的项目。如果仅是 CRUD(增删改查)操作,事务脚本模式(Transaction Script)或简单的 MVC 就足够。不要为了 DDD 而 DDD。


五、总结

DDD 是一套处理复杂业务问题的方法论,核心是:

  1. 战略设计:限界上下文划分边界,通用语言统一认知,上下文映射处理协作
  2. 战术设计:聚合根保证一致性边界,实体与值对象承载状态,领域事件实现解耦
  3. 工程落地:分层架构 + 依赖反转 + CQRS + 领域事件驱动

Go 语言天然的接口机制和组合哲学,非常适合 DDD 的落地实践——小接口、组合代替继承、依赖反转自然融入项目结构。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部