首先,把最真摯的情感送與梅西,加油!
寫在前面
閱讀目錄:
上一篇《設計窘境:來自 Repository 的一絲線索,Domain Model 再重新設計》。
講本篇內容之前,先回顧上一篇所討論的內容,主要是 Repository(倉儲)的職責問題,屬于領域?還是應用層?其實到頭來也沒有準確的結論,但是最終比較偏向于倉儲定義在領域,實現在基礎層,調用在應用層。你可能有些疑問,為什么要討論倉儲的職責問題?看過上一篇的內容你可能會有些答案,這也就是上一篇博文標題,為什么是“設計窘境”的原因。
本篇博文標題定義為:”撥亂反正“,就像戰亂紛爭的年代,清剿叛軍,回歸大統,所表達的意思就是,排除一切干擾因素,回歸正確的業務場景,然后進行干凈的領域模型設計。領域驅動設計這個實踐系列已經寫了大概六七篇博文了,從領域模型到底如何設計?到領域模型重新設計,到領域模型再重新設計,到現在的領域模型再再重新設計。。。對,被你看出來了,領域驅動設計中最重要的就是領域模型的設計,但是到現在為止,領域模型的設計一直沒有完成,而是一次一次的被推翻重建,道路是曲折的,前途是光明的,但是這個過程真是太痛苦了,回顧現有的這個過程,就會發現,為什么領域模型設計這么難?原因就是領域模型中的職責分配問題,誰存在?誰不存在?誰屬于誰?誰不屬于誰?誰是誰的?誰是誰的誰的誰(好像是一段歌詞,不好意思哈,打順手了)?但這一切的前提都是建立在正確的業務場景之上,那我們這個業務場景究竟是什么?希望你能從本篇博文中找到些許答案。
如果想了解前面大大小小的“坑”,請訪問《[17]小菜學習編程-DDD》,如果不想了解(推薦),那就請從本篇博文開始了解吧。
重申業務場景
其實這個項目(MessageManager)一開始設計的時候,定義為短消息系統,也就是類似博客園短消息系統,發送人給接收人發送一個短消息,接收人接收到這個短消息進行查看,發送人和接收人可以查看各自的收發件箱,大概也就是這樣的一個業務場景。但是后來才發現真正的業務場景是,短消息系統中的“短”字應該去掉,也就變成了消息系統,不一定只適用于短消息發送,還可能發送郵件、發送短信等,但是這種發送不會像短消息那種需要進行持久化,他們只是一個發送的動作。
有朋友可能看到這可能會有些疑問,發送郵件、發送短信這種信息發送,不應該屬于應用層所干的事嗎?其實這很容易造成誤解,比如一個在線商城應用程序中,業務需求要求每提交一筆訂單發送一封郵件給客戶,我們一般會在應用層接收來自領域處理完訂單的請求后,調用基礎層提供的服務完成郵件發送,這種設計是沒有什么問題的。需要明確的是現在的業務場景是消息系統,而不是在線商城,聚焦的是消息業務,那所有具體的消息都是業務,也就包含郵件發送和短信發送,因為他們是屬于消息的一種。
毫無疑問,我們現在的消息系統就必須要抽離出,所有消息的抽象業務邏輯,以適用于所有消息業務場景的具體應用,我覺得這才是《道德經》中“有之以為利,無之以為用”的真諦所在(以前關于這個觀點的解讀,現在感覺都是在瞎扯),消息領域模型所展現的就是“無”,體現出來的結果就是“有”。說具體點就是,所有消息應用包含什么東西,我覺得就是三個對象:發送人對象、接收人對象和消息對象,這三個對象組成一個完整的消息系統,不管短消息、郵件和短信都是如此,三者缺一不可,缺少任何一種就不是一個完整的消息系統,可能在不同的消息場景中會有些變化,比如短消息系統中,發送人是一個用戶對象,但是在郵件和短信系統中,發送人只是一個郵箱和手機號標識,但是它也代表著發送人所表達的意義。
除了抽離出消息系統所存在的對象,還要去了解整個消息業務場景中所表現的過程,在消息系統中最重要的一個用例就是消息發送,因為查看消息或者查看收發件箱只在短消息業務場景下,郵件和短信的查看可以通過電子郵箱和手機查看,在消息系統中只存在發消息這個業務,那發消息的流程是什么?首先必須有發件人(標識),填寫一個消息,貼上收件人(標識),然后送給郵遞員進行郵遞,短消息、郵件和短信發送都是這個過程,那我們再來看看下之前用戶實體的設計:
1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 using System; 7 using System.Collections.Generic; 8 9 namespace MessageManager.Domain.Entity 10 { 11 public class User : IAggregateRoot 12 { 13 public User(string loginName, string displayName) 14 { 15 if (string.IsNullOrEmpty(loginName)) 16 { 17 throw new ArgumentException("loginName can't be null"); 18 } 19 if (string.IsNullOrEmpty(displayName)) 20 { 21 throw new ArgumentException("displayName can't be null"); 22 } 23 this.ID = Guid.NewGuid().ToString(); 24 this.LoginName = loginName; 25 this.DisplayName = displayName; 26 this.SendMessages = new List<Message>(); 27 this.ReceiveMessages = new List<Message>(); 28 } 29 30 public string ID { get; set; } 31 public string LoginName { get; private set; } 32 public string DisplayName { get; private set; } 33 public virtual ICollection<Message> SendMessages { get; set; } 34 public virtual ICollection<Message> ReceiveMessages { get; set; } 35 36 public void SendMessage(User receiveUser, Message message) 37 { 38 this.SendMessages.Add(message); 39 receiveUser.ReceiveMessage(this, message); 40 } 41 private void ReceiveMessage(User sendUser, Message message) 42 { 43 this.ReceiveMessages.Add(message); 44 } 45 } 46 }
在之前的設計中,我們把用戶設計成一個實體,而且是一個獨立于消息的聚合根,因為需要通過其他屬性獲取到用戶,但是在現在的消息業務場景中,用戶是不需要存儲的,也就是說用戶的獲取或驗證都是通過外部實現的,很顯然現在的這種設計就有點不合理了。還有就是用戶實體下的 SendMessages 和 ReceiveMessages 屬性,用來表示此用戶下的發件箱和收件箱,如果存在用戶實體,這種設計也是有待商榷的,更何況用戶實體并不存在。還有后面加的 SendMessage 和 ReceiveMessage 方法,表示用戶發送消息和接收消息的一種行為,為什么要這樣設計?主要原因還是來自上一篇的討論:
《領域驅動設計》賬戶轉賬示例流程分享(來自 hailants):
-
用戶發起業務,界面層調用應用層的轉賬操作
-
應用層調用 a123 的轉賬操作,傳入對方賬戶和轉賬金額
-
賬戶 a123 調用 a234 加錢操作
-
a234 將操作添入事務單元,向賬戶 a123 返回確認
-
賬戶 a123 調用自身減錢操作,添入事務單元,向應用層返回確認
-
應用層提交事務,向界面層返回確認
相應推理出發送消息業務流程:
-
用戶發起發消息業務請求,界面調用應用層轉賬操作(傳入參數為:標題,內容,發件人名稱,收件人名稱)
-
應用層首先創建一個發件人對象(通過倉儲獲得),然后再創建收件人對象和消息對象
-
發件人對象調用用戶實體中的 SendMessage 操作(參數為收件人對象和消息對象)
-
在發件人對象中的 SendMessage 方法中,收件人對象調用用戶實體中的 ReceiveMessage 操作(參數為發件人和消息對象)
-
在以上操作的完成后添加到事務操作(具體就是往消息倉儲中添加消息領域對象)
-
應用層提交事務,向界面返回確認。
其實這種設計某種意義上也沒有什么問題,至少在短消息系統中,因為發消息本身就是用戶的一種行為,但是如果仔細一想就會覺得有些別扭,首先賬戶轉賬業務流程和發消息業務流程雖然表面上相似,但是其聚合的對象并不相同,比如賬戶轉賬示例中,聚合的是賬戶,發消息業務場景中如果這樣分析應該聚合的是用戶,但是很顯然并不是,聚合的是消息對象,如果聚合用戶就會變成用戶消息系統了,這就偏離了大方向,并不是我們所想看到的。還有就是現在的這種設計只適用于短消息業務場景,在郵件和短信業務場景中并不適用,為什么?因為郵件發送和短信發送并不存在用戶的概念(這個用戶概念并不是現實生活中的人,而是系統中的用戶,這個觀點很容易造成誤解),有的只是一個標識(電子郵件或手機號),用來體現出發件人和收件人的概念,也就是說這種標識并不是一個對象(沒有行為的對象),準確的來說應該不是一個實體,那是什么?在領域驅動設計中設計為值對象(為什么要設計成值對象,后面領域模型設計中進行說明),一個沒有行為的對象中加入行為操作,本身邏輯就存在問題,所以這種設計是有問題的。
回到發消息這個業務用例上,一個消息對象存在意義的前提是擁有標題、內容、發送人(標識)和接收人(標識),當然還存在一些選填元素,但是主要包含這四個元素,缺少任何一種,就不是一個完整的消息對象,也就是說不能用來發送。發送操作不僅僅是一個對象的行為,而應該是消息領域模型提供的一種服務,也就是領域服務,提供各種消息發送的服務,這一點很容易和基礎層的消息發送服務搞混,區分他們只需要記住一點:基礎層是技術上的實現,領域是業務上的抽象,因為這個業務場景是消息系統,那發消息就是一種業務用例,而并不是一個技術調用方法。
說了這么多,總結一下所描述的消息業務場景:抽象所有消息業務邏輯(包含短消息、郵件和短信等),應用具體的業務場景(比如短消息)。發消息業務用例:發送人(系統用戶)填寫消息,包含標題、內容、發送人(標識)、接收人(標識),調用(應用層發送請求)服務(領域服務)發送消息,相當于郵遞員投遞信件,就是這樣的一個過程,至少聽起來這么簡單,實現起來呢?我覺得那是另一方面的問題了,呵呵。
Domain Model 設計
回顧之前領域模型的設計,你會發現完全是一套一套的,也就是說差別很大,造成這種設計的主要原因是領域模型中的邊界和職責問題,這也是領域模型設計中最難的一點,如果邊界確定和職責分配和上一版本有細微的差別,那設計出來的領域模型會和上一版本完全不一樣,就比如用戶的邊界確定(是實體?還是值對象?),還有就是倉儲的職責問題(領域還是應用層?),如果不確定這些因素,設計出來的領域模型就不是真正的領域模型。
倉儲的職責問題在上一篇中有過討論,開頭也給過總結,這邊就不多說了,其實現在在設計領域模型的時候就要排除一切干擾因素,比如我現在在設計的時候就把表現層、應用層、倉儲中的項目卸載掉了,這個解決方案中的項目就只剩基礎層、領域模型和領域中的單元測試項目,這樣在設計領域模型的時候才能保證其“純凈度”。
在上面重申業務場景節點中,把發送人(標識)或接收人(標識)設計成值對象,為什么要這樣設計?我們稍后解讀,先說一下實體和值對象的區別,這兩個對象的概念網上有很多資料進行參考,但最好還是看下《領域驅動設計》這本書的定義,作者關于實體的解讀,重點強調了實體的唯一性,也就是說實體必須通過唯一標識進行區分,比如消息系統中的消息實體,雖然我和同一個人發送同樣內容的消息,但是這兩個消息就不能用同一個對象進行標識,而是兩個具有同樣消息內容的不同消息實體,換句話說,我們不僅需要知道消息是什么,而且還要知道消息是哪個。關于值對象的解讀,作者主要強調:“值對象就是那些在設計中我們只關心它們是什么,而不關心它們誰是誰的對象。”這是和實體的最好區分,就是說對于值對象,我們只要知道他們是什么就行了,而并不需要他們是哪個,就比如消息系統中的消息狀態值對象,包含兩個內容:未讀和已讀,相對于消息實體而言,我們只需要知道消息狀態是什么,它所表達的內容(我們并不關心,它從哪里來,到哪里去)。還有就是值對象是一般相對于實體而言的,就是說值對象一般附屬在實體上,如果獨立于實體,他們就不存在任何意義,就像消息狀態值對象,它如果獨立于消息實體,就沒有什么意義了,因為消息狀態只有相對于消息對象而言才有存在的意義。
內容有點多,換個行。
那為什么要把發送人(標識)或接收人(標識)設計成值對象?那我們分析一下消息系統中收發件人,首先需要明確一點的就是,我們設計的是消息系統,并非是用戶消息系統,也就是說把用戶中的行為剔除掉(SendMessage 和 ReceiveMessage),對象除掉行為之后就只有屬性了,如果一個實體中只有屬性,是不是所必要的呢?對于消息系統而言,用戶是不被存儲的,也就是說用戶只是在消息系統中作為一個標識,所謂標識就是所表現出來的一個值。比如發郵件業務場景中,用戶A(123@gmail.com)給用戶B(456@gmail.com)發送了一封郵件,對于消息系統,我需要確定用戶A和用戶B嗎?顯然不需要,因為我只要知道 123@gmail.com 這個郵箱給 456@gmail.com 這個郵箱發送了一封郵件就行了,至于是哪個用戶發的,在消息系統中并不需要考慮。在短消息業務場景中也是類似,因為短消息系統中的用戶概念來自于其他系統,那其他系統對于用戶而言肯定有一個唯一標識(比如主鍵值、用戶名、顯示名等等),對于消息系統而言,我只要知道這個標識就行了,至于這個標識所代表的是哪個用戶,并不需要關心。
把發送人(標識)或接收人(標識)設計成值對象,還有一個重要原因是,如果把發送人(標識)或接收人(標識)設計成實體,那他們可以獨立于消息實體存在,但是我們所設計的是消息系統,并不是用戶消息系統,用戶來自于外部,如果在消息系統中單獨存在就有點不倫不類了,還有就是如果用戶設計成實體,這些用戶實體對象是需要存儲的,這就違背了我們的業務需求。如果把用戶設計成值對象呢?就符合我們現在的消息業務場景了,因為在消息系統中,我們只需要知道用戶是什么,而且用戶獨立于消息,對于消息系統而言將沒有任何意義。
概念理清楚,下面就是具體的設計了。
IContact 抽象接口:
1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 namespace MessageManager.Domain.ValueObject 7 { 8 public interface IContact 9 { 10 string Name { get; set; } 11 } 12 }
Sender-發送人:

