Go 与领域驱动设计:构建复杂业务系统
你有没有遇到过这样的代码:一个 UserService 里有 3000 行代码,混杂着数据库查询、业务规则校验、发邮件、记录日志……每次改一个小需求都要小心翼翼地测试整个链路。新同事入职看到这种代码,第一反应是"这谁写的?“第二反应是"我也不敢动”。
这种"大泥球"(Big Ball of Mud)架构之所以会出现,根本原因是我们把业务逻辑和技术实现混在了一起。领域驱动设计(Domain-Driven Design,简称 DDD)就是为了解决这个问题而生的方法论。
今天,我们就来探讨如何将 DDD 的核心思想应用到 Go 项目中,构建一个真正能承载复杂业务的系统。
什么是 DDD?
领域驱动设计由 Eric Evans 在 2003 年提出,核心思想是:软件的设计应该围绕业务领域,而不是技术框架。
DDD 分为两大部分:
战略设计(Strategic Design):
- 限界上下文(Bounded Context):业务边界,比如"订单上下文"、“用户上下文”、“库存上下文”
- 通用语言(Ubiquitous Language):开发和业务团队共用的语言
- 上下文映射(Context Mapping):不同限界上下文之间的关系
战术设计(Tactical Design):
- 实体(Entity):有唯一标识的对象,比如"订单"
- 值对象(Value Object):没有唯一标识、按值比较的对象,比如"金额"
- 聚合(Aggregate):一组相关对象作为修改单元,由聚合根管理
- 仓储(Repository):封装持久化逻辑
- 领域服务(Domain Service):不属于任何实体的业务逻辑
- 应用服务(Application Service):协调领域对象完成用例
- 领域事件(Domain Event):描述业务中发生的重要事件
项目案例:电商订单系统
让我们用一个电商订单系统来演示 DDD 的应用。这个系统要处理:
- 订单创建、支付、发货、取消
- 库存扣减
- 用户账户
- 促销活动
- 通知发送
项目结构设计
DDD 项目的目录结构应该清晰反映业务边界:
eshop/
├── cmd/
│ └── api/
│ └── main.go # 应用入口
├── internal/
│ ├── order/ # 订单限界上下文
│ │ ├── domain/ # 领域层
│ │ │ ├── order.go # 聚合根
│ │ │ ├── order_item.go # 实体
│ │ │ ├── money.go # 值对象
│ │ │ ├── address.go # 值对象
│ │ │ ├── order_status.go # 枚举
│ │ │ ├── events.go # 领域事件
│ │ │ ├── repository.go # 仓储接口
│ │ │ ├── errors.go # 领域错误
│ │ │ └── service.go # 领域服务
│ │ ├── application/ # 应用层
│ │ │ ├── command/ # 命令处理
│ │ │ ├── query/ # 查询处理(CQRS)
│ │ │ ├── service.go # 应用服务
│ │ │ └── dto/ # 数据传输对象
│ │ ├── infrastructure/ # 基础设施层
│ │ │ ├── persistence/ # 持久化实现
│ │ │ ├── messaging/ # 消息队列
│ │ │ └── http/ # HTTP 接口
│ │ └── interfaces/ # 用户接口层
│ │ └── http/
│ │ ├── handler.go
│ │ └── middleware.go
│ ├── inventory/ # 库存限界上下文
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ └── user/ # 用户限界上下文
│ ├── domain/
│ ├── application/
│ └── infrastructure/
├── pkg/ # 通用工具包
│ ├── events/ # 事件总线
│ ├── errors/ # 错误处理
│ └── middleware/
└── migrations/
注意每个限界上下文(order、inventory、user)都有自己的领域层、应用层和基础设施层。它们之间通过事件或防腐层通信,而不是直接耦合。
值对象(Value Object)
值对象是 DDD 中最基础但最容易被忽视的概念。它的特点:
- 没有唯一标识,按值比较
- 不可变(immutable)
- 自包含业务规则
让我们从"金额"这个值对象开始:
// internal/order/domain/money.go
package domain
import (
"errors"
"fmt"
)
// Money 金额值对象
// 使用不可变设计,所有运算都返回新对象
type Money struct {
amount int64 // 以分为单位,避免浮点精度问题
currency string // ISO 4217 货币代码
}
// NewMoney 创建金额
func NewMoney(amount int64, currency string) (Money, error) {
if amount < 0 {
return Money{}, errors.New("amount cannot be negative")
}
if !isValidCurrency(currency) {
return Money{}, fmt.Errorf("invalid currency: %s", currency)
}
return Money{amount: amount, currency: currency}, nil
}
// Zero 零金额
func Zero(currency string) Money {
return Money{amount: 0, currency: currency}
}
// Amount 获取金额(以分为单位)
func (m Money) Amount() int64 {
return m.amount
}
// Currency 获取货币
func (m Money) Currency() string {
return m.currency
}
// Add 加法
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
}
// Subtract 减法
func (m Money) Subtract(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, fmt.Errorf("cannot subtract different currencies")
}
if m.amount < other.amount {
return Money{}, errors.New("insufficient funds")
}
return Money{
amount: m.amount - other.amount,
currency: m.currency,
}, nil
}
// Multiply 乘法(用于计算折扣、税费等)
func (m Money) Multiply(multiplier float64) Money {
// 注意浮点精度问题,生产环境建议使用 shopspring/decimal
newAmount := int64(float64(m.amount) * multiplier)
return Money{
amount: newAmount,
currency: m.currency,
}
}
// IsZero 是否为零
func (m Money) IsZero() bool {
return m.amount == 0
}
// GreaterThan 是否大于另一个金额
func (m Money) GreaterThan(other Money) bool {
if m.currency != other.currency {
return false
}
return m.amount > other.amount
}
// Equals 是否相等(值对象按值比较)
func (m Money) Equals(other Money) bool {
return m.amount == other.amount && m.currency == other.currency
}
// String 字符串表示
func (m Money) String() string {
return fmt.Sprintf("%s %.2f", m.currency, float64(m.amount)/100)
}
// MarshalJSON 序列化为 JSON
func (m Money) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"amount":%d,"currency":"%s"}`, m.amount, m.currency)), nil
}
func isValidCurrency(currency string) bool {
validCurrencies := map[string]bool{
"USD": true, "EUR": true, "CNY": true,
"GBP": true, "JPY": true, "KRW": true,
}
return validCurrencies[currency]
}
另一个值对象——地址:
// internal/order/domain/address.go
package domain
import (
"errors"
"fmt"
"strings"
)
// Address 地址值对象
type Address struct {
country string
province string
city string
district string
street string
postalCode string
recipient string
phone string
}
// NewAddress 创建地址
func NewAddress(country, province, city, district, street, postalCode, recipient, phone string) (Address, error) {
a := Address{
country: country,
province: province,
city: city,
district: district,
street: street,
postalCode: postalCode,
recipient: recipient,
phone: phone,
}
if err := a.Validate(); err != nil {
return Address{}, err
}
return a, nil
}
// Validate 验证地址
func (a Address) Validate() error {
if strings.TrimSpace(a.country) == "" {
return errors.New("country is required")
}
if strings.TrimSpace(a.city) == "" {
return errors.New("city is required")
}
if strings.TrimSpace(a.street) == "" {
return errors.New("street address is required")
}
if strings.TrimSpace(a.recipient) == "" {
return errors.New("recipient is required")
}
if !isValidPhone(a.phone) {
return errors.New("invalid phone number")
}
return nil
}
// Equals 值对象按值比较
func (a Address) Equals(other Address) bool {
return a.country == other.country &&
a.province == other.province &&
a.city == other.city &&
a.district == other.district &&
a.street == other.street &&
a.postalCode == other.postalCode &&
a.recipient == other.recipient &&
a.phone == other.phone
}
// FullAddress 返回完整地址字符串
func (a Address) FullAddress() string {
parts := []string{a.country, a.province, a.city, a.district, a.street}
return strings.Join(parts, " ")
}
// Getters...
func (a Address) Country() string { return a.country }
func (a Address) Province() string { return a.province }
func (a Address) City() string { return a.city }
func (a Address) District() string { return a.district }
func (a Address) Street() string { return a.street }
func (a Address) PostalCode() string { return a.postalCode }
func (a Address) Recipient() string { return a.recipient }
func (a Address) Phone() string { return a.phone }
func isValidPhone(phone string) bool {
// 简单验证,生产环境使用更严格的正则
return len(phone) >= 8 && len(phone) <= 15
}
// String 实现 fmt.Stringer 接口
func (a Address) String() string {
return fmt.Sprintf("%s, %s, %s", a.recipient, a.FullAddress(), a.phone)
}
值对象的威力在于:业务规则被封装在类型内部,任何使用 Money 或 Address 的地方都自动享有这些规则。你不可能把一个负数金额传给系统,也不可能创建一个没有收件人的地址。
实体(Entity)
实体和值对象不同,它有唯一标识,按标识比较。让我们设计订单的聚合根:
// internal/order/domain/order.go
package domain
import (
"errors"
"fmt"
"time"
)
// OrderID 订单 ID(强类型标识)
type OrderID string
// Order 订单聚合根
// 聚合根是修改聚合内对象的唯一入口
type Order struct {
id OrderID
userID string
items []OrderItem // 实体集合
shippingAddr Address
totalAmount Money
status OrderStatus
paymentID string
cancelReason string
events []DomainEvent // 领域事件集合
createdAt time.Time
updatedAt time.Time
}
// NewOrder 创建新订单(工厂方法)
func NewOrder(id OrderID, userID string, items []OrderItem, shippingAddr Address, currency string) (*Order, error) {
if len(items) == 0 {
return nil, errors.New("order must have at least one item")
}
// 计算总金额
total := Zero(currency)
for _, item := range items {
itemTotal, err := item.Subtotal()
if err != nil {
return nil, err
}
total, err = total.Add(itemTotal)
if err != nil {
return nil, err
}
}
now := time.Now()
order := &Order{
id: id,
userID: userID,
items: items,
shippingAddr: shippingAddr,
totalAmount: total,
status: StatusPending,
events: []DomainEvent{},
createdAt: now,
updatedAt: now,
}
// 发布领域事件
order.addEvent(&OrderCreatedEvent{
OrderID: string(id),
UserID: userID,
TotalAmount: total,
Items: items,
CreatedAt: now,
})
return order, nil
}
// ID 获取订单 ID
func (o *Order) ID() OrderID {
return o.id
}
// UserID 获取用户 ID
func (o *Order) UserID() string {
return o.userID
}
// Items 获取订单项(返回副本,防止外部修改)
func (o *Order) Items() []OrderItem {
result := make([]OrderItem, len(o.items))
copy(result, o.items)
return result
}
// TotalAmount 获取总金额
func (o *Order) TotalAmount() Money {
return o.totalAmount
}
// Status 获取订单状态
func (o *Order) Status() OrderStatus {
return o.status
}
// ShippingAddress 获取收货地址
func (o *Order) ShippingAddress() Address {
return o.shippingAddr
}
// ConfirmPayment 确认支付
// 这是一个业务操作,包含业务规则
func (o *Order) ConfirmPayment(paymentID string) error {
// 业务规则:只有待支付订单可以确认
if o.status != StatusPending {
return fmt.Errorf("cannot confirm payment for order in status %s", o.status)
}
if paymentID == "" {
return errors.New("payment ID is required")
}
o.status = StatusPaid
o.paymentID = paymentID
o.updatedAt = time.Now()
// 发布领域事件
o.addEvent(&OrderPaidEvent{
OrderID: string(o.id),
PaymentID: paymentID,
Amount: o.totalAmount,
PaidAt: o.updatedAt,
})
return nil
}
// Ship 发货
func (o *Order) Ship(trackingNumber string) error {
if o.status != StatusPaid {
return fmt.Errorf("cannot ship order in status %s, must be paid first", o.status)
}
if trackingNumber == "" {
return errors.New("tracking number is required")
}
o.status = StatusShipped
o.updatedAt = time.Now()
o.addEvent(&OrderShippedEvent{
OrderID: string(o.id),
TrackingNumber: trackingNumber,
ShippedAt: o.updatedAt,
})
return nil
}
// Cancel 取消订单
func (o *Order) Cancel(reason string) error {
// 业务规则:已发货的订单不能取消
if o.status == StatusShipped || o.status == StatusDelivered {
return fmt.Errorf("cannot cancel order in status %s", o.status)
}
if reason == "" {
return errors.New("cancel reason is required")
}
oldStatus := o.status
o.status = StatusCancelled
o.cancelReason = reason
o.updatedAt = time.Now()
o.addEvent(&OrderCancelledEvent{
OrderID: string(o.id),
Reason: reason,
OldStatus: string(oldStatus),
CancelledAt: o.updatedAt,
})
return nil
}
// AddItem 添加商品项
func (o *Order) AddItem(item OrderItem) error {
// 业务规则:只有待支付的订单可以添加商品
if o.status != StatusPending {
return fmt.Errorf("cannot add items to order in status %s", o.status)
}
// 检查是否已有相同商品
for _, existing := range o.items {
if existing.ProductID() == item.ProductID() {
return fmt.Errorf("product %s already in order, use UpdateItemQuantity instead",
item.ProductID())
}
}
o.items = append(o.items, item)
// 重新计算总金额
if err := o.recalculateTotal(); err != nil {
return err
}
o.updatedAt = time.Now()
return nil
}
// UpdateItemQuantity 更新商品数量
func (o *Order) UpdateItemQuantity(productID string, newQuantity int) error {
if o.status != StatusPending {
return fmt.Errorf("cannot update items in order with status %s", o.status)
}
for i, item := range o.items {
if item.ProductID() == productID {
if err := o.items[i].SetQuantity(newQuantity); err != nil {
return err
}
if err := o.recalculateTotal(); err != nil {
return err
}
o.updatedAt = time.Now()
return nil
}
}
return fmt.Errorf("product %s not found in order", productID)
}
// recalculateTotal 重新计算总金额(私有方法)
func (o *Order) recalculateTotal() error {
total := Zero(o.totalAmount.Currency())
for _, item := range o.items {
subtotal, err := item.Subtotal()
if err != nil {
return err
}
total, err = total.Add(subtotal)
if err != nil {
return err
}
}
o.totalAmount = total
return nil
}
// Events 获取并发布领域事件
func (o *Order) Events() []DomainEvent {
events := o.events
o.events = nil // 发布后清空
return events
}
// addEvent 添加领域事件(私有方法)
func (o *Order) addEvent(event DomainEvent) {
o.events = append(o.events, event)
}
// Equals 实体按标识比较
func (o *Order) Equals(other *Order) bool {
if other == nil {
return false
}
return o.id == other.id
}
订单项(实体)
OrderItem 是 Order 聚合内的实体。它有自己的标识,但只能通过 Order 来修改:
// internal/order/domain/order_item.go
package domain
import (
"errors"
"fmt"
)
// OrderItemID 订单项 ID
type OrderItemID string
// OrderItem 订单项(实体)
type OrderItem struct {
id OrderItemID
productID string
name string
price Money
quantity int
}
// NewOrderItem 创建订单项
func NewOrderItem(id OrderItemID, productID, name string, price Money, quantity int) (OrderItem, error) {
if productID == "" {
return OrderItem{}, errors.New("product ID is required")
}
if name == "" {
return OrderItem{}, errors.New("product name is required")
}
if quantity <= 0 {
return OrderItem{}, errors.New("quantity must be greater than zero")
}
return OrderItem{
id: id,
productID: productID,
name: name,
price: price,
quantity: quantity,
}, nil
}
// ID 获取 ID
func (i OrderItem) ID() OrderItemID {
return i.id
}
// ProductID 获取商品 ID
func (i OrderItem) ProductID() string {
return i.productID
}
// Name 获取商品名
func (i OrderItem) Name() string {
return i.name
}
// Price 获取单价
func (i OrderItem) Price() Money {
return i.price
}
// Quantity 获取数量
func (i OrderItem) Quantity() int {
return i.quantity
}
// Subtotal 计算小计金额
func (i OrderItem) Subtotal() (Money, error) {
return i.price.Multiply(float64(i.quantity)), nil
}
// SetQuantity 设置数量(只允许 Order 聚合内部调用)
// 注意:Go 没有访问修饰符,我们通过约定和接口来控制
func (i *OrderItem) SetQuantity(qty int) error {
if qty <= 0 {
return fmt.Errorf("quantity must be greater than zero, got %d", qty)
}
if qty > 1000 {
return errors.New("quantity cannot exceed 1000")
}
i.quantity = qty
return nil
}
// Equals 实体按标识比较
func (i OrderItem) Equals(other OrderItem) bool {
return i.id == other.id
}
领域事件(Domain Event)
领域事件描述业务中发生的重要事情。它让限界上下文之间可以松耦合地通信:
// internal/order/domain/events.go
package domain
import (
"time"
"github.com/google/uuid"
)
// DomainEvent 领域事件接口
type DomainEvent interface {
EventID() string
EventName() string
OccurredAt() time.Time
}
// BaseEvent 基础事件实现
type BaseEvent struct {
id string
name string
occurredAt time.Time
}
func NewBaseEvent(name string) BaseEvent {
return BaseEvent{
id: uuid.New().String(),
name: name,
occurredAt: time.Now(),
}
}
func (e BaseEvent) EventID() string { return e.id }
func (e BaseEvent) EventName() string { return e.name }
func (e BaseEvent) OccurredAt() time.Time { return e.occurredAt }
// OrderCreatedEvent 订单创建事件
type OrderCreatedEvent struct {
BaseEvent
OrderID string
UserID string
TotalAmount Money
Items []OrderItem
CreatedAt time.Time
}
func NewOrderCreatedEvent(orderID, userID string, total Money, items []OrderItem) *OrderCreatedEvent {
return &OrderCreatedEvent{
BaseEvent: NewBaseEvent("order.created"),
OrderID: orderID,
UserID: userID,
TotalAmount: total,
Items: items,
CreatedAt: time.Now(),
}
}
// OrderPaidEvent 订单支付事件
type OrderPaidEvent struct {
BaseEvent
OrderID string
PaymentID string
Amount Money
PaidAt time.Time
}
// OrderShippedEvent 订单发货事件
type OrderShippedEvent struct {
BaseEvent
OrderID string
TrackingNumber string
ShippedAt time.Time
}
// OrderCancelledEvent 订单取消事件
type OrderCancelledEvent struct {
BaseEvent
OrderID string
Reason string
OldStatus string
CancelledAt time.Time
}
// InventoryReservedEvent 库存预留事件(由库存上下文发出)
type InventoryReservedEvent struct {
BaseEvent
ReservationID string
OrderID string
Items []ReservedItem
}
// ReservedItem 预留商品项
type ReservedItem struct {
ProductID string
Quantity int
}
// InventoryReservationFailedEvent 库存预留失败事件
type InventoryReservationFailedEvent struct {
BaseEvent
OrderID string
FailedProduct string
Reason string
}
事件总线(Event Bus)
为了让限界上下文之间通过事件通信,我们需要一个事件总线:
// pkg/events/bus.go
package events
import (
"context"
"sync"
)
// Event 通用事件接口
type Event interface {
EventName() string
}
// Handler 事件处理器
type Handler func(ctx context.Context, event Event) error
// Bus 事件总线
type Bus interface {
Publish(ctx context.Context, event Event) error
Subscribe(eventName string, handler Handler)
}
// InMemoryBus 内存事件总线(开发和测试用)
type InMemoryBus struct {
mu sync.RWMutex
handlers map[string][]Handler
}
func NewInMemoryBus() *InMemoryBus {
return &InMemoryBus{
handlers: make(map[string][]Handler),
}
}
func (b *InMemoryBus) Publish(ctx context.Context, event Event) error {
b.mu.RLock()
handlers, ok := b.handlers[event.EventName()]
b.mu.RUnlock()
if !ok {
return nil
}
// 异步执行所有处理器
var wg sync.WaitGroup
for _, handler := range handlers {
wg.Add(1)
go func(h Handler) {
defer wg.Done()
_ = h(ctx, event) // 生产环境应该处理错误
}(handler)
}
wg.Wait()
return nil
}
func (b *InMemoryBus) Subscribe(eventName string, handler Handler) {
b.mu.Lock()
defer b.mu.Unlock()
b.handlers[eventName] = append(b.handlers[eventName], handler)
}
// Dispatch 从聚合根中发布所有领域事件
func Dispatch(ctx context.Context, bus Bus, events []Event) error {
for _, event := range events {
if err := bus.Publish(ctx, event); err != nil {
return err
}
}
return nil
}
生产环境通常会使用 RabbitMQ、Kafka 或 NATS 作为事件总线。
仓储接口(Repository)
仓储模式是 DDD 中最核心的抽象之一。接口定义在领域层,实现在基础设施层:
// internal/order/domain/repository.go
package domain
import (
"context"
)
// OrderRepository 订单仓储接口
// 注意:接口定义在领域层,由基础设施层实现
// 这样领域层不依赖任何持久化技术
type OrderRepository interface {
// Save 保存订单(插入或更新)
Save(ctx context.Context, order *Order) error
// GetByID 根据 ID 获取订单
GetByID(ctx context.Context, id OrderID) (*Order, error)
// GetByUserID 根据用户 ID 查询订单
GetByUserID(ctx context.Context, userID string, offset, limit int) ([]*Order, error)
// GetByStatus 根据状态查询订单
GetByStatus(ctx context.Context, status OrderStatus) ([]*Order, error)
// NextID 生成新的订单 ID
NextID(ctx context.Context) (OrderID, error)
}
// UnitOfWork 工作单元(保证聚合的事务完整性)
type UnitOfWork interface {
// Execute 在事务中执行操作
Execute(ctx context.Context, fn func(ctx context.Context) error) error
}
仓储实现位于基础设施层,可以使用 PostgreSQL、MongoDB 等任何存储:
// internal/order/infrastructure/persistence/postgres_order_repo.go
package persistence
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/yourusername/eshop/internal/order/domain"
)
// postgresOrderRepo 基于 PostgreSQL 的订单仓储实现
type postgresOrderRepo struct {
db *sql.DB
}
// NewPostgresOrderRepository 创建仓储
func NewPostgresOrderRepository(db *sql.DB) domain.OrderRepository {
return &postgresOrderRepo{db: db}
}
func (r *postgresOrderRepo) Save(ctx context.Context, order *domain.Order) error {
// 序列化聚合根为 JSON(或使用多表存储)
itemsJSON, err := json.Marshal(order.Items())
if err != nil {
return fmt.Errorf("failed to marshal items: %w", err)
}
addrJSON, err := json.Marshal(order.ShippingAddress())
if err != nil {
return fmt.Errorf("failed to marshal address: %w", err)
}
query := `
INSERT INTO orders (id, user_id, items, shipping_address, total_amount, currency, status, payment_id, cancel_reason, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (id) DO UPDATE SET
items = EXCLUDED.items,
total_amount = EXCLUDED.total_amount,
status = EXCLUDED.status,
payment_id = EXCLUDED.payment_id,
cancel_reason = EXCLUDED.cancel_reason,
updated_at = EXCLUDED.updated_at
`
_, err = r.db.ExecContext(ctx, query,
order.ID(),
order.UserID(),
itemsJSON,
addrJSON,
order.TotalAmount().Amount(),
order.TotalAmount().Currency(),
order.Status(),
order.PaymentID(),
order.CancelReason(),
order.CreatedAt(),
order.UpdatedAt(),
)
return err
}
func (r *postgresOrderRepo) GetByID(ctx context.Context, id domain.OrderID) (*domain.Order, error) {
var (
userID string
itemsJSON []byte
addrJSON []byte
amount int64
currency string
status string
paymentID sql.NullString
cancelReason sql.NullString
createdAt time.Time
updatedAt time.Time
)
query := `
SELECT user_id, items, shipping_address, total_amount, currency, status, payment_id, cancel_reason, created_at, updated_at
FROM orders
WHERE id = $1
`
err := r.db.QueryRowContext(ctx, query, id).Scan(
&userID, &itemsJSON, &addrJSON, &amount, ¤cy, &status,
&paymentID, &cancelReason, &createdAt, &updatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("order %s not found: %w", id, err)
}
return nil, err
}
// 反序列化
var items []domain.OrderItem
if err := json.Unmarshal(itemsJSON, &items); err != nil {
return nil, err
}
var addr domain.Address
if err := json.Unmarshal(addrJSON, &addr); err != nil {
return nil, err
}
total, err := domain.NewMoney(amount, currency)
if err != nil {
return nil, err
}
// 重建聚合根(使用工厂方法)
order := domain.ReconstructOrder(
id, userID, items, addr, total,
domain.OrderStatus(status),
paymentID.String,
cancelReason.String,
createdAt, updatedAt,
)
return order, nil
}
func (r *postgresOrderRepo) NextID(ctx context.Context) (domain.OrderID, error) {
return domain.OrderID(uuid.New().String()), nil
}
// ... 其他方法实现 ...
为了让领域层可以"重建"聚合根(从数据库读取时),我们需要一个特殊的重构方法:
// ReconstructOrder 重构订单(从数据库读取时使用)
// 注意:这个方法不会触发领域事件,因为它只是恢复既有状态
func ReconstructOrder(
id OrderID,
userID string,
items []OrderItem,
shippingAddr Address,
totalAmount Money,
status OrderStatus,
paymentID, cancelReason string,
createdAt, updatedAt time.Time,
) *Order {
return &Order{
id: id,
userID: userID,
items: items,
shippingAddr: shippingAddr,
totalAmount: totalAmount,
status: status,
paymentID: paymentID,
cancelReason: cancelReason,
events: []DomainEvent{}, // 重构时不触发事件
createdAt: createdAt,
updatedAt: updatedAt,
}
}
应用服务(Application Service)
应用服务是领域层的"指挥官",负责:
- 编排领域对象完成用例
- 管理事务
- 发布领域事件
- 协调多个限界上下文
// internal/order/application/service.go
package application
import (
"context"
"errors"
"fmt"
"github.com/yourusername/eshop/internal/order/application/dto"
"github.com/yourusername/eshop/internal/order/domain"
"github.com/yourusername/eshop/pkg/events"
)
// OrderService 订单应用服务
// 注意:应用服务只做"薄薄的一层",不包含业务规则
type OrderService struct {
orderRepo domain.OrderRepository
inventoryClient InventoryClient // 防腐层(调用库存上下文)
eventBus events.Bus
uow domain.UnitOfWork
}
// NewOrderService 创建应用服务
func NewOrderService(
orderRepo domain.OrderRepository,
inventoryClient InventoryClient,
eventBus events.Bus,
uow domain.UnitOfWork,
) *OrderService {
return &OrderService{
orderRepo: orderRepo,
inventoryClient: inventoryClient,
eventBus: eventBus,
uow: uow,
}
}
// PlaceOrder 下单(用例)
func (s *OrderService) PlaceOrder(ctx context.Context, req *dto.PlaceOrderRequest) (*dto.OrderResponse, error) {
// 1. 生成订单 ID
orderID, err := s.orderRepo.NextID(ctx)
if err != nil {
return nil, fmt.Errorf("failed to generate order ID: %w", err)
}
// 2. 构建订单项
var items []domain.OrderItem
for i, item := range req.Items {
// 调用库存服务(通过防腐层)获取商品信息和价格
productInfo, err := s.inventoryClient.GetProductInfo(ctx, item.ProductID)
if err != nil {
return nil, fmt.Errorf("failed to get product info: %w", err)
}
price, err := domain.NewMoney(productInfo.PriceInCents, productInfo.Currency)
if err != nil {
return nil, err
}
orderItem, err := domain.NewOrderItem(
domain.OrderItemID(fmt.Sprintf("%s-%d", orderID, i)),
item.ProductID,
productInfo.Name,
price,
item.Quantity,
)
if err != nil {
return nil, err
}
items = append(items, orderItem)
}
// 3. 构建收货地址
addr, err := domain.NewAddress(
req.ShippingAddress.Country,
req.ShippingAddress.Province,
req.ShippingAddress.City,
req.ShippingAddress.District,
req.ShippingAddress.Street,
req.ShippingAddress.PostalCode,
req.ShippingAddress.Recipient,
req.ShippingAddress.Phone,
)
if err != nil {
return nil, fmt.Errorf("invalid shipping address: %w", err)
}
// 4. 创建订单(领域对象负责业务规则)
order, err := domain.NewOrder(orderID, req.UserID, items, addr, "CNY")
if err != nil {
return nil, fmt.Errorf("failed to create order: %w", err)
}
// 5. 预留库存(通过防腐层调用库存上下文)
reservationID, err := s.inventoryClient.ReserveStock(ctx, req.UserID, string(orderID), items)
if err != nil {
return nil, fmt.Errorf("failed to reserve inventory: %w", err)
}
// 6. 在事务中保存订单并发布事件
err = s.uow.Execute(ctx, func(ctx context.Context) error {
if err := s.orderRepo.Save(ctx, order); err != nil {
return err
}
// 发布领域事件
domainEvents := order.Events()
for _, event := range domainEvents {
if err := s.eventBus.Publish(ctx, event); err != nil {
return err
}
}
return nil
})
if err != nil {
// 补偿操作:释放库存
_ = s.inventoryClient.ReleaseReservation(ctx, reservationID)
return nil, fmt.Errorf("failed to save order: %w", err)
}
// 7. 返回 DTO
return dto.FromOrder(order), nil
}
// ConfirmPayment 确认支付(用例)
func (s *OrderService) ConfirmPayment(ctx context.Context, orderID string, paymentID string) error {
// 1. 加载聚合根
id := domain.OrderID(orderID)
order, err := s.orderRepo.GetByID(ctx, id)
if err != nil {
return err
}
// 2. 调用领域方法(业务规则在聚合根内部)
if err := order.ConfirmPayment(paymentID); err != nil {
return err
}
// 3. 在事务中保存并发布事件
return s.uow.Execute(ctx, func(ctx context.Context) error {
if err := s.orderRepo.Save(ctx, order); err != nil {
return err
}
for _, event := range order.Events() {
if err := s.eventBus.Publish(ctx, event); err != nil {
return err
}
}
return nil
})
}
// CancelOrder 取消订单
func (s *OrderService) CancelOrder(ctx context.Context, orderID, reason string) error {
id := domain.OrderID(orderID)
order, err := s.orderRepo.GetByID(ctx, id)
if err != nil {
return err
}
if err := order.Cancel(reason); err != nil {
return err
}
return s.uow.Execute(ctx, func(ctx context.Context) error {
if err := s.orderRepo.Save(ctx, order); err != nil {
return err
}
for _, event := range order.Events() {
if err := s.eventBus.Publish(ctx, event); err != nil {
return err
}
}
return nil
})
}
防腐层(Anti-Corruption Layer)
当两个限界上下文需要通信时,防腐层可以保护你的领域模型不受外部模型污染:
// internal/order/application/inventory_client.go
package application
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/yourusername/eshop/internal/order/domain"
)
// InventoryClient 库存客户端接口(防腐层)
// 这个接口是用"订单上下文"的语言定义的,不包含任何库存上下文的细节
type InventoryClient interface {
GetProductInfo(ctx context.Context, productID string) (*ProductInfo, error)
ReserveStock(ctx context.Context, userID, orderID string, items []domain.OrderItem) (string, error)
ReleaseReservation(ctx context.Context, reservationID string) error
}
// ProductInfo 商品信息(订单上下文视角)
type ProductInfo struct {
ProductID string
Name string
PriceInCents int64
Currency string
InStock bool
}
// httpInventoryClient 基于 HTTP 的库存客户端实现
// 它把库存上下文的 API 转换为订单上下文的模型
type httpInventoryClient struct {
baseURL string
httpClient *http.Client
}
func NewHTTPInventoryClient(baseURL string) InventoryClient {
return &httpInventoryClient{
baseURL: baseURL,
httpClient: &http.Client{},
}
}
func (c *httpInventoryClient) GetProductInfo(ctx context.Context, productID string) (*ProductInfo, error) {
req, err := http.NewRequestWithContext(ctx, "GET",
fmt.Sprintf("%s/api/v1/products/%s", c.baseURL, productID), nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("inventory service returned status %d", resp.StatusCode)
}
// 解析库存上下文的响应(可能是完全不同的结构)
var inventoryResp struct {
SKU string `json:"sku"`
DisplayName string `json:"display_name"`
Price float64 `json:"price"`
CurrencyCode string `json:"currency_code"`
StockCount int `json:"stock_count"`
}
if err := json.NewDecoder(resp.Body).Decode(&inventoryResp); err != nil {
return nil, err
}
// 转换为订单上下文的模型
return &ProductInfo{
ProductID: inventoryResp.SKU,
Name: inventoryResp.DisplayName,
PriceInCents: int64(inventoryResp.Price * 100),
Currency: inventoryResp.CurrencyCode,
InStock: inventoryResp.StockCount > 0,
}, nil
}
func (c *httpInventoryClient) ReserveStock(ctx context.Context, userID, orderID string, items []domain.OrderItem) (string, error) {
// 构建库存上下文的请求格式
type reserveItem struct {
SKU string `json:"sku"`
Quantity int `json:"quantity"`
}
var reqItems []reserveItem
for _, item := range items {
reqItems = append(reqItems, reserveItem{
SKU: item.ProductID(),
Quantity: item.Quantity(),
})
}
reqBody := struct {
UserID string `json:"user_id"`
OrderID string `json:"order_id"`
Items []reserveItem `json:"items"`
}{
UserID: userID,
OrderID: orderID,
Items: reqItems,
}
// ... 发送请求、解析响应、转换为订单上下文的 reservation ID ...
return "res_" + orderID, nil // 简化
}
func (c *httpInventoryClient) ReleaseReservation(ctx context.Context, reservationID string) error {
// ... 调用库存上下文 API 释放预留 ...
return nil
}
防腐层的关键价值:即使库存上下文的 API 大改,订单上下文也只需要修改防腐层的实现代码,领域模型完全不受影响。
领域服务(Domain Service)
某些业务逻辑不属于任何实体,比如跨聚合的计算,这时可以用领域服务:
// internal/order/domain/service.go
package domain
import (
"context"
"errors"
)
// PricingService 定价领域服务
// 负责复杂的定价逻辑(折扣、税费、运费等)
type PricingService struct {
discountRepo DiscountRepository
taxRepo TaxRepository
}
// DiscountRepository 折扣仓储
type DiscountRepository interface {
GetActiveDiscounts(ctx context.Context, userID string, productIDs []string) ([]Discount, error)
}
// TaxRepository 税率仓储
type TaxRepository interface {
GetTaxRate(ctx context.Context, country string) (float64, error)
}
// Discount 折扣
type Discount struct {
ID string
Code string
PercentOff float64
MaxDiscount Money
}
// NewPricingService 创建定价服务
func NewPricingService(discountRepo DiscountRepository, taxRepo TaxRepository) *PricingService {
return &PricingService{
discountRepo: discountRepo,
taxRepo: taxRepo,
}
}
// CalculateFinalAmount 计算最终金额
func (s *PricingService) CalculateFinalAmount(ctx context.Context, order *Order, userID string) (Money, error) {
// 1. 获取商品 ID 列表
productIDs := make([]string, 0, len(order.Items()))
for _, item := range order.Items() {
productIDs = append(productIDs, item.ProductID())
}
// 2. 应用折扣
discounts, err := s.discountRepo.GetActiveDiscounts(ctx, userID, productIDs)
if err != nil {
return Money{}, err
}
subtotal := order.TotalAmount()
discountAmount := Zero(subtotal.Currency())
for _, discount := range discounts {
// 计算折扣金额
discounted := subtotal.Multiply(discount.PercentOff / 100)
// 不超过最大折扣
if discount.MaxDiscount.GreaterThan(Zero(discount.MaxDiscount.Currency())) {
if discounted.GreaterThan(discount.MaxDiscount) {
discounted = discount.MaxDiscount
}
}
discountAmount, _ = discountAmount.Add(discounted)
}
afterDiscount, err := subtotal.Subtract(discountAmount)
if err != nil {
return Money{}, err
}
// 3. 应用税费
shippingCountry := order.ShippingAddress().Country()
taxRate, err := s.taxRepo.GetTaxRate(ctx, shippingCountry)
if err != nil {
return Money{}, err
}
taxAmount := afterDiscount.Multiply(taxRate / 100)
finalAmount, err := afterDiscount.Add(taxAmount)
if err != nil {
return Money{}, err
}
return finalAmount, nil
}
// ShippingCostCalculator 运费计算领域服务
type ShippingCostCalculator struct {
rules []ShippingRule
}
// ShippingRule 运费规则
type ShippingRule struct {
MinAmount Money
MaxAmount Money
Cost Money
FreeShipping bool
}
func (c *ShippingCostCalculator) Calculate(amount Money, addr Address) (Money, error) {
for _, rule := range c.rules {
if amount.GreaterThan(rule.MinAmount) || amount.Equals(rule.MinAmount) {
if rule.FreeShipping {
return Zero(amount.Currency()), nil
}
return rule.Cost, nil
}
}
return Money{}, errors.New("no shipping rule matched")
}
CQRS:命令与查询分离
CQRS(Command Query Responsibility Segregation)是一种将"写操作"和"读操作"分离的模式。写操作走聚合根保证一致性,读操作可以绕过聚合根直接查询:
// internal/order/application/command/place_order.go
package command
import (
"context"
"github.com/yourusername/eshop/internal/order/application/dto"
"github.com/yourusername/eshop/internal/order/application"
)
// PlaceOrderCommand 下单命令
type PlaceOrderCommand struct {
UserID string
Items []dto.OrderItemRequest
ShippingAddress dto.AddressRequest
}
// PlaceOrderHandler 下单命令处理器
type PlaceOrderHandler struct {
service *application.OrderService
}
func NewPlaceOrderHandler(service *application.OrderService) *PlaceOrderHandler {
return &PlaceOrderHandler{service: service}
}
func (h *PlaceOrderHandler) Handle(ctx context.Context, cmd *PlaceOrderCommand) (*dto.OrderResponse, error) {
req := &dto.PlaceOrderRequest{
UserID: cmd.UserID,
Items: cmd.Items,
ShippingAddress: cmd.ShippingAddress,
}
return h.service.PlaceOrder(ctx, req)
}
// internal/order/application/query/order_list.go
package query
import (
"context"
"database/sql"
"github.com/yourusername/eshop/internal/order/application/dto"
)
// OrderListQuery 订单列表查询
type OrderListQuery struct {
db *sql.DB
}
func NewOrderListQuery(db *sql.DB) *OrderListQuery {
return &OrderListQuery{db: db}
}
// ListByUser 查询用户的订单列表
// 注意:这里直接查询数据库,绕过聚合根,以获得更好的读性能
func (q *OrderListQuery) ListByUser(ctx context.Context, userID string, page, pageSize int) (*dto.OrderListResponse, error) {
offset := (page - 1) * pageSize
query := `
SELECT o.id, o.user_id, o.status, o.total_amount, o.currency, o.created_at,
COUNT(oi.id) as item_count
FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.user_id = $1
GROUP BY o.id
ORDER BY o.created_at DESC
LIMIT $2 OFFSET $3
`
rows, err := q.db.QueryContext(ctx, query, userID, pageSize, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var orders []dto.OrderSummary
for rows.Next() {
var o dto.OrderSummary
if err := rows.Scan(
&o.ID, &o.UserID, &o.Status, &o.TotalAmount,
&o.Currency, &o.CreatedAt, &o.ItemCount,
); err != nil {
return nil, err
}
orders = append(orders, o)
}
// 查询总数
var total int64
err = q.db.QueryRowContext(ctx,
"SELECT COUNT(*) FROM orders WHERE user_id = $1", userID).Scan(&total)
if err != nil {
return nil, err
}
return &dto.OrderListResponse{
Orders: orders,
Total: total,
Page: page,
}, nil
}
CQRS 的优势在于:写模型保证业务一致性,读模型可以针对查询场景优化(比如使用 Elasticsearch、Redis 缓存等)。
事件溯源(Event Sourcing)
事件溯源是一种特殊的持久化方式:不存储对象的当前状态,而是存储所有的事件,通过重放事件来重建对象。
// internal/order/domain/event_store.go
package domain
import (
"context"
"time"
)
// StoredEvent 存储的事件
type StoredEvent struct {
EventID string
AggregateID string
EventType string
Payload []byte
Version int
OccurredAt time.Time
}
// EventStore 事件存储接口
type EventStore interface {
// SaveEvents 保存事件
SaveEvents(ctx context.Context, aggregateID string, events []DomainEvent, expectedVersion int) error
// LoadEvents 加载事件
LoadEvents(ctx context.Context, aggregateID string) ([]StoredEvent, error)
}
// EventSourcedOrder 基于事件溯源的订单聚合根
type EventSourcedOrder struct {
id OrderID
userID string
items []OrderItem
status OrderStatus
version int
uncommitted []DomainEvent
}
// Load 从事件流重建聚合根
func (o *EventSourcedOrder) Load(events []StoredEvent) error {
for _, stored := range events {
event, err := DeserializeEvent(stored.EventType, stored.Payload)
if err != nil {
return err
}
o.apply(event)
o.version++
}
return nil
}
// apply 应用事件到聚合根(私有方法)
func (o *EventSourcedOrder) apply(event DomainEvent) {
switch e := event.(type) {
case *OrderCreatedEvent:
o.id = OrderID(e.OrderID)
o.userID = e.UserID
o.items = e.Items
o.status = StatusPending
case *OrderPaidEvent:
o.status = StatusPaid
case *OrderShippedEvent:
o.status = StatusShipped
case *OrderCancelledEvent:
o.status = StatusCancelled
}
}
// Record 记录新事件
func (o *EventSourcedOrder) Record(event DomainEvent) {
o.apply(event)
o.uncommitted = append(o.uncommitted, event)
}
// UncommittedEvents 获取未提交的事件
func (o *EventSourcedOrder) UncommittedEvents() []DomainEvent {
events := o.uncommitted
o.uncommitted = nil
return events
}
// Version 获取当前版本
func (o *EventSourcedOrder) Version() int {
return o.version
}
事件溯源的优势:
- 完整的历史:可以回溯到任何历史状态
- 审计日志:事件本身就是审计日志
- 灵活的查询:可以通过投影(Projection)生成多种查询视图
- 时间旅行:可以"重放"事件到某个时间点
HTTP 接口层
最后让我们看看如何在 HTTP 接口中使用应用服务:
// internal/order/interfaces/http/handler.go
package http
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/yourusername/eshop/internal/order/application"
"github.com/yourusername/eshop/internal/order/application/command"
"github.com/yourusername/eshop/internal/order/application/dto"
"github.com/yourusername/eshop/internal/order/application/query"
)
// OrderHandler 订单 HTTP 处理器
type OrderHandler struct {
placeOrderHandler *command.PlaceOrderHandler
orderService *application.OrderService
orderListQuery *query.OrderListQuery
}
func NewOrderHandler(
placeOrderHandler *command.PlaceOrderHandler,
orderService *application.OrderService,
orderListQuery *query.OrderListQuery,
) *OrderHandler {
return &OrderHandler{
placeOrderHandler: placeOrderHandler,
orderService: orderService,
orderListQuery: orderListQuery,
}
}
// Routes 配置路由
func (h *OrderHandler) Routes() chi.Router {
r := chi.NewRouter()
r.Post("/", h.PlaceOrder)
r.Get("/", h.ListOrders)
r.Get("/{id}", h.GetOrder)
r.Post("/{id}/pay", h.ConfirmPayment)
r.Post("/{id}/cancel", h.CancelOrder)
return r
}
// PlaceOrder 下单
func (h *OrderHandler) PlaceOrder(w http.ResponseWriter, r *http.Request) {
var req dto.PlaceOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
// 从认证中间件获取用户 ID
userID := r.Context().Value("userID").(string)
req.UserID = userID
cmd := &command.PlaceOrderCommand{
UserID: req.UserID,
Items: req.Items,
ShippingAddress: req.ShippingAddress,
}
resp, err := h.placeOrderHandler.Handle(r.Context(), cmd)
if err != nil {
writeError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(resp)
}
// ConfirmPayment 确认支付
func (h *OrderHandler) ConfirmPayment(w http.ResponseWriter, r *http.Request) {
orderID := chi.URLParam(r, "id")
var req struct {
PaymentID string `json:"payment_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := h.orderService.ConfirmPayment(r.Context(), orderID, req.PaymentID); err != nil {
writeError(w, err)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}
// ListOrders 查询订单列表(走 CQRS 的查询侧)
func (h *OrderHandler) ListOrders(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("userID").(string)
page := parseIntParam(r, "page", 1)
pageSize := parseIntParam(r, "page_size", 20)
resp, err := h.orderListQuery.ListByUser(r.Context(), userID, page, pageSize)
if err != nil {
writeError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// ... 其他处理器方法 ...
func writeError(w http.ResponseWriter, err error) {
status := http.StatusInternalServerError
message := "internal server error"
// 根据错误类型返回合适的状态码
switch err.Error() {
case "order not found":
status = http.StatusNotFound
case "insufficient inventory":
status = http.StatusConflict
default:
// 业务错误
if isBusinessError(err) {
status = http.StatusBadRequest
message = err.Error()
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]string{"error": message})
}
总结
恭喜你完成了领域驱动设计的学习之旅!让我们回顾核心要点:
- 战略设计:用限界上下文划分业务边界,用通用语言统一团队沟通
- 值对象:封装业务规则,不可变,按值比较(如 Money、Address)
- 实体与聚合根:实体有唯一标识,聚合根是修改聚合内对象的唯一入口
- 领域事件:描述业务中发生的重要事件,用于限界上下文之间的松耦合通信
- 仓储:接口定义在领域层,实现在基础设施层,实现依赖倒置
- 应用服务:薄薄的一层,只负责编排,不包含业务规则
- 防腐层:保护领域模型不受外部模型污染
- CQRS:命令和查询分离,写模型保证一致性,读模型优化性能
- 事件溯源:存储事件而非状态,可以重放事件重建历史
DDD 不是银弹,它适用于业务逻辑复杂的系统。对于简单的 CRUD 应用,DDD 反而会增加不必要的复杂度。但当你需要构建一个能持续演进、承载复杂业务规则的系统时,DDD 就是你的最佳武器。
Go 语言的简洁和强类型特性与 DDD 相得益彰——没有继承、没有反射魔法、没有复杂的注解,一切都清晰明了。这正是构建长期可维护系统所需要的。
希望这篇长达 96 篇的 Go 语言入门系列能帮助你从入门到精通。记住,编程之路没有终点,每一行代码都是一次新的探索。祝你编码愉快!
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。