关于基于Event Sourcing模式实现的领域模型如何处理模型重构的问题的思考

简介:

基于DDD+Event Sourcing设计的模型如何处理模型重构?

问题背景:ddd的核心是聚合,一个聚合内包含一些实体,其中一个是根实体,这个大家都有共识;另外,如果将DDD与Event Sourcing结合,那就是一个聚合根会产生一些event;那么这里的问题是:如果一个领域对象,一开始是entity,后来升级为聚合根,但是该entity之前根本没有对应的event,因为它不是聚合根。因此它升级后我们如何通过event sourcing获取升级后的聚合根最新状态;同理,相反的例子是聚合根降级为实体,该如何处理。

基于哲学方面的一些思考:
之前ORM时代,数据就是数据,我们直接存储数据,然后读取存储的数据即可,很简单;
现在Event Sourcing了,数据用事件表示,我们不在存储数据本身,而是存储与该数据相关的所有事件,包括数据被创建的事件在内;这种思维是好的,我们希望通过保存数据的“完整的历史”来达到任意时刻都能还原数据的目标。但是我们仅仅保存event就真的保存了“完整的历史”了吗?显然不是,我认为历史包含两部分信息:1)事件;2)逻辑;目前我们只保存事件而没有保存逻辑;但是我们又要希望通过事件溯源还原“完整的历史”,怎么可能?!
但是,我们为了确保能还原数据,所以代码重构都小心翼翼,比如确保尽量不改原来的事件,尽量用新事件实现业务变化或新业务功能。另外,对于处理事件的逻辑也尽量确保能兼容老的事件。之所以要这么别扭是因为我们没办法把历史的事件和历史的事件处理逻辑一同持久化。实际上我们总是在用老的事件与最新的代码逻辑相结合进行重演,这实际上是很危险的事情。

然后碰到我上面提出的尖锐问题,实际上很难有优雅的解决方案了。上面我提出的问题其实很难解决:无论是聚合根升级还是降级,都意味着新对象的事件我们无法获取或者说根本之前没有任何与新对象相关的事件,自然就无法再用事件溯源的方式得到该对象了。而实际上这个对象什么都没做,只是做了个升级或降级处理而已;

那么问题出在哪里呢?我认为是DDD的聚合导致的问题。我们之所以要设计出聚合,主要原因是为了通过聚合的手段确保业务上具有内聚关系具有数据一致性规则(Invariants)的领域对象之间方便的维护其一致性;而事件溯源从概念上来说并不针对整个aggregate,而是针对单个的entity.现在一旦将DDD与event sourcing结合,那势必会导致模型中一些对象没有与其相关的event,这就会给我们后期模型重构带来巨大的问题。

既然问题找到了,那我想解决方案也很容易了。就是如果要用event soucing,就必须抛弃聚合的概念,让一切对象回归平等,所有的entity都相互平等,当然value object还是保持不变,因为其只是一个值而已;然后让每个entity都能产生事件,这样就不会有因为某些entity没有事件而导致重构时遇到巨大问题的情况了。

自此,也许你会说,没有聚合那不就是贫血模型了吗?我不这么认为!聚合的意义有两个:1)更好的表达业务完整概念,因为有些对象却是在概念上就是内聚其他一些对象的,比如一辆汽车有四个轮子,汽车内聚轮子;2)为了维护对象之间的Invariants,这个不多解释了,我想大家都理解;那我认为第一点其实和功能无关,是概念上好理解才这样做;关于第二点维护对象之间的Invariants,我认为有很多方法,不必必须显式的定义聚合来实现,我们只要确保所有的entity都能很好的规定其自身哪些属性必须有,哪些属性不能变,哪些可以变,哪些可以在什么范围内变,等等规则约束。这样也同样能实现不变性约束;实际上这种方式和DDD看起来非常接近,但是绝不是贫血模型,因为贫血模型是所有entity的所有属性当然id除外都有get;set;然后所有逻辑全部在service中以transaction script的方式实现;而我上面说的方式实际上entity该有的职责和业务规则判断还是放在entity内部做掉,但是和经典DDD相比,经典DDD的大部分规则和一致性逻辑都在聚合根内完成,而我的方式则由各个entity合起来实现相同的规则和一致性约束;