1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 namespace MessageManager.Domain.ValueObject 7 { 8 public class Sender : IContact 9 { 10 public Sender(string name) 11 { 12 this.Name = name; 13 } 14 public string Name { get; set; } 15 } 16 }
Recipient-接收人:

1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 namespace MessageManager.Domain.ValueObject 7 { 8 public class Recipient : IContact 9 { 10 public Recipient(string name) 11 { 12 this.Name = name; 13 } 14 public string Name { get; set; } 15 } 16 }
因為發送人和接收人都是聯系人的一種,只不過所扮演的角色不同,所以我們可以把他們抽象出來,IContact 接口中只有一個 Name 屬性,用來表示我們上面所討論的標識,用 Name 也更符合現實生活中的名稱(發送人和接收人)。
消息領域模型中的對象確定后,下面就是發送消息服務了,因為消息業務場景中,不只包含短消息的發送,還有郵箱發送和短信發送等,所以我們需要把消息領域服務抽象出來。
ISendMessageService 發送消息領域服務:
1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 using MessageManager.Domain.Entity; 7 namespace MessageManager.Domain.DomainService 8 { 9 public interface ISendMessageService 10 { 11 bool SendMessage(Message message); 12 } 13 }
SendShortMessageService 短消息領域發送服務實現:

1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 using MessageManager.Domain.Entity; 7 namespace MessageManager.Domain.DomainService 8 { 9 /// <summary> 10 /// SendShortMessag領域服務實現-短消息發送 11 /// </summary> 12 public class SendShortMessageService : ISendMessageService 13 { 14 public bool SendMessage(Message message) 15 { 16 return true; 17 } 18 } 19 }
除了短消息發送服務,我們還可以實現郵箱發送服務(業務上的 SendMailMessageService),其內部可以調用基礎層的發送郵件服務(技術上的 SendMailService),這兩個概念容易搞混,需要區分開。通過這個消息領域服務,我們可以在其他的應用程序中進行調用,使用什么消息發送,只需要在調用的時候注入相應的接口實現即可,為什么這么厲害?因為它是所有消息發送的抽象描述,哈哈。
我們再來看下單元測試代碼:
1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 using MessageManager.Domain.DomainService; 7 using MessageManager.Domain.Entity; 8 using MessageManager.Domain.ValueObject; 9 using System; 10 using Xunit; 11 12 namespace MessageManager.Domain.Tests 13 { 14 public class MessageDomainTest 15 { 16 /// <summary> 17 /// 消息發送-短消息 18 /// </summary> 19 [Fact] 20 public void DomainTest_SendShortMessage() 21 { 22 ISendMessageService sendMessageService = new SendShortMessageService(); 23 IContact sender = new Sender("sender"); 24 IContact recipient = new Recipient("recipient"); 25 Message message = new Message("title", "content ", sender, recipient); 26 Assert.True(sendMessageService.SendMessage(message)); 27 } 28 } 29 }
從單元測試的代碼我們可以很清楚的描述發消息這個業務用例,首先創建一個發送短消息的領域服務對象,然后分別創建發送人和接收人對象,標識分別為:sender 和 recipient,下面創建一個消息對象,然后調用領域服務傳入消息對象參數進行發送,完成整個的發消息業務。雖然看起來簡單,也很容易造成誤解,有人也會懷疑領域模型就是這樣?其實就是這樣,測試用例代碼描述的就是業務用例,它體現出來的就是領域模型,當然,SendMessage 方法中只有一段代碼,但是它所表達的就是這個業務場景的具體體現。
有時候領域模型設計不出來,可以先寫領域模型測試用例的偽代碼,因為領域模型的測試用例反應的就是業務需求,一個完成業務場景的具體過程,這也是一種開發的方式,DDD+TDD=?(某一方面的相加)
后記
有朋友可能會覺得:如此簡單的業務場景,使用領域驅動設計開發,會不會太簡單了?或者說根本不適合?我只想說:大哥,如果你覺得簡單,請收了我,可好?
其實任何存在具體的業務場景,不管簡單或不簡單,都可以使用領域驅動設計開發,領域驅動設計應對的是復雜度,這個復雜度可以理解為未來的復雜度,如果在前期開發的時候,領域模型設計的好,那么后面改東西就會很方便,當然到現在為止,我還只是道聽途說,并沒有真正體會它的好處,但是我很期待,我想那種感覺應該會很美妙。
關于本篇博文內容就是這些,不管錯與對,歡迎大家討論,只有這樣,大家才可以學到更多,不只是你我哦。
MessageManager 項目開源地址:
- GitHub 開源地址:https://github.com/yuezhongxin/MessageManager
- ASP.NET MVC 發布地址:http://www.xishuaiblog.com:8081/
- ASP.NET WebAPI 發布地址:http://www.xishuaiblog.com:8082/api/Message/GetMessagesBySendUser/小菜
如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^
文章列表