领域驱动设计(DDD)的实践经验分享之持久化透明

简介:

前一篇文章中,我谈到了领域驱动设计中,关于ORM工具该如何使用的问题。谈了很多我心里的想法,大家也对我的观点做了一些回复,或多或少让我深深感觉到面向对象设计和领域驱动设计是两个不同层次的东西。你会面向对象并不代表你就会面向领域设计。后来,我无意中发现了一个网站,http://www.jdon.com,这个网站中所包含的知识在我看来非常深入,而且基本上都包含了现在一些最新的设计思想。我看了几篇文章后渐渐感觉到领域驱动设计并不是我想象中那么简单。其实学技术,学框架并不是太难,只要你肯花时间就一定能慢慢领悟。但要学会领域建模,我现在觉得非一朝一夕就能学会。好了。接下来还是回到我今天晚上的这篇文章的主题吧。

首先,既然是领域驱动设计,最后就会设计出一个模型,该模型只包含领域对象和领域逻辑。我学习领域驱动设计时间还很短,所以也没能设计出好的模型出来。但我也确实在思考该如何为我设计的那个自以为设计良好的模型和外界解耦。然后通过各处查看资料也积累的一些技巧,在此在狗胆拿出来和大家分享一下。

关于如何让一个领域模型和外部的交互解耦,我主要考虑以下两点:

  1. 领域对象是否应该使用Repository;
  2. 如何让数据持久层透明的对领域对象进行持久化;
假设,我们现在讨论的领域模型处在下面的分层架构中:

Presentation Layer + Service Layer + Domain Layer + Persistence Layer

其中Domain Layer中就包含了我们说的领域模型,接下来我说的领域层的时候意思就是在强调领域模型的概念。

关于领域对象是否应该使用Repository: 

关于这个问题,我觉得领域对象不应该也不需要使用Repository,甚至在我看来领域层中根本就不需要有Repository。先想想我们为什么要把Repository放在领域层中?因为我们把它看作是一个管理特定领域对象的容器,我们会用它来获取或保存领域对象,并且它也知道该如何将它所管理的任何领域对象的修改持久化到数据库。总体来说,Repository可以将领域和持久层完全隔离。好了,我能想到的Repository的作用大概就是这个了。接下来看看Repository的缺点,我觉得它没有明显的缺点。但是他会有一个潜在的危险,如果领域模型过分依赖于Repository,那将很容易导致领域模型变成贫血模型,因为你很可能会习惯性地将业务逻辑转移到Repository中。不知道我的理解是否对,但我就是这么觉得的。后来我去网上找了一些资料,发现领域事件这么一个东东,觉得事件确实应该被引入到模型中来。但是看了别人的很多领域事件的实现方式,要么太复杂,一搞就是一个框架,要么太简单,不实用。所以打算先学习其设计意图,然后自己写一个简单的适合自己的领域事件和领域模型相结合的简单框架。到现在为止终于整出一个能工作的东西出来了。先回答一个问题,就是为什么要用领域事件而不是用Repository来做Repository做的事情?我的回答是,因为这两种实现方式会导致领域对象和外界交互时的主动性不同,领域对象触发一个领域事件是是一个主动的行为,而领域对象调用Repository的某个方法是一种请求性的被动的行为。事件可以确保领域对象永远处于某个领域逻辑的起始位置,这样就能确保领域模型的业务逻辑不会被分散。整个领域模型是完全封闭的,它不需要去请求别人帮它完成某个任务,而是只要告诉别人我做了什么或者我将要做什么或者我想要做什么,等等。总之,一定是领域模型去通知别人,而不是去请求别人。而调用Repository就不一样了,它就会使领域领域模型依赖于Repository,即便只是接口的依赖。

首先声明一下,我下面所提到的任何事件和事件总线和CQRS架构中的事件和事件总线有一定的区别。我说的事件的主要职责是用来让领域对象和外界进行互动的,并且我的事件的实现方式和CQRS中也有比较大的区别,我的事件不会被持久化到数据库,仅仅是一种通讯的实现机制。

好了,如果我们用领域事件,那要怎么实现领域事件呢?其实很简单,分为两个步骤:1)生成一个事件;2)通知“事件总线“去发布这个事件;下面来看一个例子:

