一、DDD 核心概念梳理
1.1 什么是领域驱动设计?
领域驱动设计(Domain-Driven Design,缩写DDD)是Eric Evans在2003年提出的一种软件设计方法论。它的核心思想是:将业务领域(Domain)作为软件设计的中心,通过业务专家(Domain Expert)与开发团队的紧密协作,建立统一的领域语言(Ubiquitous Language),并将业务逻辑落地为代码。
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
- 依赖退一,接口退一:
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 是一套处理复杂业务问题的方法论,核心是:
- 战略设计:限界上下文划分边界,通用语言统一认知,上下文映射处理协作
- 战术设计:聚合根保证一致性边界,实体与值对象承载状态,领域事件实现解耦
- 工程落地:分层架构 + 依赖反转 + CQRS + 领域事件驱动
Go 语言天然的接口机制和组合哲学,非常适合 DDD 的落地实践——小接口、组合代替继承、依赖反转自然融入项目结构。

发表评论 取消回复