欢迎光临汤雪华的博客

一个人一辈子能坚持做好一件事情就够了!坚持是一种刻意的练习,不断寻找缺点突破缺点的过程,而不是重复做某件事情。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

基于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聚合根了。这里实际上就是我说的上面的第三个要素发生了结构性变化,导致我们无法通过事件溯源重建对象

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