假设某个论坛中的帖子(Thread),它有一个属性(PostList)表示该帖子的所有的回复(Post)。

复制代码
 1  private  List < Post >  PostList
 2  {
 3       get
 4      {
 5           if  (posts  ==   null )
 6          {
 7              RaiseEvent( new  ThreadPostsQueryEvent
 8              {
 9                  Id  =  Id,
10                  SetPosts  =   new  Action < IEnumerable < Post >> (postList  =>  posts  =  postList.ToList())
11              });
12              SortPosts(posts,  true );
13          }
14           return  posts;
15      }
16  }
17  protected   void  RaiseEvent(IEvent evnt)
18  {
19      InstanceLocator.Current.GetInstance < IEventBus > ().Publish(evnt);
20  }
复制代码

这里我希望大家不要太关注这个PostList本身的设计是否合理,而是重点关注事件在将领域对象和外部解耦的实现上。

首先,我们先实例化一个事件ThreadPostsQueryEvent,并初始化它,然后调用RaiseEvent方法来“触发”这个事件,其实实质上就是让事件总线IEventBus来发布这个事件。现在,你也许会问,那领域对象不是和IEventBus耦合了吗?的确,是这样的,但没关系因为我们只是用它来发布事件的,用它来告诉外界领域模型中发生了某个事件。这样外界就可以接收到这个事件,然后一些关心这个事件的Event Handler就会响应这个事件。基于这样的设计,那事件总线(即IEventBus)也是属于领域模型的一部分,它的职责就是发布某个事件。我觉得它应该算是整个领域模型的核心了,因为任何领域对象需要和外界交互时,都是先告诉它,然后由它来通知外界。

另外,你可能还注意到了上例中的一个细节。那就是事件中携带了一个委托实例,并把它传了出去。它的作用显而易见,就是获取事件的响应信息。上例中,ThreadPostsQueryEvent事件的意图就是要告诉外界说,我想要属于我的所有的回复。 然后外界处理了这个事件后如何将返回值(回复信息)返回给领域模型呢?我想到的我认为最简单也是最直接的方式就是让事件传递一个委托出去,然后外界直接调用该委托来将处理结果返回给领域模型。我觉得这样做并没有破坏事件的独立性,原因在于被事件传递出去的委托并不是事件自己去掉的,而是外界掉的。所以可以理解为是外界响应事件并调用某个它并不认识但知道如何调用的委托方法,之所以说外界不认识该委托方法是因为该委托方法是私有的,模型并没有把它暴露出来,外界不需要知道该委托方法的方法名和具体实现,那是事件的事情,它不需要也无权关心。

有了上面这样的设计,我想我们就能很轻而易举的将模型可能和外界产生的任何交互全部用类似上面这种事件来完成了。比如,告诉外界我发生了什么,我将要发生什么,我想要什么,等等。如果需要根据外界的响应的结果来决定接下来做什么事情的情况,就传递一个委托方法实例出去即可。 而且我还觉得,利用事件可以很方便的实现延迟加载(Lazy Load)而不需要依赖于任何的ORM框架。当我们需要某个还没有Load的Aggregate Child时,只要触发一个事件即可。

最后,关于IEventBus实例,我是通过Ioc容器注入进来,这让就可以让领域模型和外界完全解耦,不依赖外界的任何东西。因此,我们的领域模型就不在需要Repository了,它只需要有:领域对象+领域事件+一个EventBus。当然,可能还有领域服务和领域工厂,非常干净。

关于如何让数据持久层透明的对领域对象进行持久化:

上面提到,Repository不会出现在领域模型中,但并不表示我们不会再用到它。Repository确实是一个用来将领域模型和数据持久化隔离的好东西。我认为我们可以将它用在前面提到的Service Layer,注意,这个Service Layer不是领域层的中Service。大家都知道,Service Layer层的逻辑是控制逻辑,而领域层的逻辑是业务逻辑。接下来先说一下我所表达的持久化透明的意思:领域层不需要知道领域对象如何被持久化。好了,有了这个目标后,我就可以谈一下该如何做到这个目标。

说白了,就是要解决让Repository实现对领域对象的新增、删除、更新三种操作的跟踪,并让Repository知道该如何持久化。我想该是贴一段代码的时候了。下面是我设计的Repository的架构:

