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

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

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

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

二、问题来了

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

三、问题分析

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

四、说干就干  

实现类:

    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. Socket聊天程序——服务端

    写在前面: 昨天在博客记录自己抽空写的一个Socket聊天程序的初始设计,那是这个程序的整体设计,为了完整性,今天把服务端的设计细化记录一下,首页贴出Socket聊天程序的服务端大体设计图,如下图: ...

  2. ExtJS 4.2 介绍

    本篇介绍ExtJS相关知识,是以ExtJS4.2.1版本为基础进行说明,包括:ExtJS的特点.MVC模式.4.2.1GPL版本资源的下载和说明以及4种主题的演示. 目录 1. 介绍 1.1 说明 1 ...

  3. 谈谈一些有趣的CSS题目(一)-- 左边竖条的实现方法

    开本系列,讨论一些有趣的 CSS 题目,抛开实用性而言,一些题目为了拓宽一下解决问题的思路,此外,涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题中有你感觉 ...

  4. PHP中遍历XML之SimpleXML

    简单来讲述一些XML吧,XML是可扩展标记语言,是一种用于标记电子文件使其具有结构性的标记语言.XML是当今用于传输数据的两大工具之一,另外一个是json. 我们在PHP中使用XML也是用来传输数据, ...

  5. JVM类加载

    JVM的类加载机制就是:JVM把描述类的class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被JVM直接使用的Java类型 ClassLoader JVM中的ClassLoade ...

  6. CSS中强悍的相对单位之em(em-and-elastic-layouts)学习小记

    使用相对单位em注意点 1.浏览器默认字体是16px,即1em = 16px,根元素设置如下 html{ font-size: 100%; /* WinIE text resize correctio ...

  7. javaScript中的小细节-script标签中的预解析

    首先介绍预解析,虽然预解析字面意思很好理解,但是却是出坑出的最多的地方,也是bug经常会有的地方,利用好预解析的特性可以解决很多问题,并且提高代码的质量及数量,浏览器在解析代码前会把变量的声明和函数( ...

  8. Win7安装MySQL-5.7.16过程

    1.在C盘新建MYSQL文件夹:2.将mysql-5.7.16-winx64拷贝到C:\MYSQL文件夹下,更名为mysql-5.7.16:3.在mysql-5.7.16目录下,建my.ini文件,内 ...

  9. JavaScript

    2015-08-01 16:20 JavaScript使用时需要注意的地方 1.引入JS的位置:最好的做法是把<script>的标签放到HTML文档的最后.</body>标签之 ...

  10. grunt配置任务

    这个指南解释了如何使用 Gruntfile 来为你的项目配置task.如果你还不知道 Gruntfile 是什么,请先阅读 快速入门 指南并看看这个Gruntfile 实例. Grunt配置 Grun ...