最近在开发过程中,遇到了一个场景,甚是棘手,在这里分享一下。希望大家脑洞大开一起来想一下解决思路。鄙人也想了一个方案拿出来和大家一起探讨一下是否合理。

一、简单介绍一下涉及的对象概念

  工作单元:维护变化的对象列表,在整块业务逻辑处理完全之后一次性写入到数据库中。

  领域事件:领域对象本身发生某些变化时,发布的通知事件,告诉订阅者处理相关流程。

二、问题来了

  我认为最合理的领域事件的触发点应该设计在领域对象内部,那么问题来了。当这个领域对象发生变化的上下文是一个复杂的业务场景,整个流程中会涉及到多个领域对象,所以需要通过工作单元来保证数据写入的一致性。此时其中各个产生变化的领域对象的领域事件如果实时被发布出去,那么当工作单元在最终提交到数据库时,如果产生了回滚,那么会导致发布了错误的领域事件,产生未知的后果。

三、问题分析

  我能够想到的方案是,这里领域事件的发布也通过一个类似于工作单元一样的概念进行持续的管理,在领域对象中的发布只是做一个记录,只有在工作单元提交成功之后,才实际发布其中所有的领域事件。

四、说干就干  

实现类:

    public class DomainEventConsistentQueue : IDisposable
{
private readonly List<IDomainEvent> _domainEvents = new List<IDomainEvent>();
private bool _publishing = false; public void RegisterEvent(IDomainEvent domainEvent)
{
if (_publishing)
{
throw new ApplicationException("当前事件一致性队列已被发布,无法添加新的事件!");
} if (_domainEvents.Any(ent => ent == domainEvent)) //防止相同事件被重复添加
return; _domainEvents.Add(domainEvent);
} public void Clear()
{
_domainEvents.Clear();
_publishing = false;
} public void PublishEvents()
{
if (_publishing)
{
return;
} if (_domainEvents == null)
return; try
{
_publishing = true;
foreach (var domainEvent in _domainEvents)
{
DomainEventBus.Instance().Publish(domainEvent);
}
}
finally
{
Clear();
}
} public void Dispose()
{
Clear();
}
}

使用方式:

             var aggregateA = new AggregateRootA();
var aggregateB = new AggregateRootB(); using (var queue = new DomainEventConsistentQueue())
{
using (var unitwork = new SqlServerUnitOfWork(GlobalConfig.DBConnectString))
{
8 aggregateA.Event(queue);
9 aggregateB.Event(queue); var isSuccess = unitwork.Commit();
if (isSuccess)
queue.PublishEvents();
}
} public class AggregateRootA : AggregateRoot
{
public void Event(DomainEventConsistentQueue queue)
{
queue.RegisterEvent(new DomainEventA());
}
} public class AggregateRootB : AggregateRoot
{
public void Event(DomainEventConsistentQueue queue)
{
queue.RegisterEvent(new DomainEventB());
}
} public class DomainEventA : IDomainEvent
{
public DateTime OccurredOn()
{
throw new NotImplementedException();
} public void Read()
{
throw new NotImplementedException();
} public bool IsRead
{
get { throw new NotImplementedException(); }
}
} public class DomainEventB : IDomainEvent
{
public DateTime OccurredOn()
{
throw new NotImplementedException();
} public void Read()
{
throw new NotImplementedException();
} public bool IsRead
{
get { throw new NotImplementedException(); }
}
}

问题是解决了,但是标红的这段代码看着特别变扭,在产生领域事件的领域对象方法上需要增加一个与表达的业务无关的参数,这个大大破坏了DDD设计的初衷——统一语言(Ubiquitous Language),简洁明了的表达出每个业务行为,业务交流应与代码保持一致。像这2行表达起来如“AggregateRootA Event DomainEventConsistentQueue”这个 DomainEventConsistentQueue其实并不是领域对象,所以其并不是领域的一部分。