复制代码
 1  public   interface  IEntity < TEntityId >
 2  {
 3      TEntityId Id {  get ; }
 4  }
 5  public   interface  IAggregateRoot < TEntityId >  : IEntity < TEntityId >
 6  {
 7  }
 8  public   interface  IRepository < TAggregateRoot, TEntityId >  : ICanPersistRepository
 9       where  TAggregateRoot :  class , IAggregateRoot < TEntityId >
10  {
11      TAggregateRoot Get(TEntityId id);
12       void  Add(TAggregateRoot aggregateRoot);
13       void  Remove(TAggregateRoot aggregateRoot);
14  }
15  public   interface  ICanPersistRepository
16  {
17       void  PersistChanges();
18  }
19  public   interface  ISectionRepository : IRepository < Section, Guid > ,
20                                          IEventHandler < SectionGroupChangedEvent > ,
21                                          IEventHandler < SectionAdminUserAddedEvent > ,
22                                          IEventHandler < SectionAdminUserRemovedEvent > ,
23                                          IEventHandler < SectionAdminUsersQueryEvent > ,
24                                          IEventHandler < SectionTotalThreadCountQueryEvent > ,
25                                          IEventHandler < SectionQueryEvent > ,
26                                          IEventHandler < SectionCreatedEvent >
27  {
28  }
复制代码

IEntity表示领域模型中的实体,IAggreageRoot表示聚合根,IRepository就是前面所说的Repository,ICanPersistRepository接口表示某个Repository是否有持久化的能力。之所以把持久化的功能独立定义在一个接口中是为了考虑事务的设计,这点我会将后面的文章中再做更详细的讨论。 Section表示一个论坛中的版块,它是一个聚合根。而像SectionGroupChangedEvent等这些就是领域事件了。最后,ISectionRepository就是管理Section的Repository了。

上面的设计和实现我认为已经基本上解决持久化的问题了,比如ISectionRepository可以记录新增和删除的Section,而对于Section的部分修改,Section会以事件的方式通知外界,由于ISectionRepository会响应Section的这些事件,所以自然也就知道这些更新了。最后就是如何记录Section中的那些没有用事件来通知的修改。你可能会问,为什么不用事件来通知呢?下面听我的解释:

我觉得一般一个领域对象包含一些基本属性,还包含一些引用属性,还有一些方法,等。以一个论坛版块为例:

复制代码
  1  public   class  Section : AggregateRoot < Guid >
  2  {
  3       #region  Private Variables
  4 
  5       private  Group group;
  6       private   int ?  totalThreadCount;
  7       private  List < User >  adminUserList;
  8 
  9       #endregion
 10 
 11       #region  Constructors
 12 
 13       public  Section(Guid id, Group group) :  base (id)
 14      {
 15           this .group  =  group;
 16      }
 17 
 18       #endregion
 19 
 20       #region  Public Properties
 21 
 22      [TrackingProperty]
 23       public   string  Subject {  get set ; }
 24 
 25      [TrackingProperty]
 26       public   bool  Enabled {  get set ; }
 27 
 28       public  Group Group
 29      {
 30           get
 31          {
 32               return  group;
 33          }
 34           set
 35          {
 36               if  (group  !=  value  &&  value  !=   null )
 37              {
 38                  group  =  value;
 39                  RaiseEvent( new  SectionGroupChangedEvent { Id  =  Id, Group  =  group });
 40              }
 41          }
 42      }
 43       public   int  TotalThreadCount
 44      {
 45           get
 46          {
 47               if  (totalThreadCount  ==   null )
 48              {
 49                  RaiseEvent( new  SectionTotalThreadCountQueryEvent
 50                  {
 51                      Id  =  Id,
 52                      SetTotalThreadCount  =   new  Action < int > (count  =>  totalThreadCount  =  count)
 53                  });
 54              }
 55               return  totalThreadCount.Value;
 56          }
 57      }
 58       public  ReadOnlyCollection < User >  AdminUsers
 59      {
 60           get
 61          {
 62               return  AdminUserList.AsReadOnly();
 63          }
 64      }
 65 
 66       #endregion
 67 
 68       #region  Public Methods
 69 
 70       public   void  AddAdminUser(User user)
 71      {
 72           if  ( ! AdminUserList.Contains(user))
 73          {
 74              AdminUserList.Add(user);
 75              RaiseEvent( new  SectionAdminUserAddedEvent { Id  =  Id, User  =  user });
 76          }
 77      }
 78       public   void  RemoveAdminUser(User user)
 79      {
 80           if  (AdminUserList.Contains(user))
 81          {
 82              AdminUserList.Remove(user);
 83              RaiseEvent( new  SectionAdminUserRemovedEvent { Id  =  Id, User  =  user });
 84          }
 85      }
 86 
 87       #endregion
 88 
 89       #region  Private Properties
 90 
 91       private  List < User >  AdminUserList
 92      {
 93           get
 94          {
 95               if  (adminUserList  ==   null )
 96              {
 97                  RaiseEvent( new  SectionAdminUsersQueryEvent
 98                  {
 99                      Id  =  Id,
100                      SetUsers  =   new  Action < IEnumerable < User >> (users  =>  adminUserList  =  users.ToList())
101                  });
102              }
103               return  adminUserList;
104          }
105      }
106 
107       #endregion
108  }
复制代码