到这里,其实event sourcing还是面临小范围(单个entity内部)的代码重构的压力,但这我们总能找到相对成本比较轻的解决方案,比如尽量不改原来事件,只新增事件属性,不删除事件属性。即总是采用与原事件兼容的修改方式来修改事件,这其实是可以接受的。


大家觉得怎么样呢?很希望能多听听大家的想法。
--------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------

为了能更好的说明问题,我写了个简单的小例子。下面有对这个例子的详细描述,以及基于该例子的问题描述;

复制代码
// 团队聚合根
public  class Team : EntityBase< int>, IAggregateRoot
{
     private IList<Member> _members =  new List<Member>();

     public IEnumerable<Member> Members {  get {  return _members; } }

     public  void AddMember( string name,  string email)
    {
        ApplyEvent( new MemberAdded(name, email,  this.Id));
    }
     public  void UpdateMemberName( int memberId,  string newName)
    {
        ApplyEvent( new MemberNameUpdated(memberId, newName,  this.Id));
    }


     private  void OnMemberAdded(MemberAdded evnt)
    {
        _members.AddMember( new Member(evnt.Name, evnt.Email));
    }
     private  void OnMemberNameUpdated(MemberNameUpdated evnt)
    {
         var member = _members.FindMemberById(evnt.MemberId);
        member.SetName(evnt.NewName);
    }
}
// 团队成员新增事件
public  class MemberAdded
{
     public  string Name {  getprivate  set; }
     public  string Email {  getprivate  set; }
     public  int TeamId {  getprivate  set; }

     public MemberAdded( string name,  string email,  int teamId)
    {
         this.Name = name;
         this.Email = email;
         this.TeamId = teamId;
    }
}
// 团队成员名称修改事件
public  class MemberNameUpdated
{
     public  int MemberId {  getprivate  set; }
     public  string NewName {  getprivate  set; }
     public  int TeamId {  getprivate  set; }

     public OnMemberNameUpdated( int memberId,  string newName,  int teamId)
    {
         this.MemberId = memberId;
         this.NewName = newName;
         this.TeamId = teamId;    
    }
}

// 团队成员实体
public  class Member : EntityBase< int>
{
     public  string Name {  getprivate  set; }
     public  string Email {  getprivate  set; }

     public Member( string name,  string email)
    {
         this.Name = name;
         this.Email = email;
    }

     public  void SetName( string name)
    {
        Assert.IsNotNullOrEmpty(name);
        Assert.LengthLessThen(name,  255);

         this.Name = name;
    }
复制代码

上面的例子中,有一个聚合根,Team,表示一个团队;Team内聚了一些团队成员,Member;Member是实体;
这里聚合根,实体,就是DDD中的Aggregate Root与Entity。这里没问题吧!另外,上面的例子,我采用了Event Sourcing的方式来实现模型。
Event Sourcing的核心思想有两点:
1)用与某个对象相关的事件来记录对象的每一次变化,一次变化一个事件,对象的创建是第一个事件,如TeamCreated事件表示一个团队被创建了;
2)对象的重建不需通过ORM,而是直接使用之前记录的事件进行逐个重演最终得到对象最新状态,这个重演的过程我们称为事件溯源,英文叫Event Sourcing;

不知我上述对Event Sourcing的描述是否和大家的理解一致?

好了,本文提到的关于“历史不仅仅由事件组成,还必须由处理该事件的逻辑组成”。这句话的意思是,事件要进行重演,必须与一定的逻辑结合,事件本质上只是一些数据,
包含了某次变化的相关信息,它不包含逻辑,是静态的值对象;那逻辑是什么呢?主要指两方面:
1)上面Team类里的OnMemberAdded和OnMemberNameUpdated这两个方法,这两个方法实际上是事件的处理函数,职责是负责更新聚合的相关状态;
2)这些事件处理函数在更新聚合状态时实际上是依赖于当前聚合的内部结构的;

所以,事件要能够顺利的按照和历史的方式完全一致的重演,依赖于三个要素必须和历史一致:
1)事件不变;
2)聚合内部的事件处理逻辑不变,或者即便要变也必须和以前的逻辑兼容;
3)事件处理逻辑依赖的聚合的内部结构不变,或者即便要变也必须和以前的结构兼容;

