天天看點

撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?

寫在前面

除了博文内容之外,和 netfocus 兄的讨論,也可以讓你學到很多(至少我是這樣),不要錯過哦。

撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?

  閱讀目錄:

  • 迷霧森林
  • 找回自我
  • 開源位址
  • 後記

  毫無疑問,領域驅動設計的核心是領域模型,領域模型的核心是實作業務邏輯,也就是說,在應對具體的業務場景的時候,實作業務邏輯是領域驅動設計最重要的一環,在寫這篇博文之前,先總結下之前關于 DDD(領域驅動設計)的三篇博文:

  • 我的“第一次”,就這樣沒了:DDD(領域驅動設計)理論結合實踐:僞領域驅動設計,隻是用 .NET 實作的一個“空殼”,僅此而已。
  • 一縷陽光:DDD(領域驅動設計)應對具體業務場景,如何聚焦 Domain Model(領域模型)?:隻是聚焦領域模型(認清各個部分的職責,讓設計的焦點集中在領域模型中),文中關于領域模型的實作就是一個“渣”,僅此而已。
  • 死去活來,而不變質:Domain Model(領域模型) 和 EntityFramework 如何正确進行對象關系映射?:走了個彎路,ORM 的映射關系及倉儲的實作,應該是在本篇内容之後探讨,原因都是腳本驅動模式惹的禍,如果說腳本驅動模式是惡魔(特定的環境,也有好處,不能一概而論,這邊隻是一個比喻),那領域驅動設計可以看作是天使,心裡想的是天使,卻聽了惡魔的話,為什麼?因為它在你心中已根深蒂固,僅此而已。

  OO(面向對象)設計指導我們要面向(停頓一下)對象設計,也就是說你要去面向這個對象,去思考它的本質,說白了就是思考:它是從哪裡來,到哪裡去,生長軌迹,擁有的事物,自身的變化規律,現在的狀态等等方面,更深入一點就是站在哲學的角度去思考萬物的本質(請參考老子的道德經),咳咳,說大了。

  但是,在現實生活中,我們是做軟體設計的程式員,需要做出東西給使用者用的,而不是像老子那樣逍遙的每天面對星空,去思考宇宙萬物(如果有興趣,你也可以試試,隻要不被你的女朋友罵死)。也就是說我們要面對具體的業務場景,而并非隻是單純的 OO,這就要求我們除了要 OO 之外,還要去探讨業務邏輯的本質以及實作。

  以下内容是本人掉的一個又一個深坑,拼死爬了上來,在 DDD 的道路上,友情提醒各位:前方有坑,園友們請小心謹慎。

  迷霧森林中,切勿迷失自我,不幸的是,我就這樣迷失了:

  具體的業務場景還是短消息系統-MessageManager,存在 Message 和 User 兩個領域模型,業務邏輯:一個使用者給另一個使用者發送消息,就是這麼簡單,可以看作是一個最簡單的業務邏輯,當然在發送消息這個過程中會有其他的業務邏輯,先不探讨 Message 領域模型和 User 領域模型如何協調完成這個業務邏輯,我們先看以下,我在第一篇《我的“第一次”,就這樣沒了:DDD(領域驅動設計)理論結合實踐》博文中,關于領域模型的實作:

1 /**
 2 * author:xishaui
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5 
 6 using System;
 7 
 8 namespace MessageManager.Domain.DomainModel
 9 {
10     public class Message : IAggregateRoot
11     {
12         #region 構造方法
13         public Message()
14         {
15             this.ID = Guid.NewGuid().ToString();
16         }
17         #endregion
18 
19         #region 實體成員
20         public string FromUserID { get; set; }
21         public string FromUserName { get; set; }
22         public string ToUserID { get; set; }
23         public string ToUserName { get; set; }
24         public string Title { get; set; }
25         public string Content { get; set; }
26         public DateTime SendTime { get; set; }
27         public bool IsRead { get; set; }
28         public virtual User FromUser { get; set; }
29         public virtual User ToUser { get; set; }
30         #endregion
31 
32         #region IEntity成員
33         /// <summary>
34         /// 擷取或設定目前實體對象的全局唯一辨別。
35         /// </summary>
36         public string ID { get; set; }
37         #endregion
38     }
39 }      

  Are you kidding me?不,你沒看錯,以上就是 Message 領域模型的實作代碼,User 領域模型的代碼我就不貼了,比這個還要簡單,隻包含 ID 和 Name 兩個字段屬性,領域驅動設計主張的是充血模型,隻包含字段屬性的領域模型是極其貧血的,像上面的 Message 領域模型,充血的領域模型實作的是業務邏輯。上面我們說的發送消息這個業務邏輯,在領域模型中為什麼沒有展現?既然是基于領域驅動設計,那為什麼我還要這樣設計呢?這是為什麼呢?當時設計完之後,我也在思考這個問題,難道腦袋有問題?不可能吧?看下應用層的代碼就知道了:

1 /**
  2 * author:xishuai
  3 * address:https://www.github.com/yuezhongxin/MessageManager
  4 **/
  5 
  6 using AutoMapper;
  7 using MessageManager.Application.DTO;
  8 using MessageManager.Domain.DomainModel;
  9 using MessageManager.Domain.Repositories;
 10 using System.Collections.Generic;
 11 
 12 namespace MessageManager.Application.Implementation
 13 {
 14     /// <summary>
 15     /// Message管理應用層接口實作
 16     /// </summary>
 17     public class MessageServiceImpl : ApplicationService, IMessageService
 18     {
 19         #region Private Fields
 20         private readonly IMessageRepository messageRepository;
 21         private readonly IUserRepository userRepository;
 22         #endregion
 23 
 24         #region Ctor
 25         /// <summary>
 26         /// 初始化一個<c>MessageServiceImpl</c>類型的執行個體。
 27         /// </summary>
 28         /// <param name="context">用來初始化<c>MessageServiceImpl</c>類型的倉儲上下文執行個體。</param>
 29         /// <param name="messageRepository">“消息”倉儲執行個體。</param>
 30         /// <param name="userRepository">“使用者”倉儲執行個體。</param>
 31         public MessageServiceImpl(IRepositoryContext context,
 32             IMessageRepository messageRepository,
 33             IUserRepository userRepository)
 34             : base(context)
 35         {
 36             this.messageRepository = messageRepository;
 37             this.userRepository = userRepository;
 38         }
 39         #endregion
 40 
 41         #region IMessageService Members
 42         /// <summary>
 43         /// 通過發送方擷取消息清單
 44         /// </summary>
 45         /// <param name="userDTO">發送方</param>
 46         /// <returns>消息清單</returns>
 47         public IEnumerable<MessageDTO> GetMessagesBySendUser(UserDTO sendUserDTO)
 48         {
 49             //User user = userRepository.GetUserByName(sendUserDTO.Name);
 50             var messages = messageRepository.GetMessagesBySendUser(Mapper.Map<UserDTO, User>(sendUserDTO));
 51             if (messages == null)
 52                 return null;
 53             var ret = new List<MessageDTO>();
 54             foreach (var message in messages)
 55             {
 56                 ret.Add(Mapper.Map<Message, MessageDTO>(message));
 57             }
 58             return ret;
 59         }
 60         /// <summary>
 61         /// 通過接受方擷取消息清單
 62         /// </summary>
 63         /// <param name="userDTO">接受方</param>
 64         /// <returns>消息清單</returns>
 65         public IEnumerable<MessageDTO> GetMessagesByReceiveUser(UserDTO receiveUserDTO)
 66         {
 67             //User user = userRepository.GetUserByName(receiveUserDTO.Name);
 68             var messages = messageRepository.GetMessagesByReceiveUser(Mapper.Map<UserDTO, User>(receiveUserDTO));
 69             if (messages == null)
 70                 return null;
 71             var ret = new List<MessageDTO>();
 72             foreach (var message in messages)
 73             {
 74                 ret.Add(Mapper.Map<Message, MessageDTO>(message));
 75             }
 76             return ret;
 77         }
 78         /// <summary>
 79         /// 删除消息
 80         /// </summary>
 81         /// <param name="messageDTO"></param>
 82         /// <returns></returns>
 83         public bool DeleteMessage(MessageDTO messageDTO)
 84         {
 85             messageRepository.Remove(Mapper.Map<MessageDTO, Message>(messageDTO));
 86             return messageRepository.Context.Commit();
 87         }
 88         /// <summary>
 89         /// 發送消息
 90         /// </summary>
 91         /// <param name="messageDTO"></param>
 92         /// <returns></returns>
 93         public bool SendMessage(MessageDTO messageDTO)
 94         {
 95             Message message = Mapper.Map<MessageDTO, Message>(messageDTO);
 96             message.FromUserID = userRepository.GetUserByName(messageDTO.FromUserName).ID;
 97             message.ToUserID = userRepository.GetUserByName(messageDTO.ToUserName).ID;
 98             messageRepository.Add(message);
 99             return messageRepository.Context.Commit();
100         }
101         /// <summary>
102         /// 檢視消息
103         /// </summary>
104         /// <param name="ID"></param>
105         /// <returns></returns>
106         public MessageDTO ShowMessage(string ID, string isRead)
107         {
108             Message message = messageRepository.GetByKey(ID);
109             if (isRead == "1")
110             {
111                 message.IsRead = true;
112                 messageRepository.Update(message);
113                 messageRepository.Context.Commit();
114             }
115             return Mapper.Map<Message, MessageDTO>(message);
116         }
117         #endregion
118     }
119 }      

  可以看到應用層的代碼真是不忍直視,撇開其他操作,我們看下 SendMessage 這個方法,首先 MessageDTO 這個參數就不應該存在,下面用 AutoMapper 進行對象轉化,然後再進行指派操作,這個過程就是典型的過程思維模式,沒有展現出一點的 OO 思想,指派完之後,使用 Repository(倉儲)進行持久話,發送消息的這個業務邏輯展現在哪?如果硬要說展現的話,那就是:messageRepository.Add(message) 這段代碼了,想想當時無知的認為,發送消息的業務邏輯展現就是持久化資料庫,還真是可笑。

  在這篇博文發表後,很多園友也都意識到了這個問題,什麼問題?主要是以下兩個:

  • Domain Model(領域模型):領域模型到底該怎麼設計?你會看到,MessageManager 項目中的 User 和 Message 領域模型是非常貧血的,沒有包含任何的業務邏輯,現在網上很多關于 DDD 示例項目多數也存在這種情況,當然項目本身沒有業務,隻是簡單的“CURD”操作,但是如果是一些大型項目的複雜業務邏輯,該怎麼去實作?或者說,領域模 型完成什麼樣的業務邏輯?什麼才是真正的業務邏輯?這個問題很重要,後續探讨。
  • Application(應用層):應用層作為協調服務層,當遇到複雜性的業務邏輯時,到底如何實作,而不使其變成 BLL(業務邏輯層)?認清本質很重要,後續探讨。

  簡而言之就是:領域模型太貧血;應用層變成了業務邏輯層。意識到問題,那就找問題所在,經過一番探查,把 Repository 作為了重點懷疑對象,為什麼?主要是我當時以為 Repository 的職責有問題,也就有了下面的這篇博文《一縷陽光:DDD(領域驅動設計)應對具體業務場景,如何聚焦 Domain Model(領域模型)?》,在這篇博文中,關于上面原因的分析,主要講到了以下兩個節點的内容:

  • Repository(倉儲)職責所在?
  • Domain Service(領域服務)的加入

  雖然博文中也講到了領域模型的重新設計,但是設計之後還是一坨屎,這邊就不拿出來誤導大家了。回到上面的問題,關于 Repository 的職責問題,我當時是這樣分析的:

Repository 應用在應用層,這樣就緻使應用層和基礎層(我把資料持久化放在基礎層了)通信,忽略了最重要的領域層,領域層在其中起到的作用最多也就是傳遞一個非常貧血的領域模型,然後通過 Repository 進行“CRUD”,這樣的結果是,應用層不變成所謂的 BLL(常說的業務邏輯層)才怪,另外,因為業務邏輯都放在應用層了,領域模型也變得更加貧血。

  乍一看,上面的分析還真沒什麼問題(看來我還是蠻會忽悠人的,嘿嘿),Repository 服務于領域,是以就必須把 Repository 的調用放在領域層中,領域模型又不能直接和 Repository 通信,是以我後來就把 Domain Service(領域服務)加了進來,讓領域服務和 Repository 進行協調,然後應用層和就和領域服務通信了,然後的然後。。。

  有朋友看到這,會覺得沒錯啊,就是這樣啊(如果你也這樣認為,那我就去幹傳銷了),先不讨論對錯,我們看下領域服務究竟實作的是個什麼東西?領域模型變成了什麼?應用層又變成了什麼?

  領域服務代碼:

撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?
撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?
1 /**
 2 * author:xishuai
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5 
 6 using MessageManager.Domain.DomainModel;
 7 using MessageManager.Domain.Repositories;
 8 using System.Collections.Generic;
 9 
10 namespace MessageManager.Domain.DomainService
11 {
12     /// <summary>
13     /// Message領域服務實作
14     /// </summary>
15     public class MessageDomainService : IMessageDomainService
16     {
17         #region Private Fields
18         private readonly IMessageRepository messageRepository;
19         private readonly IUserRepository userRepository;
20         #endregion
21 
22         #region Ctor
23         public MessageDomainService(IMessageRepository messageRepository, IUserRepository userRepository)
24         {
25             this.messageRepository = messageRepository;
26             this.userRepository = userRepository;
27         }
28         #endregion
29 
30         #region IMessageDomainService Members
31         public bool DeleteMessage(Message message)
32         {
33             messageRepository.Remove(message);
34             return messageRepository.Context.Commit();
35         }
36         public bool SendMessage(Message message)
37         {
38             message.LoadUserName(userRepository.GetUser(new User { Name = message.FromUserName })
39                 , userRepository.GetUser(new User { Name = message.ToUserName }));
40             messageRepository.Add(message);
41             return messageRepository.Context.Commit();
42         }
43         public Message ShowMessage(string id, User currentUser)
44         {
45             Message message = messageRepository.GetByKey(id);
46             message.ReadMessage(userRepository.GetUser(new User { Name = currentUser.Name }));
47             messageRepository.Update(message);
48             messageRepository.Context.Commit();
49             return message;
50         }
51         public IEnumerable<Message> GetMessagesBySendUser(User user)
52         {
53             User userResult = userRepository.GetUser(user);
54             return messageRepository.GetMessagesBySendUser(userResult);
55         }
56         public IEnumerable<Message> GetMessagesByReceiveUser(User user)
57         {
58             User userResult = userRepository.GetUser(user);
59             return messageRepository.GetMessagesByReceiveUser(userResult);
60         }
61         public int GetNoReadCount(User user)
62         {
63             User userResult = userRepository.GetUser(user);
64             return messageRepository.GetNoReadCount(userResult);
65         }
66         #endregion
67     }
68 }      

View Code

  應用層代碼:

撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?
撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?
1 /**
  2 * author:xishuai
  3 * address:https://www.github.com/yuezhongxin/MessageManager
  4 **/
  5 
  6 using AutoMapper;
  7 using MessageManager.Application.DTO;
  8 using MessageManager.Domain.DomainModel;
  9 using MessageManager.Domain.DomainService;
 10 using System.Collections.Generic;
 11 
 12 namespace MessageManager.Application.Implementation
 13 {
 14     /// <summary>
 15     /// Message管理應用層接口實作
 16     /// </summary>
 17     public class MessageServiceImpl : ApplicationService, IMessageService
 18     {
 19         #region Private Fields
 20         private readonly IMessageDomainService messageService;
 21         #endregion
 22 
 23         #region Ctor
 24         /// <summary>
 25         /// 初始化一個<c>MessageServiceImpl</c>類型的執行個體。
 26         /// </summary>
 27         /// <param name="messageRepository">“消息”服務執行個體。</param>
 28         public MessageServiceImpl(IMessageDomainService messageService)
 29         {
 30             this.messageService = messageService;
 31         }
 32         #endregion
 33 
 34         #region IMessageService Members
 35         /// <summary>
 36         /// 通過發送方擷取消息清單
 37         /// </summary>
 38         /// <param name="userDTO">發送方</param>
 39         /// <returns>消息清單</returns>
 40         public IEnumerable<MessageDTO> GetMessagesBySendUser(UserDTO sendUserDTO)
 41         {
 42             //User user = userRepository.GetUserByName(sendUserDTO.Name);
 43             var messages = messageService.GetMessagesBySendUser(Mapper.Map<UserDTO, User>(sendUserDTO));
 44             if (messages == null)
 45                 return null;
 46             var ret = new List<MessageDTO>();
 47             foreach (var message in messages)
 48             {
 49                 ret.Add(Mapper.Map<Message, MessageDTO>(message));
 50             }
 51             return ret;
 52         }
 53         /// <summary>
 54         /// 通過接受方擷取消息清單
 55         /// </summary>
 56         /// <param name="userDTO">接受方</param>
 57         /// <returns>消息清單</returns>
 58         public IEnumerable<MessageDTO> GetMessagesByReceiveUser(UserDTO receiveUserDTO)
 59         {
 60             //User user = userRepository.GetUserByName(receiveUserDTO.Name);
 61             var messages = messageService.GetMessagesByReceiveUser(Mapper.Map<UserDTO, User>(receiveUserDTO));
 62             if (messages == null)
 63                 return null;
 64             var ret = new List<MessageDTO>();
 65             foreach (var message in messages)
 66             {
 67                 ret.Add(Mapper.Map<Message, MessageDTO>(message));
 68             }
 69             return ret;
 70         }
 71         /// <summary>
 72         /// 删除消息
 73         /// </summary>
 74         /// <param name="messageDTO"></param>
 75         /// <returns></returns>
 76         public bool DeleteMessage(MessageDTO messageDTO)
 77         {
 78             return messageService.DeleteMessage(Mapper.Map<MessageDTO, Message>(messageDTO));
 79         }
 80         /// <summary>
 81         /// 發送消息
 82         /// </summary>
 83         /// <param name="messageDTO"></param>
 84         /// <returns></returns>
 85         public bool SendMessage(MessageDTO messageDTO)
 86         {
 87             return messageService.SendMessage(Mapper.Map<MessageDTO, Message>(messageDTO));
 88         }
 89         /// <summary>
 90         /// 檢視消息
 91         /// </summary>
 92         /// <param name="ID"></param>
 93         /// <returns></returns>
 94         public MessageDTO ShowMessage(string id, UserDTO currentUserDTO)
 95         {
 96             Message message = messageService.ShowMessage(id, Mapper.Map<UserDTO, User>(currentUserDTO));
 97             return Mapper.Map<Message, MessageDTO>(message);
 98         }
 99         /// <summary>
100         /// 擷取未讀消息數
101         /// </summary>
102         /// <param name="user"></param>
103         /// <returns></returns>
104         public int GetNoReadCount(UserDTO userDTO)
105         {
106             return messageService.GetNoReadCount(Mapper.Map<UserDTO, User>(userDTO));
107         }
108         #endregion
109     }
110 }      

  領域模型代碼:

撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?
撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?
1 /**
 2 * author:xishaui
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5 
 6 using System;
 7 
 8 namespace MessageManager.Domain.DomainModel
 9 {
10     public class Message : IAggregateRoot
11     {
12         #region 構造方法
13         public Message()
14         {
15             this.ID = Guid.NewGuid().ToString();
16         }
17         #endregion
18 
19         #region 實體成員
20         public string FromUserID { get; set; }
21         public string FromUserName { get; set; }
22         public string ToUserID { get; set; }
23         public string ToUserName { get; set; }
24         public string Title { get; set; }
25         public string Content { get; set; }
26         public DateTime SendTime { get; set; }
27         public bool IsRead { get; set; }
28         public virtual User FromUser { get; set; }
29         public virtual User ToUser { get; set; }
30         #endregion
31 
32         #region 業務邏輯
33         /// <summary>
34         /// 閱讀消息
35         /// </summary>
36         /// <param name="CurrentUser"></param>
37         public void ReadMessage(User currentUser)
38         {
39             if (!this.IsRead && currentUser.ID.Equals(ToUserID))
40             {
41                 this.IsRead = true;
42             }
43         }
44         /// <summary>
45         /// 加載使用者
46         /// </summary>
47         /// <param name="sendUser"></param>
48         /// <param name="receiveUser"></param>
49         public void LoadUserName(User sendUser, User receiveUser)
50         {
51             this.FromUserID = sendUser.ID;
52             this.ToUserID = receiveUser.ID;
53         }
54         #endregion
55 
56         #region IEntity成員
57         /// <summary>
58         /// 擷取或設定目前實體對象的全局唯一辨別。
59         /// </summary>
60         public string ID { get; set; }
61         #endregion
62     }
63 }      

  其實上面代碼,如果是 DDD 大神來看的話,他隻要看領域模型中的代碼就行了,因為領域驅動設計的核心就是領域模型,那領域模型變成了什麼?隻是添加了 ReadMessage 和 LoadUserName 兩個不是業務邏輯的業務邏輯方法(因為隻有他們兩個,如果把他們兩個去掉,就變回原來的貧血模型了,是以,你懂的),領域服務中的 SendMessage 方法變的和原來的應用層代碼一樣,要說變化的話,隻是把 Application 單詞變成了 Domain Service 這個單詞,其他無任何變化,應用層的代碼也就變成了下面這樣:

1         /// <summary>
2         /// 發送消息
3         /// </summary>
4         /// <param name="messageDTO"></param>
5         /// <returns></returns>
6         public bool SendMessage(MessageDTO messageDTO)
7         {
8             return messageService.SendMessage(Mapper.Map<MessageDTO, Message>(messageDTO));
9         }      

  在領域驅動設計中,應用層的定義是很薄的一層,可以看到,上面的應用層代碼也未免太薄了吧,為什麼?因為原來它的工作讓領域服務做了,導緻現在變成了一個調用外殼(也就是可有可無的東西,沒有任何意義)。

》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》分割線《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《

  為什麼會有分割線?因為在應對具體業務場景中,上面做的操作都是無用功,為什麼?因為設計的領域模型中什麼東西都沒有(指的是業務邏輯),沒有任何東西的領域模型,還是真正的領域驅動設計嗎?關于這個問題,傻子都知道,當然我也不傻,嘿嘿。

  認識到這個根本問題後,下面就抛開一切外在因素,比如領域服務、倉儲、應用層、表現層等等,這些統統不管,隻做領域模型的設計,讓真正的設計焦點集中在領域模型上,然後再針對領域模型做單元測試。

  對,就是這麼簡單,至少聽起來的确簡單,事實真是這樣嗎?我卻不這樣認為,因為就是這個簡單的問題,我為此痛苦了兩三天的時間,說誇張點就是:吃不下飯,睡不着覺,在這個過程中,領域模型的一行代碼我也沒有寫,不是不想寫,而是不知道如何寫?這是最最痛苦的,真是體會到了才知道。寫不出來怎麼辦?我就找遍網上所有關于領域模型設計的資料(大部分都是英文,隻能很痛苦的看),還有《領域驅動設計》和《企業應用架構模式》這兩本書,希望能從中找到些靈感(就像畫畫,難點就在如何畫第一筆),并不是模仿,這個也模仿不來,因為每個業務場景都不相同,遺憾的是沒有找到任何的靈感,唯一找到的線索就是 OO 設計(大家都知道,我卻不知道,因為蒙了)。

  既然是要面向對象,那就分析一下對象,主要包含兩個:使用者和消息。對象擁有自身的屬性、狀态和行為,發送消息是使用者的一種行為,是以發送消息這個操作應該放在使用者中,那現在消息隻有自身的一些屬性值,因為在面向對象中,它是固定的,隻有通過使用者來調用它,使用者領域模型代碼如下:

1 /**
 2 * author:xishaui
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5 
 6 using MessageManager.Domain.Demo.V1.Event;
 7 using System;
 8 using System.Collections.Generic;
 9 
10 namespace MessageManager.Domain.Demo.V1
11 {
12     public class User
13     {
14         public User()
15         {
16             this.ID = Guid.NewGuid().ToString();
17         }
18 
19         public string ID { get; set; }
20         public string Name { get; set; }
21         public virtual ICollection<Message> SendMessages { get; set; }
22         public virtual ICollection<Message> ReceiveMessages { get; set; }
23 
24         public void SendMessage(Message message)
25         {
26             User toUser = GetUser(message.ToUser);
27             if (toUser == null)
28             {
29                 throw new NotImplementedException();
30             }
31             message.FromUser = this;
32             message.ToUser = toUser;
33             this.SendMessages.Add(message);
34             toUser.ReceiveMessages.Add(message);
35             DomainEvents.Raise(new MessageEvent() { DoMessage = message });
36             ///
37         }
38     }
39 }      

  按照面向對象設計,消息是使用者的附屬對象,隻有使用者存在,消息才有意義,一個使用者對象擁有多個消息的對象集合,那怎麼展現出發送消息這個動作呢?答案就是:this.SendMessages.Add(message) 這段代碼,表示往使用者對象的消息集合填充消息對象,這樣就會相對于使用者對象來說,這條消息的發送動作就完成了。先不考慮這樣設計的合理或者不合理,我們看下消息模型中的代碼,就會發現裡面隻有一些字段屬性,沒有任何的操作,還有就是如果我們要添加消息的其他動作,比如查找,删除等等,按照上面的分析,我們就會在使用者對象中添加這些操作,因為這些動作都是使用者所具有的,合理嗎?至少聽起來就不合理。

  身處這個迷霧森林,才知道它的恐怖之處,不斷的迷失自我,以緻最後可能連自己都不相信,并懷疑自己。

  在迷霧森林之中,如何找回自我?而不迷失,沒有确切的答案,我隻能尋覓那一縷陽光一步一步的往前行。。。

  首先,Repository,和你說聲抱歉,非常抱歉,讓你蒙冤,是我誤會你了,因為我對業務邏輯的不了解,以緻做出錯誤的做法。

  回到短消息系統-MessageManager,需要注意的是,我們做的是消息系統,一切的一切都應該圍繞 Message 領域模型展開,在這個系統中,最重要的就是發送消息這個業務邏輯,什麼叫發消息?不要被上面的面向對象所迷惑,隻考慮發消息這個具體的業務,我們來分析一下:比如在現實生活中,我們要給女朋友寫信,首先我們要寫信的内容,寫完之後,要寫一下女朋友的位址資訊及名字,這個寫信才算完成,郵差郵遞并不在這個業務邏輯之内了,因為這封信我寫上收件人之後,這封信相對于我來說就已經發出了,後面隻不過是收件人收不收得到的問題了(即使我寫好,沒有寄出去)。也就是說郵差郵遞這個工作過程相當于資料的持久化,寫信的這個過程就是郵遞(發消息的業務邏輯),just it。

  了解上面的内容很重要,然後我們再來看 Message 這個領域模型,建立這個對象的時候,就說明我們已經把消息的内容寫好了,也就是必要的東西,比如:消息标題、消息内容、發送人、發送時間等等,用代碼實作就是在 Message 領域模型中的構造函數傳遞必要值,代碼如下:

1 /**
 2 * author:xishuai
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5 
 6 using System;
 7 
 8 namespace MessageManager.Domain.DomainModel
 9 {
10     public class Message : IAggregateRoot
11     {
12         public Message(string title, string content, User sendUser)
13         {
14             if (title.Equals("") || content.Equals("") || sendUser == null)
15             {
16                 throw new ArgumentNullException();
17             }
18             this.ID = Guid.NewGuid().ToString();
19             this.Title = title;
20             this.Content = content;
21             this.SendTime = DateTime.Now;
22             this.State = MessageState.NoRead;
23             this.SendUser = sendUser;
24         }
25         public string ID { get; set; }
26         public string Title { get; set; }
27         public string Content { get; set; }
28         public DateTime SendTime { get; set; }
29         public MessageState State { get; set; }
30         public virtual User SendUser { get; set; }
31         public virtual User ReceiveUser { get; set; }
32 
33         public bool Send(User receiveUser)
34         {
35             if (receiveUser == null)
36             {
37                 throw new ArgumentNullException();
38             }
39             this.ReceiveUser = receiveUser;
40             return true;
41             ///to do...
42         }
43     }
44 }      

  在執行個體 Message 領域模型之前要對必要值進行判斷,發送消息的關鍵代碼就是:ReceiveUser = receiveUser,表示為這條消息“貼上”收件人的标簽,訓示這條消息的發送動作已經完成,當然在這個 Send 業務方法中可能還會有其他業務邏輯的加入,其實發送消息就是這樣,在 Message 這個領域模型中,沒有什麼資料庫的概念,隻是描述這個業務功能,僅此而已。

  在領域驅動設計的過程中,你會忘記資料庫的存在,使用接口注入,我們可以想怎麼操作就怎麼操作,資料庫隻是業務場景中資料的存儲的一種方式,這個工作應該是你做完所有的業務設計之後執行,如果想進行單元測試,使用 IRepository 接口,我們甚至可以虛拟一切想要的對象(是對象,不是資料值)。

  我們再來看下應用層的實作:

1 /**
 2 * author:xishuai
 3 * address:https://www.github.com/yuezhongxin/MessageManager
 4 **/
 5 
 6 using MessageManager.Domain.DomainModel;
 7 using MessageManager.Domain.Repositories;
 8 
 9 namespace MessageManager.Application.Implementation