同样,希望大家不要把重点放在分析我设计的领域对象(Section)是否合理,我现在清楚的知道自己在如何设计领域对象方面还没什么经验,还要好好学习。我希望大家只要把焦点放在我是如何做到让一个领域对象告诉外界或让外界有能力知道他的状态更新了。

首先,上例中,Subject、Enabled这两个就是我说的基本属性,而Group就是一个引用属性,Group是一个版块分组,一个版块分组下有多个版块,是一对多的关系。所以Section会有对一个Group的引用。另外,TotalThreadCount(版块总帖子数)和AdminUsers(版主信息)也是引用属性。判定什么属性是基本属性什么属性是引用属性的方法很简单,就是看该属性的数据是否是该AggregateRoot类本身固有的简单类型或值类型。如果不是,则是引用属性,如果是则是基本属性。比如TotalThreadCount,根据我目前的设计他并不是一个基本属性,因为它的获取要通过发事件来获取。 同理Group和AdminUsers也不是。

一般情况下,我们对于基本属性的修改,往往是直接赋值的,比如下面的例子:

复制代码
 1  public  BaseReply UpdateSection(UpdateSectionRequest request)
 2  {
 3      BaseReply reply  =   new  BaseReply();
 4 
 5       using  (IUnitOfWork unitOfWork  =  InstanceLocator.Current.GetInstance < IUnitOfWork > ())
 6      {
 7           try
 8          {
 9              request.Validate();
10              var sectionRepository  =  InstanceLocator.Current.GetInstance < ISectionRepository > ();
11              Section section  =  sectionRepository.Get(request.Id);
12              section.Subject  =  request.Subject;
13              section.Enabled  =  request.Enabled;
14              unitOfWork.SubmitChanges();
15              reply.Success  =   true ;
16          }
17           catch  (BusinessValidationException ex)
18          {
19              reply.Success  =   false ;
20              reply.ErrorState.ErrorItems  =  ex.ValidationError.GetErrors().ToErrorItemList();
21          }
22           catch  (Exception ex)
23          {
24              reply.Success  =   false ;
25              reply.ErrorState.ExceptionMessage  =  ex.Message;
26          }
27      };
28 
29       return  reply;
30  }
复制代码

上面的UpdateSection是Service Layer层中的一个方法,用来更新一个Section。该方法的执行流程是:首先根据Repository根据SectionId获取领域对象Section,然后更新Section的Subject和Body属性(第12行和13行),最后调用Unit of Work的SubmitChanges方法将修改持久化到数据库。当Subject和Body属性被修改时并没有触发任何的事件。主要是我考虑到如果要为每个这种简单属性都弄个与之对应的事件,那会导致事件泛滥。并且每次一个基本属性被修改,就触发一个事件,这样性能也不好。再者,有些情况下一些属性会被连续修改好多次,举个例子,比如现在你把Subject先赋值为“subject1”,后来又赋值为"subject2",如果每次都触发事件,那就会出发两个事件,也就是该字段会被持久化两次,但实际上我们只关心Subject属性最后的状态而已。因此,我觉得更好的做法,应该是对于这种基本属性被修改时,不触发事件,而应该采用备份初始状态和在保存是比较是否被修改的方法来实现。但是考虑到基础框架可能不知道哪些属性需要被备份,如果把整个领域对象的所有属性都备份,那无疑性能会很差,所以用了一个折中的方法,就是在需要备份的属性上加一个“TrackingProperty”的特性来指明该属性需要被备份。具体的实现方法可以看下面的介绍。