而我们现在做到的只是第一个要素不变,第二和第三个要素我们很可能会进行重构;
当然你可能会说,第二点你也基本不会变,因为你的事件处理逻辑一般都是简单的属性赋值,即简单的更改聚合相关属性的状态,那行,如果你真这样做,那确实问题不大;实际上也必须这样做!
但是第三个要素呢?第三个要素实际上就是我说的模型结构重构,最严重的重构情况则是:聚合根降级为实体,或者实体升级为聚合根,简称聚合根的升级与降级;

对于这两种情况,在应用了Event Sourcing的情况下,那是很可怕的。因为从上面我的代码中可以看出Member起初只是个实体,它没有自己的事件,所有的事件都只和聚合根关联,即Team。
但是我们之后如果想重构,把Member升级为聚合根了,这个重构之前在ORM时代,那时非常简单的事情,基本什么都不必变,但是在Event Sourcing的模式下,就有大问题。
因为我们没有与Member对应的事件,自然就无法应用事件溯源来重建Member聚合根了。这里实际上就是我说的上面的第三个要素发生了结构性变化,导致我们无法通过事件溯源重建对象

看到这里,大家再回过头去看一下我最上面对问题的阐述可能更好理解一点吧!


目录
相关文章
|
7月前
|
运维 Java 数据库
如何实现最终一致性,有哪些解决方案
如何实现最终一致性,有哪些解决方案
|
10月前
|
设计模式 Java
【Java设计模式 规范与重构】 一 重构的目的、内容、时机、方法
【Java设计模式 规范与重构】 一 重构的目的、内容、时机、方法
116 0
|
11月前
|
设计模式 JSON 缓存
如何“好好利用多态”写出又臭又长又难以维护的代码?| Feeds 流重构方案
如何“好好利用多态”写出又臭又长又难以维护的代码?| Feeds 流重构方案
57 0
|
11月前
|
设计模式 架构师 数据建模
「领域驱动设计DDD」事件风暴简介:实现域驱动设计的简便方法
「领域驱动设计DDD」事件风暴简介:实现域驱动设计的简便方法
「领域驱动设计DDD」事件风暴简介:实现域驱动设计的简便方法
|
安全
从想法到设计的过程
在接下来的几节里,我会向你展示游戏制作的整个流程,从开始的一个粗略的想法,到游戏设计,再到最终的游戏制作。
107 0
从想法到设计的过程
|
机器学习/深度学习 缓存 前端开发
基于 Observable 构建前端防腐策略
To B 业务的生命周期与迭代通常会持续多年,随着产品的迭代与演进,以接口调用为核心的前后端关系会变得非常复杂。在多年迭代后,接口的任何一处修改都可能给产品带来难以预计的问题。在这种情况下,构建更稳健的前端应用,保证前端在长期迭代下的稳健与可拓展性就变得非常重要。本文将重点介绍如何利用接口防腐策略避免或减少接口变更对前端的影响。
基于 Observable 构建前端防腐策略
|
缓存 监控 前端开发
消息复杂计算的抽象和简化
本文将介绍客户端消息数据计算的问题与解决方案。
136 0
消息复杂计算的抽象和简化
|
存储 搜索推荐 Java
大中台模式下如何构建复杂业务核心状态机组件
大中台战略下,中台将公司业务的公共能力下沉,并采用更加合理、可复用的架构和技术来实现这些基础能力。在电商行业内,将面临货物的采购、商品上架、交易发生、订单状态变化、客服介入等大量状态维护。
大中台模式下如何构建复杂业务核心状态机组件
|
资源调度 前端开发 安全
5G 逻辑架构的重构 | 带你读《5G时代的承载网》之十一
5G 的架构设计主要需要满足关键性能需求和网络运营需求,为便于理解 5G 架构设计的革新,本节将以现有 4G 的网络架构为基础,逐一分析现有架 构的局限性,对比 5G 新的性能需求和运营需求,对现有架构进行分解、重构, 逐步靠近 3GPP 确定的 5G 新架构。
5G 逻辑架构的重构   | 带你读《5G时代的承载网》之十一
|
云计算 人工智能 数据处理
对话行癫:解密阿里云顶层设计和底层逻辑
几十个问题,万字长文,阿里云新任总裁行癫履新后首次深入讨论阿里云对云计算未来的判断,深度解读未来阿里云生态战略,揭秘阿里技术委员会和阿里中台思想的原生思考。
2291 0