DDD设计中的Unitwork与DomainEvent如何相容?
最近在开发过程中,遇到了一个场景,甚是棘手,在这里分享一下。希望大家脑洞大开一起来想一下解决思路。鄙人也想了一个方案拿出来和大家一起探讨一下是否合理。
一、简单介绍一下涉及的对象概念
工作单元:维护变化的对象列表,在整块业务逻辑处理完全之后一次性写入到数据库中。
领域事件:领域对象本身发生某些变化时,发布的通知事件,告诉订阅者处理相关流程。
二、问题来了
我认为最合理的领域事件的触发点应该设计在领域对象内部,那么问题来了。当这个领域对象发生变化的上下文是一个复杂的业务场景,整个流程中会涉及到多个领域对象,所以需要通过工作单元来保证数据写入的一致性。此时其中各个产生变化的领域对象的领域事件如果实时被发布出去,那么当工作单元在最终提交到数据库时,如果产生了回滚,那么会导致发布了错误的领域事件,产生未知的后果。
三、问题分析
我能够想到的方案是,这里领域事件的发布也通过一个类似于工作单元一样的概念进行持续的管理,在领域对象中的发布只是做一个记录,只有在工作单元提交成功之后,才实际发布其中所有的领域事件。
四、说干就干
实现类:
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如何相容?的更多相关文章
- DDD中的Unitwork与DomainEvent如何相容?(续)
上篇中说到了面临的问题(传送门:DDD设计中的Unitwork与DomainEvent如何相容?),和当时实现的一个解决方案.在实际使用了几天后,有了新的思路,和@trunks 兄提出的观点类似.下面 ...
- 如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展
阅读目录 前言 回顾 本地的一致性 领域事件发布出现异常 订阅者处理出现异常 结语 一.前言 上篇中我们初步运用了领域事件,其中还有一些问题我们没有解决,所以实现是不健壮的,下面先来回顾一下. 二.回 ...
- 如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单
阅读目录 前言 解决数据一致性的方案 回到DDD 设计 实现 结语 一.前言 之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍.现在来到了这最后一个环节,提交订单.单从业务上看,这个 ...
- 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...
- 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...
- 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...
- 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...
- 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...
- 如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发
阅读目录 前言 单元测试 纠正错误,重新出发 结语 一.前言 实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发 ...
随机推荐
- 【.net 深呼吸】细说CodeDom(8):分支与循环
有人会问,为啥 CodeDom 不会生成 switch 语句,为啥没生成 while 语句之类.要注意,CodeDom只关心代码逻辑,而不是语法,语法是给写代码的人用的.如果用.net的“反编译”工具 ...
- 【探索】无形验证码 —— PoW 算力验证
先来思考一个问题:如何写一个能消耗对方时间的程序? 消耗时间还不简单,休眠一下就可以了: Sleep(1000) 这确实消耗了时间,但并没有消耗 CPU.如果对方开了变速齿轮,这瞬间就能完成. 不过要 ...
- 猫哥网络编程系列:HTTP PEM 万能调试法
注:本文内容较长且细节较多,建议先收藏再阅读,原文将在 Github 上维护与更新. 在 HTTP 接口开发与调试过程中,我们经常遇到以下类似的问题: 为什么本地环境接口可以调用成功,但放到手机上就跑 ...
- pt-table-checksum
pt-table-checksum是percona公司提供的一个用于在线比对主从数据一致性的工具. 实现原理 将一张大表分成多个chunk,每次针对一个chunk进行校验,同时将校验的结果通过REPL ...
- 【解决方案】Myeclipse 10 安装 GIT 插件 集成 步骤 图解
工程开发中,往往要使用到集成GIT ,那么下面说说插件安装步骤 PS:以Myeclipse 10 为例,讲解集成安装步骤. ----------------------main------------ ...
- Android注解使用之注解编译android-apt如何切换到annotationProcessor
前言: 自从EventBus 3.x发布之后其通过注解预编译的方式解决了之前通过反射机制所引起的性能效率问题,其中注解预编译所采用的的就是android-apt的方式,不过最近Apt工具的作者宣布了不 ...
- c# 基础 object ,new操作符,类型转换
参考页面: http://www.yuanjiaocheng.net/webapi/config-webapi.html http://www.yuanjiaocheng.net/webapi/web ...
- Kooboo CMS技术文档之四:Kooboo CMS的站点组成部分
Kooboo CMS本着功能独立分离的原则,将站点分为三部分组成:用户管理,站点管理和内容数据库管理.各个功能之间既可独立使用,也可以容易组成在一起形成一个完整的系统. 用户管理 管理整个系统内的用户 ...
- maven打包插件:appassembler
1.打包成bat 打包命令:mvn clean package appassembler:assemble <plugin> <groupId>org.codehaus.moj ...
- 参数探测(Parameter Sniffing)影响存储过程执行效率解决方案
如果SQL query中有参数,SQL Server 会创建一个参数嗅探进程以提高执行性能.该计划通常是最好的并被保存以重复利用.只是偶尔,不会选择最优的执行计划而影响执行效率. SQL Server ...