在我的设计中,我会遵循这样的原则,如果是引用属性的任何修改,就通过发事件,因为往往这种属性的修改往往比较难跟踪(想象一下集合中套集合,又套其他引用对象什么的,真那个复杂呀,对吧),而且往往都是更新其他关联表中的数据;如果是基本类型,则用一个Attribute特性来标识,并且在属性值修改时也不会发事件。但是加一个Attribute已经足以,因为我们可以在将一个Section通过ISectionRepository取出来的时候,将Section中标识了TrackingProperty特性的属性通过一个Dictionary保存起来。Dictionary的Key是属性名,Value是属性值。也就是会将Section的所有简单属性的值备份起来。然后当ISectionRepository在做持久化操作的时候,我们将最新的Section中的基本属性和之前备份过的Dictionary中的值进行比较,如果有修改过,则更新,没修改过,则不更新。下面是我实现的关于如何备份和判断是否有修改的相关代码:

复制代码
 1  private  AggregateRootBackupObject < TEntityId >  CreateBackupObject(TAggregateRoot aggregateRoot)
 2  {
 3      var backupObject  =   new  AggregateRootBackupObject < TEntityId > () { Id  =  aggregateRoot.Id };
 4 
 5      GetTrackingProperties(aggregateRoot).ForEach(
 6          propertyInfo  =>  backupObject.TrackingProperties.Add(propertyInfo.Name, propertyInfo.GetValue(aggregateRoot,  null ))
 7      );
 8 
 9       return  backupObject;
10  }
11  private   bool  IsAggregateRootModified(TrackObject < TAggregateRoot, TEntityId >  trackingObject)
12  {
13       if  (trackingObject.Status  ==  ObjectStatus.Tracking  &&  trackingObject.CurrentValue  !=   null )
14      {
15           foreach  (var propertyInfo  in  GetTrackingProperties(trackingObject.CurrentValue))
16          {
17              var backupValue  =  trackingObject.BackupValue.TrackingProperties[propertyInfo.Name];
18              var currentValue  =  propertyInfo.GetValue(trackingObject.CurrentValue,  null );
19               if  (backupValue  !=  currentValue)
20              {
21                   return   true ;
22              }
23          }
24      }
25       return   false ;
26  }
27  private  List < PropertyInfo >  GetTrackingProperties(TAggregateRoot aggregateRoot)
28  {
29       return  (from propertyInfo  in  aggregateRoot.GetType().GetProperties(BindingFlags.Public  |  BindingFlags.Instance)
30               where  propertyInfo.GetCustomAttributes( typeof (TrackingPropertyAttribute),  true ).Length  >   0
31              select propertyInfo).ToList();
32  }
33 
34 
35  public   enum  ObjectStatus
36  {
37      New,
38      Tracking,
39      Removed
40  }
41  public   class  AggregateRootBackupObject < TEntityId >
42  {
43       public  AggregateRootBackupObject() { TrackingProperties  =   new  Dictionary < string object > (); }
44       public  TEntityId Id {  get set ; }
45       public  Dictionary < string object >  TrackingProperties {  get private   set ; }
46  }
47  public   class  TrackObject < TAggregateRoot, TEntityId >   where  TAggregateRoot :  class , IAggregateRoot < TEntityId >
48  {
49       public  AggregateRootBackupObject < TEntityId >  BackupValue {  get set ; }
50       public  TAggregateRoot CurrentValue {  get set ; }
51       public  ObjectStatus Status {  get set ; }
52  }
复制代码

关于代码的思路,我已经在上面阐述过了。相信大家应该能轻松看懂。没什么深奥的东西的。

好了,总结一下,关于如何让Repository跟踪AggregateRoot的新增、修改、删除。我是通过如下的设计来实现的:

