下面是订单(Order)聚合的代码示例:
public class Order : IAggregateRoot, IEventPublisher { public static class States { public const int Created = 0; public const int Booked = 1; public const int Rejected = 2; public const int Confirmed = 3; } private List<IEvent> events = new List<IEvent>(); ... public Guid Id { get; private set; } public Guid UserId { get; private set; } public Guid ConferenceId { get; private set; } public virtual ObservableCollection<TicketOrderLine> Lines { get; private set; } public int State { get; private set; } public IEnumerable<IEvent> Events { get { return this.events; } } public void MarkAsBooked() { if (this.State != States.Created) throw new InvalidOperationException(); this.State = States.Booked; } public void Reject() { if (this.State != States.Created) throw new InvalidOperationException(); this.State = States.Rejected; } }注意类的属性没有全被标记为virtual。在这个类的原始版本中,属性Id、UserId、ConferenceId和State都被标记为virtual。下面是两个开发人员之间的讨论:
开发人员1:我确信你不应该使属性都成为虚拟的,除非对象关系映射(ORM)层需要。如果只是出于测试目的,实体和聚合根永远不能用mock测试。如果你需要mock来测试实体和聚合根,那么很明显,设计中有问题。
开发人员2:在默认情况下,我更喜欢开放和可扩展性。你永远不知道将来会出现什么需求,把属性标记为virtual并不费什么事。这当然是有争议的,在.net中有点不标准。这样吧,我们可能只需要给延迟加载的集合属性标记为virtual。
开发人员1:使用CQRS模式通常会使延迟加载的效果消失,所以你也不应该需要它。这样会让代码更简单。
开发人员2:CQRS并没有说要使用事件源(Event Sourcing),但如果使用包含对象的聚合根,无论如何都需要它,对吗?
开发人员1:这不是关于Event Sourcing的,而是关于DDD的。当聚合边界正确时,你就不需要延迟加载。
开发人员2:需要明确的是,聚合边界在这里是为了将应该一起更改的内容分组,以保持一致性。延迟加载就意味着已经分组在一起的东西其实并不需要分组。
开发人员1:我同意。我发现在命令端延迟加载意味着建模错误。如果我不需要命令端的值,那么它就不应该在那里。此外,我不喜欢virtual,除非它们有特定的用途(或者对象关系映射(ORM)工具的需求)。在我看来,这违反了开闭原则:你以各种可能有意也可能无意的方式敞开了自己接受修改的大门,而且即使发生了什么影响,也可能无法立即发现。
译者注:ORM要求属性必须为虚,Java里著名的Hibernate就是这么搞得,所以NHibernate也是这样的。
开发人员2:模型中的订单聚合有一个订单项列表。确定我们不需要加载就能把它标记为已订好的吗?我们建立的模型有问题吗?
开发人员1:OrderItems列表很长吗?如果是,那么建模可能是错误的,因为你并不一定需要那个级别的事务。通常,较晚的来回获取和更新OrderItems的成本可能比预先加载它们要高,你应该评估列表的通常大小,并进行一些性能度量。首先让它变得简单,其次如果需要的话进行优化。
-感谢Jeremie Chassaing和Craig Wilson
聚合和流程管理器下图展示了写模型(Write Model)中存在的对象。有两个聚合,Order和SeatsAvailability,每个都包含多个实体类型。此外,还有一个RegistrationProcessManager类来管理聚合之间的交互。
下图中的表展示了流程管理器在给定当前状态和特定类型消息时的行为。
注册会议的过程从UI发送RegisterToConference命令开始。基础设施将此命令传递给订单(Order)聚合。这个命令的结果是:系统创建了一个新的订单(Order)聚合实例,并且这个新实例引发了一个OrderOrdered事件。订单(Order)聚合类中的构造函数中的以下代码示例展示了这种情况。请注意系统如何使用Guid来标识不同的实体。
public Order(Guid id, Guid userId, Guid conferenceId, IEnumerable<OrderItem> lines) { this.Id = id; this.UserId = userId; this.ConferenceId = conferenceId; this.Lines = new ObservableCollection<OrderItem>(items); this.events.Add( new OrderPlaced { OrderId = this.Id, ConferenceId = this.ConferenceId, UserId = this.UserId, Seats = this.Lines.Select(x => new OrderPlaced.Seat { SeatTypeId = x.SeatTypeId, Quantity = x.Quantity }).ToArray() }); }备注:要查看基础设施组件如何传递命令和事件,在后面的图里有。