ASP.NET Core Web API下事件驱动型架构的实现(四):CQRS架构中聚合与聚合根的实现 (3)

image

其中,实体(IEntity)、聚合根(IAggregateRoot)都是大家耳熟能详的领域驱动设计的概念。由于实体都是通过Id进行唯一标识,所以,IEntity会有一个id的属性,为了简单起见,我们使用Guid作为它的类型。聚合根(IAggregateRoot)继承于IEntity接口,有趣的是,在我们目前的场景中,IAggregateRoot并不包含任何成员,它仅仅是一个空接口,在整个框架代码中,它仅作为泛型的类型约束。Note:这种做法其实也是非常常见的一种框架设计模式。具有事件溯源能力的聚合根(IAggregateRootWithEventSourcing)又继承于IAggregateRoot接口,并且有如下三个成员:

uncommittedEvents:用于缓存发生在当前聚合中的领域事件

version:表示当前聚合的版本号

Replay:将指定的一系列领域事件“应用”到当前的聚合上,也就是所谓的事件回放

此外,你还发现我们还有两个神奇的接口:IPurgable和IPersistedVersionSetter。这两个接口的职责是:

IPurgable表示,实现了该接口的类型具有某种清空操作,比如清空某个队列,或者将对象状态恢复到初始状态。让IAggregateRootWithEventSourcing继承于该接口是因为,当仓储完成了聚合中领域事件的保存和派发之后,需要清空聚合中缓存的事件,以保证在今后,发生在同一时间点的同样的事件不会被再次保存和派发

IPersistedVersionSetter接口允许调用者对聚合的“保存版本号”进行设置。这个版本号表示了在事件存储中,属于当前聚合的所有事件的个数。试想,如果一个聚合的“保存版本号”为4(即在事件存储中有4个事件是属于该聚合的),那么,如果再有2个事件发生在这个聚合中,于是,该聚合的版本就是4+2=6.

Note:为什么不将这两个接口中的方法直接放在IAggregateRootWithEventSourcing中呢?是因为单一职责原则。聚合本身不应该存在所谓之“清空缓存”或者“设置保存版本号”这样的概念,这样的概念对于技术人员来说比较容易理解,可是如果将这些技术细节加入领域模型中,就会污染领域模型,造成领域专家无法理解领域模型,这是违背面向对象分析与设计的单一职责原则的,也违背了领域驱动设计的原则。那么,即使把这些方法通过额外的接口独立出去,实现了IAggregateRootWithEventSourcing接口的类型,不还是要实现这两个接口中的方法吗?这样,聚合的访问者不还是可以访问这两个额外的方法吗?的确如此,这些接口是需要被实现的,但是我们可以使用C#中接口的显式实现,这样的话,如果不将IAggregateRootWithEventSourcing强制转换成IPurgable或者IPersistedVersionSetter的话,是无法直接通过聚合根对象本身来访问这些方法的,这起到了非常好的保护作用。接口的显式实现在软件系统的框架设计中也是常用手段。

抽象类AggregateRootWithEventSourcing的实现

在上面的类图中,IAggregateRootWithEventSourcing最终由AggregateRootWithEventSourcing抽象类实现。不要抱怨类的名字太长,它有助于我们理解这一类型在我们的领域模型中的角色和功能。下面的代码列出了该抽象类的主要部分的实现:

public abstract class AggregateRootWithEventSourcing : IAggregateRootWithEventSourcing { private readonly Lazy<Dictionary<string, MethodInfo>> registeredHandlers; private readonly Queue<IDomainEvent> uncommittedEvents = new Queue<IDomainEvent>(); private Guid id; private long persistedVersion = 0; private object sync = new object(); protected AggregateRootWithEventSourcing() : this(Guid.NewGuid()) { } protected AggregateRootWithEventSourcing(Guid id) { registeredHandlers = new Lazy<Dictionary<string, MethodInfo>>(() => { var registry = new Dictionary<string, MethodInfo>(); var methodInfoList = from mi in this.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) let returnType = mi.ReturnType let parameters = mi.GetParameters() where mi.IsDefined(typeof(HandlesInlineAttribute), false) && returnType == typeof(void) && parameters.Length == 1 && typeof(IDomainEvent).IsAssignableFrom(parameters[0].ParameterType) select new { EventName = parameters[0].ParameterType.FullName, MethodInfo = mi }; foreach (var methodInfo in methodInfoList) { registry.Add(methodInfo.EventName, methodInfo.MethodInfo); } return registry; }); Raise(new AggregateCreatedEvent(id)); } public Guid Id => id; long IPersistedVersionSetter.PersistedVersion { set => Interlocked.Exchange(ref this.persistedVersion, value); } public IEnumerable<IDomainEvent> UncommittedEvents => uncommittedEvents; public long Version => this.uncommittedEvents.Count + this.persistedVersion; void IPurgable.Purge() { lock (sync) { uncommittedEvents.Clear(); } } public void Replay(IEnumerable<IDomainEvent> events) { ((IPurgable)this).Purge(); events.OrderBy(e => e.Timestamp) .ToList() .ForEach(e => { HandleEvent(e); Interlocked.Increment(ref this.persistedVersion); }); } [HandlesInline] protected void OnAggregateCreated(AggregateCreatedEvent @event) { this.id = @event.NewId; } protected void Raise<TDomainEvent>(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent { lock (sync) { // 首先处理事件数据。 this.HandleEvent(domainEvent); // 然后设置事件的元数据,包括当前事件所对应的聚合根类型以及 // 聚合的ID值。 domainEvent.AggregateRootId = this.id; domainEvent.AggregateRootType = this.GetType().AssemblyQualifiedName; domainEvent.Sequence = this.Version + 1; // 最后将事件缓存在“未提交事件”列表中。 this.uncommittedEvents.Enqueue(domainEvent); } } private void HandleEvent<TDomainEvent>(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent { var key = domainEvent.GetType().FullName; if (registeredHandlers.Value.ContainsKey(key)) { registeredHandlers.Value[key].Invoke(this, new object[] { domainEvent }); } } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsywf.html