仓储层是否违反领域驱动设计(DDD)的聚合根封装规则?
核心矛盾很明确:客户端需要创建符合业务规则的新聚合根(自动生成ID、初始化默认值、验证输入),而仓储需要从持久化数据重建已有聚合根(复用存储的ID、日期等字段,不需要重新初始化),但要避免客户端拿到重建能力破坏封装。下面是几个落地的解决思路:
1. 包级私有/内部可见的重建方法
把聚合根和仓储放在同一个代码包(模块)内,给聚合根添加一个仅包内可见的重建方法,客户端在包外无法访问这个方法,只有仓储能调用它来从存储数据构建实例。
以Go语言为例:
// 聚合根定义在 order 包内 package order import ( "errors" "time" "github.com/google/uuid" ) type Order struct { ID string CreatedAt time.Time CustomerID string // 其他业务字段 } // 公开的New方法:供客户端创建新订单,自动生成ID和创建时间 func NewOrder(customerID string) (*Order, error) { if customerID == "" { return nil, errors.New("customer ID cannot be empty") } return &Order{ ID: uuid.NewString(), CreatedAt: time.Now(), CustomerID: customerID, }, nil } // 包内可见的reconstruct方法:仅仓储能调用,从存储数据重建订单 func reconstructOrder(id string, createdAt time.Time, customerID string) (*Order, error) { if id == "" { return nil, errors.New("order ID cannot be empty") } return &Order{ ID: id, CreatedAt: createdAt, CustomerID: customerID, }, nil }
仓储和聚合根同属order包,直接调用reconstructOrder方法从数据库查询结果构建实例;客户端只能调用公开的NewOrder,无法接触到重建逻辑。
2. 接口隔离客户端访问
给聚合根定义两个接口:一个面向客户端的业务接口,仅暴露业务行为;另一个面向仓储的重建接口,包含重建方法。聚合根实现这两个接口,但客户端只能拿到业务接口类型的实例,无法访问重建方法。
以Java为例:
import java.time.LocalDateTime; import java.util.UUID; // 客户端可见的业务接口 public interface Order { void addItem(Item item); void confirm(); // 仅暴露业务行为,无创建/重建方法 } // 仓储可见的重建接口(包级私有) interface ReconstructableOrder extends Order { static ReconstructableOrder reconstruct(String id, LocalDateTime createdAt, CustomerId customerId) { return new OrderImpl(id, createdAt, customerId); } } // 聚合根实现类(包级私有) class OrderImpl implements ReconstructableOrder { private final String id; private final LocalDateTime createdAt; private final CustomerId customerId; // 私有构造函数,仅内部使用 private OrderImpl(String id, LocalDateTime createdAt, CustomerId customerId) { this.id = id; this.createdAt = createdAt; this.customerId = customerId; if (id == null || createdAt == null || customerId == null) { throw new IllegalArgumentException("Invalid order data"); } } // 公开的工厂方法,供客户端创建新订单 public static Order newOrder(CustomerId customerId) { if (customerId == null) { throw new IllegalArgumentException("Customer ID cannot be null"); } return new OrderImpl(UUID.randomUUID().toString(), LocalDateTime.now(), customerId); } // 实现业务方法 @Override public void addItem(Item item) { /* 业务逻辑 */ } @Override public void confirm() { /* 业务逻辑 */ } }
客户端依赖Order接口,只能调用newOrder创建实例;仓储依赖ReconstructableOrder接口,调用reconstruct方法重建实例,完全隔离了两种场景的能力。
3. 仓储专属的内部工厂类
把重建逻辑封装在一个仅仓储能访问的工厂类里,比如聚合根的私有内部类,或者与仓储同包的工厂类,专门负责从持久化数据构建聚合根。
以C#为例:
using System; public interface IOrder { Guid Id { get; } DateTime CreatedAt { get; } Guid CustomerId { get; } void AddItem(Item item); } public class Order : IOrder { // 私有构造函数,禁止外部直接创建 private Order(Guid id, DateTime createdAt, Guid customerId) { Id = id; CreatedAt = createdAt; CustomerId = customerId; if (id == Guid.Empty) throw new ArgumentException("Order ID cannot be empty"); } // 公开的工厂方法:供客户端创建新订单 public static IOrder NewOrder(Guid customerId) { if (customerId == Guid.Empty) throw new ArgumentException("Customer ID cannot be empty"); return new Order(Guid.NewGuid(), DateTime.UtcNow, customerId); } // 仓储专属的内部工厂类:仅同程序集可见 internal static class OrderFactory { public static IOrder Reconstruct(Guid id, DateTime createdAt, Guid customerId) { return new Order(id, createdAt, customerId); } } // 业务字段与方法 public Guid Id { get; } public DateTime CreatedAt { get; } public Guid CustomerId { get; } public void AddItem(Item item) { /* 业务逻辑 */ } } // 仓储类与Order同程序集,可访问OrderFactory public class OrderRepository { private readonly AppDbContext _dbContext; public OrderRepository(AppDbContext dbContext) { _dbContext = dbContext; } public IOrder GetById(Guid id) { var dbData = _dbContext.Orders.Find(id); return Order.OrderFactory.Reconstruct(dbData.Id, dbData.CreatedAt, dbData.CustomerId); } }
客户端只能通过NewOrder创建实例,仓储通过内部的OrderFactory重建实例,完全避免了客户端滥用重建能力。
核心思路总结
所有方案的本质都是区分“创建新聚合根”和“重建已有聚合根”两个不同场景,通过语言的访问控制特性(包级私有、内部可见、接口隔离),把重建能力严格限制在仓储层,只向客户端开放符合业务规则的创建入口,从而保证聚合根的封装边界不被打破。
内容的提问来源于stack exchange,提问作者sultan motiri

