寫在前面
除了博文内容之外,和 netfocus 兄的讨論,也可以讓你學到很多(至少我是這樣),不要錯過哦。
閱讀目錄:
- 迷霧森林
- 找回自我
- 開源位址
- 後記
毫無疑問,領域驅動設計的核心是領域模型,領域模型的核心是實作業務邏輯,也就是說,在應對具體的業務場景的時候,實作業務邏輯是領域驅動設計最重要的一環,在寫這篇博文之前,先總結下之前關于 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 進行協調,然後應用層和就和領域服務通信了,然後的然後。。。
有朋友看到這,會覺得沒錯啊,就是這樣啊(如果你也這樣認為,那我就去幹傳銷了),先不讨論對錯,我們看下領域服務究竟實作的是個什麼東西?領域模型變成了什麼?應用層又變成了什麼?
領域服務代碼:
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
應用層代碼:
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 }
領域模型代碼:
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 是我們共同的語言,寫這篇博文的目的就是,希望園友們也可以看到希望,不要再像我一樣,迷失自我。
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空間
新浪微網誌
騰訊微網誌
微信
更多