天天看點

領域驅動設計(DDD)的實踐經驗分享之持久化透明

首先,既然是領域驅動設計,最後就會設計出一個模型,該模型隻包含領域對象和領域邏輯。我學習領域驅動設計時間還很短,是以也沒能設計出好的模型出來。但我也确實在思考該如何為我設計的那個自以為設計良好的模型和外界解耦。然後通過各處檢視資料也積累的一些技巧,在此在狗膽拿出來和大家分享一下。

關于如何讓一個領域模型和外部的互動解耦,我主要考慮以下兩點:

領域對象是否應該使用repository;

如何讓資料持久層透明的對領域對象進行持久化;

假設,我們現在讨論的領域模型處在下面的分層架構中:

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)。

領域驅動設計(DDD)的實踐經驗分享之持久化透明

 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 }

領域驅動設計(DDD)的實踐經驗分享之持久化透明

這裡我希望大家不要太關注這個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的架構:

領域驅動設計(DDD)的實踐經驗分享之持久化透明

 1 public interface ientity<tentityid>

 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 }

領域驅動設計(DDD)的實踐經驗分享之持久化透明

ientity表示領域模型中的實體,iaggreageroot表示聚合根,irepository就是前面所說的repository,icanpersistrepository接口表示某個repository是否有持久化的能力。之是以把持久化的功能獨立定義在一個接口中是為了考慮事務的設計,這點我會将後面的文章中再做更詳細的讨論。 section表示一個論壇中的版塊,它是一個聚合根。而像sectiongroupchangedevent等這些就是領域事件了。最後,isectionrepository就是管理section的repository了。

上面的設計和實作我認為已經基本上解決持久化的問題了,比如isectionrepository可以記錄新增和删除的section,而對于section的部分修改,section會以事件的方式通知外界,由于isectionrepository會響應section的這些事件,是以自然也就知道這些更新了。最後就是如何記錄section中的那些沒有用事件來通知的修改。你可能會問,為什麼不用事件來通知呢?下面聽我的解釋:

我覺得一般一個領域對象包含一些基本屬性,還包含一些引用屬性,還有一些方法,等。以一個論壇版塊為例:

領域驅動設計(DDD)的實踐經驗分享之持久化透明

  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 }

領域驅動設計(DDD)的實踐經驗分享之持久化透明

同樣,希望大家不要把重點放在分析我設計的領域對象(section)是否合理,我現在清楚的知道自己在如何設計領域對象方面還沒什麼經驗,還要好好學習。我希望大家隻要把焦點放在我是如何做到讓一個領域對象告訴外界或讓外界有能力知道他的狀态更新了。

首先,上例中,subject、enabled這兩個就是我說的基本屬性,而group就是一個引用屬性,group是一個版塊分組,一個版塊分組下有多個版塊,是一對多的關系。是以section會有對一個group的引用。另外,totalthreadcount(版塊總文章數)和adminusers(版主資訊)也是引用屬性。判定什麼屬性是基本屬性什麼屬性是引用屬性的方法很簡單,就是看該屬性的資料是否是該aggregateroot類本身固有的簡單類型或值類型。如果不是,則是引用屬性,如果是則是基本屬性。比如totalthreadcount,根據我目前的設計他并不是一個基本屬性,因為它的擷取要通過發事件來擷取。 同理group和adminusers也不是。

一般情況下,我們對于基本屬性的修改,往往是直接指派的,比如下面的例子:

領域驅動設計(DDD)的實踐經驗分享之持久化透明

 1 public basereply updatesection(updatesectionrequest request)

 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 }

領域驅動設計(DDD)的實踐經驗分享之持久化透明

上面的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中的值進行比較,如果有修改過,則更新,沒修改過,則不更新。下面是我實作的關于如何備份和判斷是否有修改的相關代碼:

領域驅動設計(DDD)的實踐經驗分享之持久化透明

 1 private aggregaterootbackupobject<tentityid> createbackupobject(taggregateroot aggregateroot)

 3     var backupobject = new aggregaterootbackupobject<tentityid>() { id = aggregateroot.id };

 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 }

領域驅動設計(DDD)的實踐經驗分享之持久化透明

關于代碼的思路,我已經在上面闡述過了。相信大家應該能輕松看懂。沒什麼深奧的東西的。

好了,總結一下,關于如何讓repository跟蹤aggregateroot的新增、修改、删除。我是通過如下的設計來實作的:

新增:通過irepository.add方法跟蹤;

修改: 簡單屬性,通過備份和比較,引用屬性,通過事件;

删除:通過irepository.remove方法跟蹤;

新增和删除以及基本屬性的修改操作在service layer層做,事件的觸發在domain layer層做。

我覺得這樣的設計已經實作了我的既定目标:

1)領域層很幹淨,連repository都沒有;

2)實作了持久化透明;

3)效率方面應該不會太差,因為repository沒有做任何多做的事情,它隻做了需要做的事情;

好了,不知不覺都這麼晚了,老婆都睡的很香了呢,我得睡了,明天睡個懶覺,哈哈。 希望本文能帶給大家一些以前沒看到過的東西。