聚合生命周期内行为变更的约束实现方案问询
这确实是领域驱动设计里处理聚合生命周期状态时的典型痛点——既要严格保证不同状态下的操作合法性,又不想让核心聚合类被一堆InvalidOperationException判断搞得臃肿不堪。你提到的初始方案(硬编码状态检查+抛出异常)代码冗余且逻辑分散;而函数式语言里用不同类型代表不同状态的方式虽能在编译期杜绝非法操作,但直接照搬进面向对象体系会面临不可变聚合的设计成本,以及基类方法隐藏的弊端。下面分享几个面向对象体系里经过实践验证的优化方案:
方案1:状态模式(State Pattern)——行为与状态分离
状态模式是面向对象里处理状态依赖行为的经典方案,核心是把每个状态的专属行为封装到独立的状态类中,聚合类仅持有当前状态实例,所有操作都委托给状态类处理。
代码示例
public interface ITradeState { ITradeState Validate(string reason); ITradeState SubmitToLedger(AccountingInformation info); string GetValidationReason(); AccountingInformation GetAccountingInfo(); } // 已录入状态:仅允许执行验证操作 public class TypedTradeState : ITradeState { public ITradeState Validate(string reason) { // 执行验证逻辑(比如校验合约合法性) return new ValidatedTradeState(reason); } public ITradeState SubmitToLedger(AccountingInformation info) { throw new InvalidOperationException("未验证的交易无法提交至账本"); } public string GetValidationReason() { throw new InvalidOperationException("未验证的交易无验证理由"); } public AccountingInformation GetAccountingInfo() { throw new InvalidOperationException("未提交的交易无会计信息"); } } // 已验证状态:仅允许执行提交操作 public class ValidatedTradeState : ITradeState { private readonly string _validationReason; public ValidatedTradeState(string validationReason) { _validationReason = validationReason; } public ITradeState Validate(string reason) { throw new InvalidOperationException("已验证的交易无需重复验证"); } public ITradeState SubmitToLedger(AccountingInformation info) { // 执行提交逻辑(比如校验会计信息格式) return new SubmittedTradeState(_validationReason, info); } public string GetValidationReason() { return _validationReason; } public AccountingInformation GetAccountingInfo() { throw new InvalidOperationException("未提交的交易无会计信息"); } } // 已提交状态:仅允许读取信息,无修改操作 public class SubmittedTradeState : ITradeState { private readonly string _validationReason; private readonly AccountingInformation _accInfo; public SubmittedTradeState(string validationReason, AccountingInformation accInfo) { _validationReason = validationReason; _accInfo = accInfo; } public ITradeState Validate(string reason) { throw new InvalidOperationException("已提交的交易无法重新验证"); } public ITradeState SubmitToLedger(AccountingInformation info) { throw new InvalidOperationException("已提交的交易无法重复提交"); } public string GetValidationReason() { return _validationReason; } public AccountingInformation GetAccountingInfo() { return _accInfo; } } // 核心聚合类:仅负责状态委托与基础属性 public class Trade { private ITradeState _currentState; public Guid Id { get; } public Contract Contract { get; } public decimal Price { get; } public Trade(Guid id, Contract contract, decimal price) { Id = id; Contract = contract; Price = price; _currentState = new TypedTradeState(); } public void Validate(string reason) { _currentState = _currentState.Validate(reason); } public void SubmitToLedger(AccountingInformation info) { _currentState = _currentState.SubmitToLedger(info); } public string GetValidationReason() { return _currentState.GetValidationReason(); } public AccountingInformation GetAccountingInfo() { return _currentState.GetAccountingInfo(); } }
优缺点
- 优势:行为与状态分离,Trade类保持简洁;新增状态仅需添加新的状态类,符合开闭原则;每个状态的逻辑集中管理,易于维护。
- 不足:仍需在运行时抛出异常(无法完全做到编译期约束),但异常逻辑被封装在状态类中,比初始方案更清晰。
方案2:不可变聚合+状态专属接口——编译期约束操作权限
这个方案借鉴函数式的类型划分思路,用不同接口约束对应状态的操作权限,聚合类实现所有接口,但通过工厂方法返回对应状态的接口实例,从编译期就限制调用方的操作范围。
代码示例(不可变版本)
// 各状态专属接口:仅暴露允许的操作 public interface ITypedTrade { IValidatedTrade Validate(string reason); Guid Id { get; } Contract Contract { get; } decimal Price { get; } } public interface IValidatedTrade { ISubmittedTrade SubmitToLedger(AccountingInformation info); Guid Id { get; } Contract Contract { get; } decimal Price { get; } string ValidationReason { get; } } public interface ISubmittedTrade { Guid Id { get; } Contract Contract { get; } decimal Price { get; } string ValidationReason { get; } AccountingInformation AccountingInfo { get; } } // 核心聚合类:不可变,状态转换返回新实例 public class Trade : ITypedTrade, IValidatedTrade, ISubmittedTrade { public Guid Id { get; } public Contract Contract { get; } public decimal Price { get; } public string ValidationReason { get; } public AccountingInformation AccountingInfo { get; } // 私有构造函数:仅允许通过工厂方法创建实例 private Trade(Guid id, Contract contract, decimal price, string validationReason, AccountingInformation accountingInfo) { Id = id; Contract = contract; Price = price; ValidationReason = validationReason; AccountingInfo = accountingInfo; } // 工厂方法:创建初始状态的交易 public static ITypedTrade Create(Guid id, Contract contract, decimal price) { return new Trade(id, contract, price, null, null); } // 显式实现接口:仅通过对应状态的接口才能调用 IValidatedTrade ITypedTrade.Validate(string reason) { if (ValidationReason != null) throw new InvalidOperationException("已验证的交易无法重复验证"); return new Trade(Id, Contract, Price, reason, null); } ISubmittedTrade IValidatedTrade.SubmitToLedger(AccountingInformation info) { if (ValidationReason == null) throw new InvalidOperationException("未验证的交易无法提交"); if (AccountingInfo != null) throw new InvalidOperationException("已提交的交易无法重复提交"); return new Trade(Id, Contract, Price, ValidationReason, info); } }
优缺点
- 优势:编译期就能约束操作——比如拿到
ITypedTrade实例的调用方,只能调用Validate,无法直接调用SubmitToLedger;不可变聚合线程安全,状态变更轨迹清晰。 - 不足:聚合类需要实现所有状态接口,若状态较多会增加代码量;仍需少量状态判断(防止非法状态转换),但范围被严格限制。
方案3:事件溯源(Event Sourcing)——事件驱动状态转换
如果你的系统已经采用领域驱动设计的事件溯源架构,那么可以用事件来驱动状态转换:每个状态变更对应一个领域事件,聚合的当前状态由重放历史事件生成,状态合法性由事件的顺序和处理逻辑保证。
代码示例
// 领域事件:记录状态变更的动作与数据 public record TradeCreated(Guid Id, Contract Contract, decimal Price); public record TradeValidated(Guid Id, string Reason); public record TradeSubmitted(Guid Id, AccountingInformation Info); public class Trade { public Guid Id { get; private set; } public Contract Contract { get; private set; } public decimal Price { get; private set; } public string ValidationReason { get; private set; } public AccountingInformation AccountingInfo { get; private set; } private readonly List<object> _uncommittedEvents = new(); // 私有构造函数:仅允许通过工厂方法或事件重放创建 private Trade() { } // 工厂方法:创建初始状态的交易 public static Trade Create(Guid id, Contract contract, decimal price) { var trade = new Trade(); trade.Apply(new TradeCreated(id, contract, price)); return trade; } // 验证操作:生成TradeValidated事件 public void Validate(string reason) { if (ValidationReason != null) throw new InvalidOperationException("已验证的交易无法重复验证"); Apply(new TradeValidated(Id, reason)); } // 提交操作:生成TradeSubmitted事件 public void SubmitToLedger(AccountingInformation info) { if (ValidationReason == null) throw new InvalidOperationException("未验证的交易无法提交"); if (AccountingInfo != null) throw new InvalidOperationException("已提交的交易无法重复提交"); Apply(new TradeSubmitted(Id, info)); } // 事件处理:更新聚合状态 private void Apply(TradeCreated @event) { Id = @event.Id; Contract = @event.Contract; Price = @event.Price; _uncommittedEvents.Add(@event); } private void Apply(TradeValidated @event) { ValidationReason = @event.Reason; _uncommittedEvents.Add(@event); } private void Apply(TradeSubmitted @event) { AccountingInfo = @event.Info; _uncommittedEvents.Add(@event); } // 事件溯源相关方法:获取未提交事件用于持久化 public IEnumerable<object> GetUncommittedEvents() => _uncommittedEvents; public void ClearUncommittedEvents() => _uncommittedEvents.Clear(); }
优缺点
- 优势:状态转换完全由事件驱动,聚合历史可追溯;状态合法性由事件顺序保证,逻辑更清晰;天然支持回溯和审计需求。
- 不足:需要引入事件溯源的基础设施(事件存储、事件重放机制),适合已经采用DDD事件溯源的系统。
方案选择建议
- 若追求编译期类型安全,尽量避免运行时异常,优先考虑「不可变聚合+状态专属接口」方案;
- 若希望代码职责清晰、易于扩展状态,状态模式是经典且可靠的选择;
- 若系统已经采用事件溯源架构,事件驱动的状态转换会和现有体系天然契合,还能保留完整的状态变更历史。
内容的提问来源于stack exchange,提问作者Fabio Marreco
相关产品推荐
相关产品推荐

