3. 领域驱动设计基础:实体、值对象、聚合、领域服务、仓储模式
领域驱动设计(Domain-Driven Design, DDD)为构建复杂业务系统提供了战略与战术层面的指导。在《Go权限审计网关领域边界》课程中,DDD的战术模式是定义权限模型、审计规则和网关核心逻辑的基石。本章将深入剖析实体、值对象、聚合、领域服务与仓储模式,并结合Go语言的实现特性进行讲解。
3.1 实体(Entity)
实体是具有唯一标识且在其生命周期内状态会持续变化的对象。在权限审计网关中,用户(User)、角色(Role)、资源(Resource) 都是典型的实体。判断一个对象是否为实体的核心标准是:是否拥有一个不随属性变化而改变的ID。
| 特征 | 说明 | Go实现示例 |
|---|---|---|
| 唯一标识 | 使用UUID或自增ID,不可变 | ID string |
| 可变属性 | 姓名、邮箱、状态等可被修改 | Name string |
| 相等性 | 通过ID判断,而非属性值 | a.ID == b.ID |
// User 实体定义
type User struct {
ID string
Name string
Email string
Status UserStatus
CreatedAt time.Time
}
// 实体相等性比较
func (u *User) Equals(other *User) bool {
return u.ID == other.ID
}
实践要点:在Go中,实体通常作为结构体(struct)实现。避免将实体直接暴露给外部层,建议通过仓储(Repository)接口进行持久化操作。
3.2 值对象(Value Object)
值对象通过属性值来定义自身,没有唯一标识,且是不可变的。在权限审计场景中,IP地址、时间范围、权限策略规则 等适合建模为值对象。值对象的核心价值在于封装复杂概念,消除“基本类型偏执”。
// IPRange 值对象:表示一个IP地址段
type IPRange struct {
Start net.IP
End net.IP
}
// 值对象不可变:返回新实例
func (r IPRange) Contains(ip net.IP) bool {
// 比较逻辑
return bytes.Compare(ip, r.Start) >= 0 && bytes.Compare(ip, r.End) <= 0
}
// 值对象相等性:比较所有属性
func (r IPRange) Equals(other IPRange) bool {
return r.Start.Equal(other.Start) && r.End.Equal(other.End)
}
| 对比维度 | 实体 | 值对象 |
|---|---|---|
| 标识 | 有唯一ID | 无ID,由属性定义 |
| 可变性 | 可变 | 不可变 |
| 生命周期 | 独立存在 | 依附于实体 |
| 相等性 | ID相等 | 所有属性相等 |
| 权限网关示例 | 用户、角色、资源 | IP范围、时间窗口、策略条件 |
3.3 聚合(Aggregate)与聚合根(Aggregate Root)
聚合是一组相关对象的集合,外部只能通过聚合根(Aggregate Root)访问聚合内部的对象。聚合保证了数据的一致性和业务不变量的保护。在权限审计网关中,权限策略聚合 是一个典型例子:聚合根为 Policy,内部包含多个 Rule(值对象)和 TargetResource(实体)。
// Policy 聚合根
type Policy struct {
ID string
Name string
Description string
Rules []Rule // 值对象集合
Targets []Resource // 实体集合
Version int
}
// 聚合内方法:保证业务不变量
func (p *Policy) AddRule(rule Rule) error {
// 校验规则是否重复
for _, r := range p.Rules {
if r.Equals(rule) {
return ErrDuplicateRule
}
}
p.Rules = append(p.Rules, rule)
p.Version++
return nil
}
聚合设计原则:
- 聚合内部通过聚合根保证一致性,外部不能直接修改内部对象。
- 一次事务只修改一个聚合实例。
- 聚合应尽量小,避免性能瓶颈。
3.4 领域服务(Domain Service)
当某个业务操作无法自然地归属于某个实体或值对象时,应将其建模为领域服务。领域服务是无状态的,它协调多个聚合或外部系统完成一个业务用例。在权限审计网关中,权限校验服务 是一个典型的领域服务。
// AuthorizationService 领域服务:负责权限校验
type AuthorizationService struct {
policyRepo PolicyRepository
userRepo UserRepository
}
func (s *AuthorizationService) CheckAccess(userID string, resourceID string, action Action) (bool, error) {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return false, err
}
policies, err := s.policyRepo.FindByUserRole(user.RoleID)
if err != nil {
return false, err
}
for _, policy := range policies {
if policy.Matches(resourceID, action) {
return true, nil
}
}
return false, nil
}
领域服务 vs 应用服务:领域服务包含业务逻辑和规则,应用服务负责流程编排、事务管理和基础设施调用。在Go项目中,领域服务应放在
internal/domain/service 包中。
3.5 仓储模式(Repository Pattern)
仓储模式为聚合提供了一种类似集合的访问接口,将领域层与持久化技术解耦。在Go中,仓储通常定义为接口,实现放在基础设施层。仓储只负责聚合根的存取,不暴露内部实体。
// PolicyRepository 仓储接口
type PolicyRepository interface {
Save(policy *Policy) error
FindByID(id string) (*Policy, error)
FindByUserRole(roleID string) ([]*Policy, error)
Delete(id string) error
}
// PostgresPolicyRepository 仓储实现(基础设施层)
type PostgresPolicyRepository struct {
db *sql.DB
}
func (r *PostgresPolicyRepository) Save(policy *Policy) error {
// 使用事务保存聚合根及其内部对象
tx, _ := r.db.Begin()
// 保存 Policy 表
// 保存 Rules 表
// 保存 Targets 表
return tx.Commit()
}
| 仓储原则 | 说明 |
|---|---|
| 面向聚合 | 每个聚合根对应一个仓储接口 |
| 接口在领域层 | 领域层定义接口,基础设施层实现 |
| 事务边界 | 仓储方法内部保证聚合的一致性 |
| 不暴露内部 | 仓储不提供直接访问内部实体的方法 |
3.6 在Go权限审计网关中的实践建议
- 实体设计:使用
string类型的UUID作为ID,避免暴露自增ID给外部。 - 值对象:优先使用不可变结构体,方法返回新实例而非修改原对象。
- 聚合边界:权限策略聚合应包含规则和资源引用,但不要包含用户信息,保持聚合小巧。
- 领域服务:权限校验、审计日志记录等跨聚合操作应放在领域服务中。
- 仓储实现:使用Go的
database/sql或gorm实现,但接口定义在领域层。
Go语言特有优势:Go的接口隐式实现特性使得仓储模式实现非常自然。领域层定义接口,基础设施层无需显式声明
implements,降低了耦合度。