文章出處

首先,把最真摯的情感送與梅西,加油!

寫在前面

  閱讀目錄:

  上一篇《設計窘境:來自 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):

  1. 用戶發起業務,界面層調用應用層的轉賬操作

  2. 應用層調用 a123 的轉賬操作,傳入對方賬戶和轉賬金額

  3. 賬戶 a123 調用 a234 加錢操作

  4. a234 將操作添入事務單元,向賬戶 a123 返回確認

  5. 賬戶 a123 調用自身減錢操作,添入事務單元,向應用層返回確認

  6. 應用層提交事務,向界面層返回確認

  相應推理出發送消息業務流程:

  1. 用戶發起發消息業務請求,界面調用應用層轉賬操作(傳入參數為:標題,內容,發件人名稱,收件人名稱)

  2. 應用層首先創建一個發件人對象(通過倉儲獲得),然后再創建收件人對象和消息對象

  3. 發件人對象調用用戶實體中的 SendMessage 操作(參數為收件人對象和消息對象)

  4. 在發件人對象中的 SendMessage 方法中,收件人對象調用用戶實體中的 ReceiveMessage 操作(參數為發件人和消息對象)

  5. 在以上操作的完成后添加到事務操作(具體就是往消息倉儲中添加消息領域對象)

  6. 應用層提交事務,向界面返回確認。

  其實這種設計某種意義上也沒有什么問題,至少在短消息系統中,因為發消息本身就是用戶的一種行為,但是如果仔細一想就會覺得有些別扭,首先賬戶轉賬業務流程和發消息業務流程雖然表面上相似,但是其聚合的對象并不相同,比如賬戶轉賬示例中,聚合的是賬戶,發消息業務場景中如果這樣分析應該聚合的是用戶,但是很顯然并不是,聚合的是消息對象,如果聚合用戶就會變成用戶消息系統了,這就偏離了大方向,并不是我們所想看到的。還有就是現在的這種設計只適用于短消息業務場景,在郵件和短信業務場景中并不適用,為什么?因為郵件發送和短信發送并不存在用戶的概念(這個用戶概念并不是現實生活中的人,而是系統中的用戶,這個觀點很容易造成誤解),有的只是一個標識(電子郵件或手機號),用來體現出發件人和收件人的概念,也就是說這種標識并不是一個對象(沒有行為的對象),準確的來說應該不是一個實體,那是什么?在領域驅動設計中設計為值對象(為什么要設計成值對象,后面領域模型設計中進行說明),一個沒有行為的對象中加入行為操作,本身邏輯就存在問題,所以這種設計是有問題的。

  回到發消息這個業務用例上,一個消息對象存在意義的前提是擁有標題、內容、發送(標識)和接收人(標識),當然還存在一些選填元素,但是主要包含這四個元素,缺少任何一種,就不是一個完整的消息對象,也就是說不能用來發送。發送操作不僅僅是一個對象的行為,而應該是消息領域模型提供的一種服務,也就是領域服務,提供各種消息發送的服務,這一點很容易和基礎層的消息發送服務搞混,區分他們只需要記住一點:基礎層是技術上的實現,領域是業務上的抽象,因為這個業務場景是消息系統,那發消息就是一種業務用例,而并不是一個技術調用方法。

  說了這么多,總結一下所描述的消息業務場景:抽象所有消息業務邏輯(包含短消息郵件和短信等),應用具體的業務場景(比如短消息)。發消息業務用例:發送人(系統用戶)填寫消息,包含標題、內容、發送人(標識)、接收人(標識),調用(應用層發送請求)服務(領域服務)發送消息,相當于郵遞員投遞信件,就是這樣的一個過程,至少聽起來這么簡單,實現起來呢?我覺得那是另一方面的問題了,呵呵。

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 }
View Code

  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 }
View Code

  因為發送人和接收人都是聯系人的一種,只不過所扮演的角色不同,所以我們可以把他們抽象出來,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 }
View Code

  除了短消息發送服務,我們還可以實現郵箱發送服務(業務上的 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 項目開源地址:

  如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()