10 {
11     /// <summary>
12     /// Message管理應用層接口實作
13     /// </summary>
14     public class MessageServiceImpl : ApplicationService, IMessageService
15     {
16         #region Private Fields
17         private readonly IMessageRepository messageRepository;
18         private readonly IUserRepository userRepository;
19         #endregion
20 
21         #region Ctor
22         /// <summary>
23         /// 初始化一個<c>MessageServiceImpl</c>類型的執行個體。
24         /// </summary>
25         /// <param name="context">用來初始化<c>MessageServiceImpl</c>類型的倉儲上下文執行個體。</param>
26         /// <param name="messageRepository">“消息”倉儲執行個體。</param>
27         /// <param name="userRepository">“使用者”倉儲執行個體。</param>
28         public MessageServiceImpl(IRepositoryContext context,
29             IMessageRepository messageRepository,
30             IUserRepository userRepository)
31             : base(context)
32         {
33             this.messageRepository = messageRepository;
34             this.userRepository = userRepository;
35         }
36         #endregion
37 
38         #region IMessageService Members
39         /// <summary>
40         /// 發送消息
41         /// </summary>
42         /// <param name="title">消息标題</param>
43         /// <param name="content">消息内容</param>
44         /// <param name="sendLoginUserName">發送人-登陸名</param>
45         /// <param name="receiveDisplayUserName">接受人-接收人</param>
46         /// <returns></returns>
47         public bool SendMessage(string title, string content, string sendLoginUserName, string receiveDisplayUserName)
48         {
49             User sendUser = userRepository.GetUserByLoginName(sendLoginUserName);
50             if (sendUser == null)
51             {
52                 return false;
53             }
54             Message message = new Message(title, content, sendUser);
55             User receiveUser = userRepository.GetUserByDisplayName(receiveDisplayUserName);
56             if (receiveUser == null)
57             {
58                 return false;
59             }
60             if (message.Send(receiveUser))
61             {
62                 return true;
63                 //messageRepository.Add(message);
64                 //return messageRepository.Context.Commit();
65             }
66             else
67             {
68                 return false;
69             }
70         }
71         #endregion
72     }
73 }      

  你會發現應用層中的 SendMessage 方法,所做的工作流程是多麼的行雲流水,一步一步的協調(不要誤讀為業務邏輯,我之前就是這樣),完美的完成這個發送消息的請求。應用層的核心是什麼?答案就是協調,什麼意思?就是說 UI 發出一個請求(比如發消息),然後應用層接到這這個請求之後,進行一些協調處理(比如取想要的使用者值,完成發送,以及發送之後的流程-發郵件等等),完成這個工作流程,而并不是這個業務邏輯,業務邏輯是發消息。

  在代碼注釋的地方,完成的是消息的持久化操作,當然我們也可以不完成這個操作,因為業務邏輯是業務邏輯,持久化是持久化,并沒有半毛錢關系,我們描述的隻是業務場景,僅此而已。

  • 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/小菜

撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?

  以上隻是一個簡單業務場景用例,就讓我在迷霧森林中迷失自我這麼久,到現在隻是看到了那一縷陽光而已。DDD 是我們共同的語言,寫這篇博文的目的就是,希望園友們也可以看到希望,不要再像我一樣,迷失自我。

  DDD(領域驅動設計)的過程,從上面一系列的問題更加證明是個疊代過程,一次一次的否決自己,然後再找回自己,反反複複,複複反反,才能成就真正的自己。可能幾天或者幾周後,看現在的這篇博文就像一坨屎一樣,但是沒關系,因為我又離真相更進了一步。

  我喜歡這個挑戰,我也會堅持的完成它,誰叫我熱愛它呢,just so so。

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

  貼一下,當時尋找的領域模型設計資料,僅作參考:

  • https://github.com/sliedig/Employing-the-Domain-Model-Pattern
  • http://msdn.microsoft.com/zh-cn/magazine/ee236415.aspx
  • http://www.cnblogs.com/1-2-3/category/109191.html
  • http://www.cnblogs.com/yimlin/archive/2006/06/15/426929.html
  • http://www.blogjava.net/AndersLin/archive/2006/10/09/74187.html
  • http://www.blogjava.net/AndersLin/archive/2006/08/25/65875.html

作者:田園裡的蟋蟀

微信公衆号:你好架構

出處:http://www.cnblogs.com/xishuai/

公衆号會不定時的分享有關架構的方方面面,包含并不局限于:Microservices(微服務)、Service Mesh(服務網格)、DDD/TDD、Spring Cloud、Dubbo、Service Fabric、Linkerd、Envoy、Istio、Conduit、Kubernetes、Docker、MacOS/Linux、Java、.NET Core/ASP.NET Core、Redis、RabbitMQ、MongoDB、GitLab、CI/CD(持續內建/持續部署)、DevOps等等。

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接。

分享到:

QQ空間

新浪微網誌

騰訊微網誌

微信

更多

繼續閱讀