新增:通过IRepository.Add方法跟踪;

修改: 简单属性,通过备份和比较,引用属性,通过事件;

删除:通过IRepository.Remove方法跟踪;

新增和删除以及基本属性的修改操作在Service Layer层做,事件的触发在Domain Layer层做。

我觉得这样的设计已经实现了我的既定目标:

1)领域层很干净,连Repository都没有;

2)实现了持久化透明;

3)效率方面应该不会太差,因为Repository没有做任何多做的事情,它只做了需要做的事情;

好了,不知不觉都这么晚了,老婆都睡的很香了呢,我得睡了,明天睡个懒觉,哈哈。 希望本文能带给大家一些以前没看到过的东西。


目录
相关文章
|
7月前
|
领域建模
架构设计 DDD领域建模 核心概念
【1月更文挑战第6天】架构设计 DDD领域建模 核心概念
|
缓存 前端开发 中间件
DDD 领域驱动设计落地实践系列:工程结构分层设计
前面几篇文章中,笔者给大家阐述了 DDD 领域驱动设计的三大过程,重点围绕如何通过战略设计与战术设计进行 DDD 落地实践进行了详细的讨论,但是还没有涉及到工程层面的落地。实际上所有的这些架构理论到最后都是为了使得我们代码结构更加清晰,从而开发出 bug 少、扩展性强、逻辑清楚的应用。因此本文就是为了解决 DDD 领域驱动落地实践最后一公里问题,将我们分析出来的领域模型通过与工程结构的映射实现真正的落地。
DDD 领域驱动设计落地实践系列:工程结构分层设计
|
Cloud Native 架构师 Devops
云原生时代领域驱动设计(DDD)的价值——从《没有银弹》说起
软件开发需要面对本质困难和附属困难。云原生、DevOps实践大幅降低了附属困难,使得架构师可以全力聚焦于业务复杂性,而DDD恰是管理业务复杂性的有效方法。
1590 0
云原生时代领域驱动设计(DDD)的价值——从《没有银弹》说起
|
4月前
|
缓存 架构师 中间件
成为工程师 - 如何做DDD领域驱动设计?
成为工程师 - 如何做DDD领域驱动设计?
|
消息中间件 缓存 架构师
【老猿说架构】系统架构设计原则和步骤
【老猿说架构】系统架构设计原则和步骤
362 0
【老猿说架构】系统架构设计原则和步骤
|
敏捷开发 消息中间件 测试技术
微服务面试必读:拆分、事务、设计的综合解析与实践指南
微服务的应用级别确实相对简单,但在实际开发中仍有一些技术难点需要解决。对于微服务组件的使用,确实不存在太大差距,但在设计和开发过程中需要积累经验。学习微服务的上手时间相对较短,可能只需一周到一个月的时间。然而,设计经验和技术难点是需要个人长期积累的,不能急于求成。因此,在使用和开发微服务时,更应该关注方案思考,展示自己对该领域的理解和见解。这样能够体现出你对问题的思考深度和解决方案的创新性。希望这次面试种子题目的解答能够帮助你应对面试官的问题!
120 0
|
架构师 算法 测试技术
小团队也能做DDD-中篇
小团队也能做DDD-中篇
234 0
|
消息中间件 JavaScript 小程序
领域驱动设计(DDD)的几种典型架构介绍
领域驱动设计(DDD)的几种典型架构介绍
|
Web App开发 机器学习/深度学习 数据可视化
OneCode 领域驱动设计(DDD)技术实践(一)
OneCode-DSM(以下简称DSM)工具集是建立是以OneCode低代码引擎为基础专注于低代码建模应用的高阶建模工具。 在OneCode引擎中,出了为普通用户提供无代码的拖动设计器,低代码的业务逻辑编排器,之外还提供了供专业业务领域专家的使用的DSM建模工具。
|
uml Java 测试技术
带你读《软件架构理论与实践》之一:软件架构概述
本书是上篇基础理论篇,重点介绍软件架构的基本理论和方法,内容包括软件架构的发展历史、软件架构的概念和建模方法、软件架构风格和模式、软件架构描述语言,以及软件架构与敏捷开发之间的关系等。