五、陷入思考

  这里突然想到,如果在运行中的每个线程的共享区域存储待发布的领域事件集合,那么不就可以随时随地的管理当前操作上下文中的领域事件了吗?这里需要引入ThreadLocal<T> 类。MSDN的解释参见https://msdn.microsoft.com/zh-cn/library/dd642243(v=vs.110).aspx。该泛型类可以提供仅针对当前线程的全局存储空间,正好能够恰到好处的解决我们现在遇到的问题。

六、说改就改

实现类:

    public class DomainEventConsistentQueue : IDisposable
{
private static readonly ThreadLocal<List<IDomainEvent>> _domainEvents = new ThreadLocal<List<IDomainEvent>>();
private static readonly ThreadLocal<bool> _publishing = new ThreadLocal<bool> { Value = false }; private static DomainEventConsistentQueue _current;
/// <summary>
/// 获取当前的领域事件一致性队列。
/// 由于使用了线程本地存储变量,此处为单例模式。
/// </summary>
/// <returns></returns>
public static DomainEventConsistentQueue Current()
{
if (_current != null)
return _current;
var temp = new DomainEventConsistentQueue();
Interlocked.CompareExchange(ref _current, temp, null);
return temp;
} public void RegisterEvent(IDomainEvent domainEvent)
{
if (_publishing.Value)
{
throw new ApplicationException("当前事件一致性队列已被发布,无法添加新的事件!");
} var domainEvents = _domainEvents.Value;
if (domainEvents == null)
{
domainEvents = new List<IDomainEvent>();
_domainEvents.Value = domainEvents;
} if (domainEvents.Any(ent => ent == domainEvent)) //防止相同事件被重复添加
return; domainEvents.Add(domainEvent);
} public void Clear()
{
_domainEvents.Value = null;
_publishing.Value = false;
} public void PublishEvents()
{
if (_publishing.Value)
{
return;
} if (_domainEvents.Value == null)
return; try
{
_publishing.Value = true;
foreach (var domainEvent in _domainEvents.Value)
{
DomainEventBus.Instance().Publish(domainEvent);
}
}
finally
{
Clear();
}
} public void Dispose()
{
Clear();
}
}

使用方式:

             var aggregateA = new AggregateRootA();
var aggregateB = new AggregateRootB(); using (var queue = DomainEventConsistentQueue.Current())
{
using (var unitwork = new SqlServerUnitOfWork(GlobalConfig.DBConnectString))
{
aggregateA.Event();
aggregateB.Event(); var isSuccess = unitwork.Commit();
if (isSuccess)
queue.PublishEvents();
}
} public class AggregateRootA : AggregateRoot
{
public void Event()
{
DomainEventConsistentQueue.Current().RegisterEvent(new DomainEventA());
}
} public class AggregateRootB : AggregateRoot
{
public void Event()
{
DomainEventConsistentQueue.Current().RegisterEvent(new DomainEventB());
}
} public class DomainEventA : IDomainEvent
{
public DateTime OccurredOn()
{
throw new NotImplementedException();
} public void Read()
{
throw new NotImplementedException();
} public bool IsRead
{
get { throw new NotImplementedException(); }
}
} public class DomainEventB : IDomainEvent
{
public DateTime OccurredOn()
{
throw new NotImplementedException();
} public void Read()
{
throw new NotImplementedException();
} public bool IsRead
{
get { throw new NotImplementedException(); }
}
}

这样代码看起来比之前优雅多了。这里的 DomainEventConsistentQueue.Current() 中操作的变量针对同一个线程在哪都是共享的,所以我们只管往里丢数据就好了~

七、方案的局限性。

  对于执行上下文的要求较高,整个领域事件的发布必须要求在同一线程内操作。所以在使用的过程中尽量避免这种情况的发生。如果实在无法避免只能通过把DomainEventConsistentQueue 当作变量在多个线程之间传递了。

以上是个人的想法,可能有所考虑不周~ 不知道各位园子里的小伙伴们是否有处理过类似场景的经验,欢迎留言探讨,相互学习~

  

作者:  Zachary
出处:https://zacharyfan.com/archives/61.html

▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

DDD设计中的Unitwork与DomainEvent如何相容?的更多相关文章

  1. DDD中的Unitwork与DomainEvent如何相容?(续)

    上篇中说到了面临的问题(传送门:DDD设计中的Unitwork与DomainEvent如何相容?),和当时实现的一个解决方案.在实际使用了几天后,有了新的思路,和@trunks 兄提出的观点类似.下面 ...

  2. 如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展

    阅读目录 前言 回顾 本地的一致性 领域事件发布出现异常 订阅者处理出现异常 结语 一.前言 上篇中我们初步运用了领域事件,其中还有一些问题我们没有解决,所以实现是不健壮的,下面先来回顾一下. 二.回 ...

  3. 如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单

    阅读目录 前言 解决数据一致性的方案 回到DDD 设计 实现 结语 一.前言 之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍.现在来到了这最后一个环节,提交订单.单从业务上看,这个 ...

  4. 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑

    阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...

  5. 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成

    阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...

  6. 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车

     阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...

  7. 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文

    阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...

  8. 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

    阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...

  9. 如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发

    阅读目录 前言 单元测试 纠正错误,重新出发 结语 一.前言 实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发 ...

随机推荐

  1. 01.SQLServer性能优化之----强大的文件组----分盘存储

    汇总篇:http://www.cnblogs.com/dunitian/p/4822808.html#tsql 文章内容皆自己的理解,如有不足之处欢迎指正~谢谢 前天有学弟问逆天:“逆天,有没有一种方 ...

  2. TODO:Laravel 内置简单登录

    TODO:Laravel 内置简单登录 1. 激活Laravel的Auth系统Laravel 利用 PHP 的新特性 trait 内置了非常完善好用的简单用户登录注册功能,适合一些不需要复杂用户权限管 ...

  3. 如何利用pt-online-schema-change进行MySQL表的主键变更

    业务运行一段时间,发现原来的主键设置并不合理,这个时候,想变更主键.这种需求在实际生产中还是蛮多的. 下面,看看pt-online-schema-change解决这类问题的处理方式. 首先,创建一张测 ...

  4. 关于.NET参数传递方式的思考

    年关将近,整个人已经没有了工作和写作的激情,估计这个时候很多人跟我差不多,该相亲的相亲,该聚会喝酒的聚会喝酒,总之就是没有了干活的心思(我有很多想法,但就是叫不动我的手脚,所以我只能看着别人在做我想做 ...

  5. C++随笔:.NET CoreCLR之GC探索(2)

    首先谢谢 @dudu 和 @张善友 这2位大神能订阅我,本来在写这个系列以前,我一直对写一些核心而且底层的知识持怀疑态度,我为什么持怀疑态度呢?因为一般写高层语言的人99%都不会碰底层,其实说句实话, ...

  6. CSS知识总结(八)

    CSS常用样式 8.变形样式 改变元素的大小,透明,旋转角度,扭曲度等. transform : none | <transform-function> <transform-fun ...

  7. 【微信小程序开发】之如何获取免费ssl证书【图文步骤】

    微信小程序要求所有网络请求都走ssl加密,因此我们开发服务端接口需要配置为https 这篇文章介绍一下如何 在 startssl 申请一个免费的ca证书. 1. 打开网站  https://www.s ...

  8. 听H3絮叨:何以让天下没有难用的流程

    最近朋友圈.网站新闻铺天盖地是"让天下没有难用的流程",有人就要问了,H3 BPM何德何能,为BPM站台,让天下没有难用的流程? 这是一个关于"办公室空想"的故 ...

  9. jQuery标准的AJAX模板

    $('#saveInformationTemplate_button').on('click', function(){ if(isEmpty($("#name").val())) ...

  10. On cloud, be cloud native

    本来不想起一个英文名,但是想来想去都没能想出一个简洁地表述该意思的中文释义,所以就用了一个英文名称,望见谅. Cloud Native是一个刚刚由VMware所提出一年左右的名词.其表示在设计并实现一 ...