战术设计:实体与值对象的Go实现、聚合根的设计原则、仓储模式
战术设计是领域驱动设计(DDD)从战略蓝图落地到代码实现的关键桥梁。本章将聚焦于DDD中最核心的三个战术构件:实体(Entity)、值对象(Value Object)以及聚合根(Aggregate Root),并结合Go语言的特性,探讨如何通过仓储模式(Repository Pattern)实现领域对象的持久化隔离。理解并正确运用这些模式,是构建高内聚、低耦合微服务的基础。
1. 实体与值对象:领域模型的基本单元
在DDD中,区分实体和值对象是建模的第一步。实体拥有唯一的身份标识,其状态会随时间变化;而值对象则描述事物的属性,没有独立身份,通过其所有属性的值来定义。
| 特性 | 实体 (Entity) | 值对象 (Value Object) |
|---|---|---|
| 身份标识 | 必须有唯一ID(如UUID、自增ID) | 无独立ID,由属性值定义 |
| 可变性 | 可变(Mutable),状态可更新 | 不可变(Immutable),替换而非修改 |
| 相等性 | 通过ID判断相等 | 通过所有属性值判断相等 |
| 生命周期 | 有独立生命周期,可被追踪 | 无生命周期,依附于实体 |
| Go实现要点 | 使用struct + 指针接收者方法 | 使用struct + 值接收者方法,返回新实例 |
1.1 实体的Go实现
在Go中,实体通常通过一个包含唯一标识符的struct来表示。所有修改实体状态的方法都应使用指针接收者,以确保对同一实例的修改生效。
// User 实体,拥有唯一ID
type User struct {
ID string // 唯一标识
Name string
Email string
CreatedAt time.Time
UpdatedAt time.Time
}
// ChangeEmail 修改邮箱,使用指针接收者
func (u *User) ChangeEmail(newEmail string) error {
if !isValidEmail(newEmail) {
return errors.New("invalid email format")
}
u.Email = newEmail
u.UpdatedAt = time.Now()
return nil
}
// NewUser 工厂方法,确保创建时生成ID
func NewUser(name, email string) (*User, error) {
if name == "" || email == "" {
return nil, errors.New("name and email are required")
}
return &User{
ID: uuid.New().String(),
Name: name,
Email: email,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}, nil
}
1.2 值对象的Go实现
值对象是不可变的。在Go中,我们通过不暴露内部字段、使用值接收者并返回新实例来实现不可变性。值对象通常用于表示地址、货币、日期范围等。
// Address 值对象,不可变
type Address struct {
street string
city string
zipCode string
country string
}
// NewAddress 构造函数,返回指针或值均可,但推荐返回值以强调不可变性
func NewAddress(street, city, zipCode, country string) (Address, error) {
if street == "" || city == "" {
return Address{}, errors.New("street and city are required")
}
return Address{
street: street,
city: city,
zipCode: zipCode,
country: country,
}, nil
}
// WithStreet 返回一个新的Address,仅修改街道
func (a Address) WithStreet(newStreet string) Address {
return Address{
street: newStreet,
city: a.city,
zipCode: a.zipCode,
country: a.country,
}
}
// Equals 值对象相等性比较
func (a Address) Equals(other Address) bool {
return a.street == other.street &&
a.city == other.city &&
a.zipCode == other.zipCode &&
a.country == other.country
}
2. 聚合根的设计原则
聚合(Aggregate)是一组相关对象的集合,聚合根(Aggregate Root)是这个集合的入口和唯一外部访问点。聚合根保证了内部对象的一致性和不变性。
2.1 聚合根的核心原则
- 一致性边界:聚合内部的所有变更必须通过聚合根进行,确保业务规则始终得到执行。
- 原子性操作:对聚合的每次操作都应是一个完整的事务边界,要么全部成功,要么全部失败。
- 引用外部ID:聚合根之间通过全局唯一标识符(ID)引用,而不是直接持有对象引用,以降低耦合。
- 小聚合原则:聚合应尽量小,只包含必须保持一致性的对象。过大的聚合会导致性能问题和并发冲突。
2.2 聚合根的Go实现示例
以下是一个Order聚合根的实现,它包含OrderItem实体和Money值对象。
// Money 值对象
type Money struct {
amount float64
currency string
}
func NewMoney(amount float64, currency string) Money {
return Money{amount: amount, currency: currency}
}
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, errors.New("currency mismatch")
}
return Money{amount: m.amount + other.amount, currency: m.currency}, nil
}
// OrderItem 实体,属于Order聚合
type OrderItem struct {
ProductID string
Quantity int
UnitPrice Money
}
// Order 聚合根
type Order struct {
ID string
CustomerID string
Items []OrderItem
Total Money
Status string
}
// NewOrder 创建订单聚合根
func NewOrder(id, customerID string) (*Order, error) {
if id == "" || customerID == "" {
return nil, errors.New("id and customerID are required")
}
return &Order{
ID: id,
CustomerID: customerID,
Items: []OrderItem{},
Total: NewMoney(0, "CNY"),
Status: "pending",
}, nil
}
// AddItem 通过聚合根添加商品,保证总价计算正确
func (o *Order) AddItem(productID string, quantity int, unitPrice Money) error {
if o.Status != "pending" {
return errors.New("can only add items to pending order")
}
item := OrderItem{
ProductID: productID,
Quantity: quantity,
UnitPrice: unitPrice,
}
o.Items = append(o.Items, item)
// 重新计算总价,保证一致性
newTotal, _ := o.Total.Add(NewMoney(float64(quantity)*unitPrice.amount, unitPrice.currency))
o.Total = newTotal
return nil
}
- 聚合根是否拥有全局唯一ID?
- 外部对象是否只能通过聚合根ID引用该聚合?
- 聚合内部的所有业务规则是否都在聚合根的方法中执行?
- 聚合是否足够小,只包含必须保持一致性的对象?
3. 仓储模式:领域与基础设施的桥梁
仓储(Repository)模式为领域模型提供了对象集合式的访问接口,它将领域层的持久化需求与基础设施层的具体实现(如数据库、缓存)解耦。在DDD中,仓储只针对聚合根设计。
3.1 仓储接口定义(领域层)
仓储接口属于领域层,它定义了如何获取和持久化聚合根,而不关心具体的技术实现。
// OrderRepository 仓储接口,定义在领域层
type OrderRepository interface {
Save(order *Order) error
FindByID(id string) (*Order, error)
FindByCustomerID(customerID string) ([]*Order, error)
Delete(id string) error
}
3.2 仓储实现(基础设施层)
具体的实现(如基于PostgreSQL或MongoDB)放在基础设施层。领域层通过依赖注入使用仓储接口,完全不依赖具体实现。
// PostgresOrderRepository 基础设施层的实现
type PostgresOrderRepository struct {
db *sql.DB
}
func NewPostgresOrderRepository(db *sql.DB) *PostgresOrderRepository {
return &PostgresOrderRepository{db: db}
}
func (r *PostgresOrderRepository) Save(order *Order) error {
// 使用事务保证聚合内多个表的原子性操作
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// 保存订单主表
_, err = tx.Exec(`INSERT INTO orders (id, customer_id, total_amount, total_currency, status)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE SET ...`,
order.ID, order.CustomerID, order.Total.amount, order.Total.currency, order.Status)
if err != nil {
return err
}
// 保存订单项
for _, item := range order.Items {
_, err = tx.Exec(`INSERT INTO order_items (order_id, product_id, quantity, unit_price_amount, unit_price_currency)
VALUES ($1, $2, $3, $4, $5)`,
order.ID, item.ProductID, item.Quantity, item.UnitPrice.amount, item.UnitPrice.currency)
if err != nil {
return err
}
}
return tx.Commit()
}
func (r *PostgresOrderRepository) FindByID(id string) (*Order, error) {
// 实现查询并重建聚合根
row := r.db.QueryRow(`SELECT id, customer_id, total_amount, total_currency, status FROM orders WHERE id = $1`, id)
order := &Order{}
err := row.Scan(&order.ID, &order.CustomerID, &order.Total.amount, &order.Total.currency, &order.Status)
if err != nil {
return nil, err
}
// 加载订单项
rows, err := r.db.Query(`SELECT product_id, quantity, unit_price_amount, unit_price_currency FROM order_items WHERE order_id = $1`, id)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var item OrderItem
rows.Scan(&item.ProductID, &item.Quantity, &item.UnitPrice.amount, &item.UnitPrice.currency)
order.Items = append(order.Items, item)
}
return order, nil
}
Save方法应保证整个聚合的持久化一致性,而不仅仅是单张表。
4. 战术设计全景图
下面的SVG图展示了实体、值对象、聚合根和仓储在DDD分层架构中的协作关系。
5. 战术设计决策矩阵
在实际建模过程中,快速判断一个概念应该设计为实体还是值对象,以及如何设计聚合根,可以参考以下决策矩阵:
| 判断维度 | 实体 | 值对象 | 聚合根 |
|---|---|---|---|
| 是否需要追踪变化? | 是 | 否 | 是(作为聚合入口) |
| 是否有独立生命周期? | 是 | 否 | 是 |
| 是否会被其他对象共享? | 通常不共享引用 | 可以安全共享 | 通过ID被外部引用 |
| 修改频率 | 高 | 低(替换而非修改) | 中高 |
| 是否包含业务规则? | 是 | 通常只包含自校验 | 是(核心业务规则) |
| 持久化方式 | 独立表 | 嵌入或序列化 | 主表 + 子表 |
6. 总结与最佳实践
- 优先使用值对象:在建模时,优先考虑使用值对象。只有当概念需要唯一身份和生命周期追踪时,才将其设计为实体。这能显著减少模型的复杂度。
- 聚合根是事务边界:一个请求只应加载和修改一个聚合根实例。跨聚合的修改应通过领域事件(Domain Events)异步处理,而不是在同一个事务中。
- 仓储只对聚合根:不要为每个实体都创建仓储。只有聚合根才拥有仓储,聚合内部的实体通过聚合根来访问和持久化。
- 依赖抽象:应用层(Application Layer)只依赖领域层的仓储接口,而非具体实现。这为单元测试和更换持久化方案提供了极大的灵活性。