一縷陽光:DDD(領域驅動設計)應對具體業務場景,如何聚焦 Domain Model(領域模型)?

寫在前面

  閱讀目錄:html

  在上一篇《個人「第一次」,就這樣沒了:DDD(領域驅動設計)理論結合實踐》博文中,簡單介紹了領域驅動設計的一些理念,並簡單完成基於領域驅動設計的具體項目 MessageManager,本人在設計 MessageManager 項目以前,並無看過 Eric Evans 的《Domain-Driven Design –Tackling Complexity in the Heart of Software》和 Martin Fowler 的《Patterns of Enterprise Application Architecture》,《企業應用架構模式》這本書正在閱讀,關於領域驅動設計,主要學習來源是園中的 netfocus、dax.net、以及清培兄的部分博文(小弟先在此謝過各位大神的無私奉獻),還有就是解道中的領域驅動設計專題,固然還有一些來自搜索引擎的部分資料,再加上本身的一些揣摩和理解,也就成爲了屬於本身的「領域驅動設計」。git

  MessageManager 項目是對本身所理解領域驅動設計的檢驗,若是你仔細看過上一篇博文,你會發現 MessageManager 其實只是領域驅動設計的「外殼」,就像咱們種黃瓜,首先要搭一個架子,以便黃瓜的生長,MessageManager 項目就至關於這個架子,核心的東西「黃瓜」並不存在,當時在設計完 MessageManager 項目的時候,其實已經發現問題的存在,因此在博文最後留下了下面兩個問題:程序員

  • Domain Model(領域模型):領域模型到底該怎麼設計?你會看到,MessageManager 項目中的 User 和 Message 領域模型是很是貧血的,沒有包含任何的業務邏輯,如今網上不少關於 DDD 示例項目多數也存在這種狀況,固然項目自己沒有業務,只是簡單的「CURD」操做,可是若是是一些大型項目的複雜業務邏輯,該怎麼去實現?或者說,領域模型完成什麼樣的業務邏輯?什麼纔是真正的業務邏輯?這個問題很重要,後續探討。
  • Application(應用層):應用層做爲協調服務層,當遇到複雜性的業務邏輯時,到底如何實現,而不使其變成 BLL(業務邏輯層)?認清本質很重要,後續探討。

  另外再貼一些園友們在上一篇的問題評論:github

  關於以上的問題,本篇博文只是作一些解讀,但願能夠對那些癡迷於領域驅動設計的朋友們一些啓示,寫得有不當之處,也歡迎指出。數據庫

問題根源是什麼?

  出現上述問題的緣由是什麼?需求簡單?設計不合理?準確的來講,應該都不是,我以爲問題的根源是沒有真正去理解領域驅動設計,或者說沒有真正用領域驅動設計的理念去設計或實現,領域驅動設計的概念網上一找一大堆,你看過幾篇文章以後也能寫出來之類的文章,爲何?由於都是泛泛之談,諸如:領域模型是領域驅動的核心;領域驅動基本分爲四層(用戶層、應用層、領域層和基礎層);領域包含實體、值對象和服務;還有一些聚合和聚合根之類的概念等等,文中也會給你列出一些關於這些概念的代碼實現,讓你瞬間感受原來領域驅動設計是這麼的高大上。設計模式

  但若是拿這些概念去實踐呢?卻根本不是那麼回事,如今使用領域驅動設計去開發的企業實在是太少了,緣由有不少種,下面大體列出一些:api

  • 開發成本過高,換句話說,就是若是使用領域驅動設計開發,須要聘請高級程序員、高級架構師和建模專家,通常這種開發人員薪資都比較高,老闆真的捨得嗎?
  • 開發週期長,花在需求分析的時間比較長,甚至比程序實現還要長,這個對老闆來講是要命的,開發週期長,通常會意味着公司的利潤下降,公司利潤下降,老闆的錢包就癟了,老闆會願意嗎?
  • 開發思惟轉變問題,使用領域驅動設計開發,須要公司裏的程序員懂得領域驅動設計,要對面向對象(OO)設計有必定的理解,現實狀況是,大部分的程序員雖然使用的是面嚮對象語言(好比 Java、C#),卻作着面向過程的事(相似 C 語言函數式的開發)。如今讓公司的程序員使用領域驅動設計開發,就比如之前是用手直接吃飯,如今讓你使用筷子吃飯,你會習慣嗎?這須要一種轉變,不少程序員會很不習慣,這也是領域驅動設計推行難的主要緣由。
  • 關於領域驅動設計實踐經驗實在太少,你們腦子中只有模模糊糊的概念,卻沒有實實在在的實踐,像 dax.net 這樣去完成幾個完整基於領域驅動設計項目的大神實在太少了,不少都是像我同樣,理解一些概念後,放出一個簡單的示例 Demo,而後就沒有而後了。

  Eric Evans 在2004年提出 DDD(領域驅動設計)的理念,距今已經十年了,推廣卻停滯不前,確實值得咱們程序員去反思。架構

  扯得有點遠了,回到這個副標題:問題的根源是什麼?答案或許會不令你滿意,就是沒有真正理解領域驅動設計。那你或許會問:那真正的領域驅動設計是什麼?這個我想只有 Eric Evans 能夠回答,但也不要把領域驅動設計看得這麼絕對,領域驅動設計只是一種指導,具體的實現要用具體的方法,正若有句古話:師傅領進門,修行在我的。每一個人有每一個人的具體悟道,但再變化也不要忘了師出同門。併發

  還有一點就是,有朋友指出簡單的業務邏輯是體現不出領域驅動設計的,關於這一點首先我是比較贊同的,但若是去拿一些大型業務場景去作領域驅動設計的示例,我我的以爲也不太現實,畢竟時間成本過高了。我我的認爲小的業務場景和大的業務場景均可以使用領域驅動設計實現,只是業務邏輯的複雜度不一樣,還有就是適用度也不一樣,小的業務場景用腳本驅動模式去實現,可能會比領域驅動設計區實現更簡單、快速,可是但凡是業務場景(不論大小),必然包含業務邏輯(CRUD 除外),那也就可使用領域驅動設計去開發,仍是那句話,只是不太適合,但作演示示例仍是能夠的。app

  業務邏輯的複雜度主要體如今領域模型中,複雜性的業務邏輯,領域模型也就越複雜,但與簡單性的領域模型實質是同樣的。關於如何真正理解領域驅動設計?這一點我我的以爲方式就是「迭代」,只有不斷的去實踐,不斷的去體會,才能真正的去理解領域驅動設計,就像 MessageManager 項目,每一次有些體會我就會以爲這樣作不合理,那就推倒重建,可能這樣作又不合理,那就推倒再重建。。。

  閒話少說,來看這一次的「迭代」:

《領域驅動設計-軟件核心複雜性應對之道》分層概念

  注:這一節點是我後面添加的,真是天意,在我寫這篇博客的時候,正好有位不知名的朋友,發消息說他看到我以前的一篇博文,我在文中跪求《領域驅動設計-軟件核心複雜性應對之道》這本書,由於網上沒得買。正好他有 Word 版,雖然內容有些錯別字,可是真心感謝這位不知名的朋友。大體閱讀了下目錄結構,確實是我想要的,接下來會認真的拜讀,有實質書的話固然更好,下面是摘自這本書的分層概念。

  在面向對象的程序中,用戶界面(UI)、數據庫和其餘支持代碼,常常被直接寫到業務對象中去。在UI和數據庫腳本的行爲中嵌入額外的業務邏輯。出現這種狀況是由於從短時間的觀點看,它是使系統運行起來的最容易的方式。當與領域相關的代碼和大量的其餘代碼混在一塊兒時,就很難閱讀並理解了。對UI的簡單改動就會改變業務邏輯。改變業務規則可能須要當心翼翼地跟蹤UI代碼、數據庫代碼或者其餘的程序元素。實現一致的模型驅動對象變得不切實際,並且自動化測試也難以使用。若是在程序的每一個行爲中包括了全部的技術和邏輯,那麼它必須很簡單,不然會難以理解。

  將一個複雜的程序進行層次劃分。爲每一層進行設計,每層都是內聚的並且只依賴於它的下層。採用標準的架構模式來完成與上層的鬆散關聯。將全部與領域模型相關的代碼都集中在一層,而且將它與用戶界面層、應用層和基礎結構層的代碼分離。領域對象能夠將重點放在表達領域模型上,不須要關心它們本身的顯示、存儲和管理應用任務等內容。這樣使模型發展得足夠豐富和清晰,足以抓住本質的業務知識並實現它。

用戶界面層(表示層) 負責向用戶顯示信息,而且解析用戶命令。外部的執行者有時可能會是其餘的計算機系統,不必定非是人。
應用層 定義軟件能夠完成的工做,而且指揮具備豐富含義的領域對象來解決問題。這個層所負責的任務對業務影響深遠,對跟其餘系統的應用層進行交互很是必要這個層要保持簡練。它不包括處理業務規則或知識,只是給下一層中相互協做的領域對象協調任務、委託工做。在這個層次中不反映業務狀況的狀態,但反映用戶或程序的任務進度的狀態
領域層(模型層) 負責表示業務概念、業務情況的信息以及業務規則。儘管保存這些內容的技術細節由基礎結構層來完成,反映業務情況的狀態在該層中被控制和使用。這一層是業務軟件的核心。
基礎結構層 爲上層提供通用的技術能力:應用的消息發送、領域持久化,爲用戶界面繪製窗口等。經過架構框架,基礎結構層還能夠支持這四層之間的交互模式。

  一個對象所表明的事物是一個具備連續性和標識的概念(能夠跟蹤該事物經歷的不一樣的狀態,甚至可讓該事物跨越不一樣的實現),仍是隻是一個用來描述事物的某種狀態的屬性?這就是實體與值對象最基本的區別。明確地選用這兩種模式中的一種來定義對象,可使對象的意義更清晰,並能夠引導咱們構造出一個健壯的設計。

  另外,領域中還存在不少的方面,若是用行爲或操做來描述它們會比用對象來描述更加清晰。儘管與面向對象建模理念稍有抵觸,但這些最好是用服務來描述,而不是將這個操做的職責強加到某些實體或值對象身上。服務用來爲客戶請求提供服務。在軟件的技術層中就有許多服務。服務也會在領域中出現,它們用於對軟件必須完成的一些活動進行建模,可是與狀態無關。有時咱們必須在對象模型中釆取一些折衷的措施——這是不可避免的,例如利用關係數據庫進行存儲時就會出現這種狀況。本章將會給出一些規則,當遇到這種複雜狀況時,遵照這些規則可使咱們保持正確的方向。

  最後,咱們對模塊(Module)的討論能夠幫助理解這樣的觀點:每一個設計決策都應該是根據對領域的正確理解來作出。高內聚、低關聯這種思想每每被當作是理想的技術標準,它們對於概念自己也是適用的。在模型驅動的設計中,模塊是模型的一部分,它們應該可以反映出領域中的概念。

Repository(倉儲)職責所在?

  言歸正題。

  Repository(倉儲)的概念能夠參考:http://www.cnblogs.com/dudu/archive/2011/05/25/repository_pattern.html,我我的比較贊同 dudu 的理解:Repository 是一個獨立的層,介於領域層與數據映射層(數據訪問層)之間。它的存在讓領域層感受不到數據訪問層的存在,它提供一個相似集合的接口提供給領域層進行領域對象的訪問。Repository 是倉庫管理員,領域層須要什麼東西只需告訴倉庫管理員,由倉庫管理員把東西拿給它,並不須要知道東西實際放在哪。

  關於 Repository 的定義,在《企業應用架構模式》書中也有說明:協調領域和數據映射層,利用相似於集合的接口來訪問領域對象。書中把 Repository 翻譯爲資源庫,實際上是和倉儲是一個意思,關於 Repository 這一節點的內容,我大概閱讀了兩三篇才理解了部份內容(這本書比較抽象難理解,須要多讀幾遍,而後根據本身的理解進行推敲揣摩),文中也給出了一個示例:查找一我的所在的部門(Java),以便於加深對 Repository 的理解。

  咱們先看一下 Repository 的定義前半句:協調領域和數據映射層,也就是 dudu 所說的介於領域層和數據映射層之間,理解這一點很重要,很是重要。而後咱們再來看 MessageManager 項目中關於 Repository 的應用(實現沒有問題),在哪應用呢?根據定義咱們應該要去領域層去找 Repository 的應用,可是咱們在 MessageManager.Domain 項目中找不到關於 Repository 的半毛應用,卻在 MessageManager.Application 項目中找到了:

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

對,你已經發現了 Repository 的蹤影,Repository 應用在應用層,這樣就導致應用層和基礎層(我把數據持久化放在基礎層了)通訊,忽略了最重要的領域層,領域層在其中起到的做用最多也就是傳遞一個很是貧血的領域模型,而後經過 Repository 進行「CRUD」,這樣的結果是,應用層不變成所謂的 BLL(常說的業務邏輯層)纔怪,另外,由於業務邏輯都放在應用層了,領域模型也變得更加貧血。

  以上分析能夠回答上一篇中遺留的問題:應用層做爲協調服務層,當遇到複雜性的業務邏輯時,到底如何實現,而不使其變成 BLL(業務邏輯層)?其實關於第一個問題(領域模型如何設計不貧血)也是能夠進行解答的,這個後一節點有說明,關於這一系列問題的形成我以爲就是 Repository 設計,出現了嚴重和理論偏移,以至於沒有把設計重點發在業務邏輯上,在此和你們說聲抱歉。

  關於「應用層中的業務邏輯」,好比下面這段代碼:

 1         /// <summary>
 2         /// 查看消息
 3         /// </summary>
 4         /// <param name="ID"></param>
 5         /// <returns></returns>
 6         public MessageDTO ShowMessage(string ID, string isRead)
 7         {
 8             Message message = messageRepository.GetByKey(ID);
 9             if (isRead == "1")
10             {
11                 message.IsRead = true;
12                 messageRepository.Update(message);
13                 messageRepository.Context.Commit();
14             }
15             return Mapper.Map<Message, MessageDTO>(message);
16         }

  對,你已經看出來了,查看消息,要根據閱讀人,而後判斷是否已讀,若是是閱讀人是收件人,而且消息是未讀狀態,要把此消息置爲已讀狀態,業務邏輯沒什麼問題,可是卻放錯了位置(應用層),應該放在領域層中(領域模型),其實這都是 Repository 惹的禍,由於應用層根本沒有和領域層通訊,關於領域模型的設計下面節點有講解。

  看了以上的內容,是否是有點:撥開濃霧,見晴天的感受?不知道你有沒有?反正我是有,關於 Repository 咱們再理解的深一點,先看一下後半句的定義:利用相似於集合的接口來訪問領域對象。正如 dudu 理解的這樣:Repository 是倉庫管理員,領域層須要什麼東西只需告訴倉庫管理員,由倉庫管理員把東西拿給它,並不須要知道東西實際放在哪。能夠這樣理解爲 Repository 就像一個查詢集合,只提供查詢給領域層,可是咱們發如今實際應用中 Repository 也提供了持久化操做,這一點確實讓 Repository 有點不三不四了,關於這一點我以爲 CQRS(Command Query Responsibility Segregation)模式能夠很好的解決,翻譯爲命令查詢的職責分離,顧名思義,就是命令(持久化)和查詢職責進行分離,由於我沒有對 CQRS 進行過研究,也沒有看到過具體的示例,因此這邊就很少說,可是我以爲這是和領域驅動設計的完美結合,後面有機會能夠研究下

  說了那麼多,那 Repository(倉儲)職責究竟是什麼?能夠這樣回答:Repository,請服務好 Domain,並且只限服務於他(防止小三),他要什麼你要給什麼,爲何?由於他是你大爺,跟着他有肉吃。

Domain Model(領域模型)從新設計

  領域模型是領域驅動設計的核心,這一點是毋容置疑的,那領域模型中的核心是什麼?或者說實現的是什麼?答案是業務邏輯,那業務邏輯又是什麼?或者說什麼樣的「業務邏輯」才能稱爲真正意義上的業務邏輯,關於這個問題,在上一篇中遺留以下:

領域模型到底該怎麼設計?你會看到,MessageManager 項目中的 User 和 Message 領域模型是很是貧血的,沒有包含任何的業務邏輯,如今網上不少關於 DDD 示例項目多數也存在這種狀況,固然項目自己沒有業務,只是簡單的「CRUD」操做,可是若是是一些大型項目的複雜業務邏輯,該怎麼去實現?或者說,領域模 型完成什麼樣的業務邏輯?什麼纔是真正的業務邏輯?這個問題很重要,後續探討。

  什麼纔是真正的業務邏輯?CRUD ?持久化?仍是諸如「GetUserByName、GetMessageByID」之類的查詢,我我的感受這些都不是真正意義上的業務邏輯(注意,是我的感受),由於每一個項目會有「CRUD」、持久化,並不僅限於某一種業務場景,像「GetUserByName、GetMessageByID」之類的查詢只是查詢,瞭解了上面 Repository 的感受,你會發現這些查詢工做應該是 Repository 作的,他是爲領域模型服務的。

  說了那麼多,那什麼纔是真正意義上的業務邏輯?我我的感受改變領域模型狀態或行爲的業務邏輯,才能稱爲真正意義上的業務邏輯(注意,是我的感受),好比我在 Repository 節點中說過的一個示例:讀取消息,要根據當前閱讀人和當前消息的狀態來設置當前消息的狀態,若是當前閱讀人爲收件人和當前消息爲未讀狀態,就要把當前消息狀態設置爲已讀,之前這個業務邏輯的實現是在應用層中:

 1         /// <summary>
 2         /// 查看消息
 3         /// </summary>
 4         /// <param name="ID"></param>
 5         /// <returns></returns>
 6         public MessageDTO ShowMessage(string ID, string isRead)
 7         {
 8             Message message = messageRepository.GetByKey(ID);
 9             if (isRead == "1")
10             {
11                 message.IsRead = true;
12                 messageRepository.Update(message);
13                 messageRepository.Context.Commit();
14             }
15             return Mapper.Map<Message, MessageDTO>(message);
16         }

  這種實現方式就會把應用層變爲所謂的 BLL(業務邏輯層)了,正確的方式實現應該在 Domain Model(領域模型)中,以下:

 1         /// <summary>
 2         /// 閱讀消息
 3         /// </summary>
 4         /// <param name="CurrentUser"></param>
 5         public void ReadMessage(User CurrentUser)
 6         {
 7             if (!this.IsRead && CurrentUser.ID.Equals(ToUserID))
 8             {
 9                 this.IsRead = true;
10             }
11         }

  由於 MessageManager 這個項目的業務場景很是簡單,不少都是簡單的 CRUD 操做,能夠抽離出真正的業務邏輯實在太少,除了上面閱讀消息,還有就是在發送消息的時候,要根據發送用戶名和接受用戶名,來設置消息的發送用戶和接受用戶的 ID 值,這個操做之前咱們也是在應用層中實現的,以下:

 1         /// <summary>
 2         /// 發送消息
 3         /// </summary>
 4         /// <param name="messageDTO"></param>
 5         /// <returns></returns>
 6         public bool SendMessage(MessageDTO messageDTO)
 7         {
 8             Message message = Mapper.Map<MessageDTO, Message>(messageDTO);
 9             message.FromUserID = userRepository.GetUserByName(messageDTO.FromUserName).ID;
10             message.ToUserID = userRepository.GetUserByName(messageDTO.ToUserName).ID;
11             messageRepository.Add(message);
12             return messageRepository.Context.Commit();
13         }

  改善在 Domain Model(領域模型)中的實現,以下:

 1         /// <summary>
 2         /// 加載用戶
 3         /// </summary>
 4         /// <param name="sendUser"></param>
 5         /// <param name="receiveUser"></param>
 6         public void LoadUserName(User sendUser,User receiveUser)
 7         {
 8             this.FromUserID = sendUser.ID;
 9             this.ToUserID = receiveUser.ID;
10         }

  由於簡單的 CRUD 操做不會發生變化,而這些業務邏輯會常常發生變化,好比往消息中加載用戶信息,可能如今加載的是 ID 值,之後可能會添加其餘的用戶值,好比:用戶地理位置等等,這樣咱們只要去修改領域模型就能夠了,應用層一點都不須要修改,若是仍是以前的實現方式,你會發現咱們是必需要修改應用層的,領域模型只是一個空殼。

Domain Service(領域服務)的加入

  關於 Domain Service(領域服務)的概念,能夠參照:http://www.cnblogs.com/netfocus/archive/2011/10/10/2204949.html#content_15,netfocus 兄關於領域服務講解的很透徹,如下摘自我的感受精彩的部分:

  • 領域中的一些概念不太適合建模爲對象,即歸類到實體對象或值對象,由於它們本質上就是一些操做,一些動做,而不是事物。這些操做或動做每每會涉及到多個領域對象,而且須要協調這些領域對象共同完成這個操做或動做。若是強行將這些操做職責分配給任何一個對象,則被分配的對象就是承擔一些不應承擔的職責,從而會致使對象的職責不明確很混亂。可是基於類的面嚮對象語言規定任何屬性或行爲都必須放在對象裏面。因此咱們須要尋找一種新的模式來表示這種跨多個對象的操做,DDD認爲服務是一個很天然的範式用來對應這種跨多個對象的操做,因此就有了領域服務這個模式。
  • 我以爲模型(實體)與服務(場景)是對領域的一種劃分,模型關注領域的個體行爲,場景關注領域的羣體行爲,模型關注領域的靜態結構,場景關注領域的動態功能。這也符合了現實中出現的各類現象,有動有靜,有獨立有協做。
  • 領域服務還有一個很重要的功能就是能夠避免領域邏輯泄露到應用層。

  另外還有一個用來講明應用層服務、領域服務、基礎服務的職責分配的小示例:

  應用層服務

  1. 獲取輸入(如一個XML請求);
  2. 發送消息給領域層服務,要求其實現轉賬的業務邏輯;
  3. 領域層服務處理成功,則調用基礎層服務發送Email通知;

  領域層服務

  1. 獲取源賬號和目標賬號,分別通知源賬號和目標賬號進行扣除金額和增長金額的操做;
  2. 提供返回結果給應用層;

  基礎層服務

  1. 按照應用層的請求,發送Email通知;

  經過上述示例,能夠很清晰的理解應用層服務、領域服務、基礎服務的職責,關於這些概念的理解,我相信 netfocus 兄是通過不少實踐得出的,由於未實踐看這些概念和實踐過以後再看這些概念,徹底是不一樣的感受。

  言歸正傳,爲何要加入 Domain Service(領域服務)?領域服務在咱們以前設計 MessageManager 項目的時候並無,其實我腦海中一直是有這個概念,由於 Repository 的職責混亂,因此最後領域模型變得如此雞肋,領域服務也就沒有加入,那爲何如今要加入領域服務呢?由於 Repository 的職責劃分,使得領域模型變成重中之重,由於應用層不和 Repository 通訊,應用層又不能直接和領域模型通訊,因此纔會有領域服務的加入,也必須有領域服務的加入。經過上面概念的理解,你可能會對領域服務的做用有必定的理解,首先領域服務沒有狀態,只有行爲,他和 Repository 同樣,也是爲領域模型服務的,只不過他像一個外交官同樣,須要和應用層打交道,用來協調領域模型和應用層,而 Repository 只是一個保姆,只是服務於領域模型。

  概念理解的差很少了,咱們來看一下具體的實現,如下是 MessageDomainService 領域服務中的一段代碼:

1         public Message ShowMessage(string ID,User CurrentUser)
2         {
3             Message message = messageRepository.GetByKey(ID);
4             message.ReadMessage(userRepository.GetUser(new User { Name = CurrentUser.Name }));
5             messageRepository.Update(message);
6             messageRepository.Context.Commit();
7             return message;
8         }

  這段代碼表示查看消息,能夠看到其實領域服務作的工做就是工做流程的控制,注意是工做流程處理,並非業務流程,業務流程 ReadMessage 是領域模型去完成的,領域模型的做用只是協調。還有個疑問就是,你會看到在領域服務中使用到了 Repository,在咱們以前的講解中,Repository 不是隻服務於領域模型嗎?其實換個角度來看,領域服務也能夠看作是領域模型的一種表現,Repository 如今主要提供的是查詢集合和持久化,領域模型不能夠自身操做,那這些工做只有領域服務去完成,關於這一點,就能夠看出 Repository 的使用有點不太合理,不知道使用 CQRS 模式會不會是另外一種情形。

  另外,你會看到這一段代碼:messageRepository.Context.Commit();,這個是 Unit Of Work(工做單元)的事務提交,這個工做是領域服務要作的嗎?關於這一點是有一些疑問,在下面節點中有解讀。

MessageManager.Domain.Tests 的加入

  關於單元測試能夠參考:http://www.cnblogs.com/xishuai/p/3728576.html,MessageManager.Domain.Tests 單元測試在以前的 MessageManager 項目中並無添加,不是不想添加,而是添加了沒什麼意義,爲何?由於以前的領域模型那麼貧血,只是一些屬性和字段,那添加單元測試有什麼意思?能測出來什麼東西?當把工做聚焦在領域模型上的時候,對領域的單元測試將會很是的有必要。

  來看 DomainTest 單元測試的部分代碼:

 1 using MessageManager.Domain.DomainModel;
 2 using MessageManager.Domain.DomainService;
 3 using MessageManager.Repositories;
 4 using MessageManager.Repositories.EntityFramework;
 5 using NUnit.Framework;
 6 using System;
 7 using System.Collections.Generic;
 8 using System.Linq;
 9 using System.Text;
10 
11 namespace MessageManager.Domain.Tests
12 {
13     [TestFixture]
14     public class DomainTest
15     {
16         [Test]
17         public void UserDomainService()
18         {
19             IUserDomainService userDomainService = new UserDomainService(
20                 new UserRepository(new EntityFrameworkRepositoryContext()));
21             List<User> users = new List<User>();
22             users.Add(new User { Name = "小菜" });
23             users.Add(new User { Name = "大神" });
24             userDomainService.AddUser(users);
25             //userDomainService.ExistUser();
26             //var user = userDomainService.GetUserByName("小菜");
27             //if (user != null)
28             //{
29             //    Console.WriteLine(user.Name);
30             //}
31         }
32     }
33 }

  其實上面我貼的單元測試的代碼有些不合理,你會看到只是測試的持久化操做,這些應該是基礎層完成的工做,應該由基礎層的單元測試進行測試的,那領域層的單元測試測試的是什麼東西?應該是領域模型中的業務邏輯,好比 ReadMessage 內的操做:

1         [Test]
2         public void MessageServiceTest()
3         {
4             IMessageDomainService messageDomainService = new MessageDomainService(
5                 new MessageRepository(new EntityFrameworkRepositoryContext()),
6                 new UserRepository(new EntityFrameworkRepositoryContext()));
7             Message message = messageDomainService.ShowMessage("ID", new User { Name = "小菜" });
8             Console.WriteLine(message.IsRead);
9         }

Application Layer(應用層)的協調?

Application Layer(應用層):定義軟件能夠完成的工做,而且指揮具備豐富含義的領域對象來解決問題。這個層所負責的任務對業務影響深遠,對跟其餘系統的應用層進行交互很是必要這個層要保持簡練。它不包括處理業務規則或知識,只是給下一層中相互協做的領域對象協調任務、委託工做。在這個層次中不反映業務狀況的狀態,但反映用戶或程序的任務進度的狀態。

  以上是《領域驅動設計-軟件核心複雜性應對之道》書中關於應用層給出的定義,應用層是很薄的一層,若是你的應用層很「厚」,那你的應用層設計就確定出現了問題。關於 Application Layer(應用層)的應用,正如 Eric Evans 所說:不包括處理業務規則或知識,只是給下一層中相互協做的領域對象協調任務、委託工做。重點就是:不包含業務邏輯,協調任務。

  若是按照本身的理解去設計應用層,極可能會像我同樣把它變成業務邏輯層,因此在設計過程當中必定要謹記上面兩點。不包含業務邏輯很好理解,前提是要理解什麼纔是真正的業務邏輯(上面有說明),後面一句協調任務又是什麼意思呢?在說明中後面還有一句:在這個層次中不反映業務狀況的狀態,但反映用戶或程序的任務進度的狀態。也就是工做流程的控制,好比一個生產流水線,應用層的做用就像是這個生產流水線的控制器,具體生產什麼它不須要管理,它只要能夠裝配零件而後進行組合展現給用戶,僅此而已,畫了一張示意圖,以便你們的理解:

  另外,應用層由於要對錶現層和領域層進行任務協調,這中間會涉及到數據的對象轉換,也就是 DTO(數據傳輸對象),有關 DTO 的概念和 AutoMapper 的使用能夠參考:http://www.cnblogs.com/xishuai/tag/DTO_AutoMapper,這些工做是在應用層中進行處理的,就像生產流水線,組裝完產品後,須要對其進行包裝才能進行展現:

 1         /// 對應用層服務進行初始化。
 2         /// </summary>
 3         /// <remarks>包含的初始化任務有:
 4         /// 1. AutoMapper框架的初始化</remarks>
 5         public static void Initialize()
 6         {
 7             Mapper.CreateMap<UserDTO, User>();
 8             Mapper.CreateMap<MessageDTO, Message>();
 9             Mapper.CreateMap<User, UserDTO>();
10             Mapper.CreateMap<Message, MessageDTO>()
11                 .ForMember(dest => dest.Status, opt => opt.ResolveUsing<CustomResolver>());
12         }
13         public class CustomResolver : ValueResolver<Message, string>
14         {
15             protected override string ResolveCore(Message source)
16             {
17                 if (source.IsRead)
18                 {
19                     return "已讀";
20                 }
21                 else
22                 {
23                     return "未讀";
24                 }
25             }
26         }

Unit Of Work(工做單元)工做範圍及實現?

  關於 Unit Of Work(工做單元)的概念能夠參考:http://www.cnblogs.com/xishuai/p/3750154.html

Unit Of Work:維護受業務事務影響的對象列表,並協調變化的寫入和併發問題的解決。即管理對象的 CRUD 操做,以及相應的事務與併發問題等。Unit of Work 是用來解決領域模型存儲和變動工做,而這些數據層業務並不屬於領域模型自己具備的。

  工做單元的概念在《企業應用架構模式》中也有說明,定義以下:維護受業務事務影響的對象列表,並協調變化的寫入和併發問題的解決。概念的理解並無什麼問題,我想表達的是工做單元的工做範圍及如何實現?先說下工做範圍,咱們看下我曾經畫的一張工做單元的流程圖:

點擊查看大圖

  從示意圖中能夠看出,工做單元的範圍是限於 Repository 的,也就是說工做單元是沒法跨 Repository 提交事務的,只能在同一個倉儲內管理事務的一致性,就像咱們使用的 using(MessageManagerDbContext context = new MessageManagerDbContext()) 同樣,只是侷限於這個 using 塊,我曾在領域層的單元測試中作以下測試:

 1         [Test]
 2         public void DomainServiceTest()
 3         {
 4             IUserDomainService userDomainService = new UserDomainService(
 5                 new UserRepository(new EntityFrameworkRepositoryContext()));
 6             IMessageDomainService messageDomainService = new MessageDomainService(
 7                 new MessageRepository(new EntityFrameworkRepositoryContext()),
 8                 new UserRepository(new EntityFrameworkRepositoryContext()));
 9             List<User> users = new List<User>();
10             users.Add(new User { Name = "小菜" });
11             users.Add(new User { Name = "大神" });
12             userDomainService.AddUser(users);
13             messageDomainService.DeleteMessage(null);
14         }

  我在 MessageDomainService 中提交事務,由於以前 UserDomainService 已經添加了用戶,可是並無添加用戶成功,工做單元中的 Committed 值爲 false,其實關於工做單元範圍的問題,我如今並無明確的想法,如今是侷限在倉儲中,那提交的事務操做就必須放在領域服務中,也就是:messageRepository.Context.Commit();,可是又會以爲這樣有些不合理,工做單元應該是貫穿整個項目的,並不必定侷限在某一倉儲中,並且事務的處理液應該放在應用層中,由於這是他的工做,協調工做流的處理。

  若是這種思想是正確的話,實現起來確實有些難度,由於如今 ORM(對象關係映射)使用的是 EntityFramework,因此工做單元的實現是很簡單的,也就是使用 SaveChanges() 方法來提交事務,我在《企業應用架構模式》中看到工做單元的實現,書中列出了一個簡單的例子,還只是集合的管理,若是不使用一些 ORM 工具,實現起來就不只僅是 SaveChanges() 一段代碼的事了,太侷限於技術了,確實是個問題。

  這一節點的內容只是提出一些疑問,並未有解決的方式,但願後面能夠探討下。

版本發佈

  MessageManager 項目解決方案目錄:

  注:ASP.NET WebAPI 暫只包含:獲取發送放消息列表和獲取接收方消息列表。

  調用示例:

  WebAPI 客戶端調用能夠參考 MessageManager.WebAPI.Tests 單元測試項目中的示例調用代碼。

  注:由於 GitHub 中對 MessageManager 項目進行了更新,若是想看上一版本,下載地址:http://pan.baidu.com/s/1gd9WmUB,能夠和現有版本對比下,方便學習。

  另外,《領域驅動設計.軟件核心複雜性應對之道》Word 版本,下載地址:http://pan.baidu.com/s/1bnndOcR

後記

  這篇博文不知不覺寫兩天了(週末),左手也有點不那麼靈活了,若是再寫下去,你們也該罵我了(看得太費勁),那就作一下總結吧:

  關於領域模型的設計,我我的感受是領域驅動設計中最難的部分,你會看當前我在 MessageManager 項目中只有兩個方法,一部分緣由是業務場景太簡單,另外一部分緣由多是我設計的不合理,複雜性業務場景的領域模型是多個類之間協調處理,並會有一部分設計模式的加入,是至關複雜的。

  須要注意的一點是,本篇以上內容並非講述 Domain Model(領域模型)到底如何實現?而是如何聚焦領域模型?只有聚焦在領域模型上,才能把領域模型設計的更合理,這也正是下一步須要探討的內容。

  仍是那句話,真正理解和運用 DDD(領域驅動設計)的惟一方式,我的感受仍是「迭代」:不斷的去實踐,不斷的去體會。不合理那就推倒重建,再不合理那就推倒再重建。。。

  若是你以爲本篇文章對你有所幫助,請點擊右下部「推薦」,^_^

  參考資料:

相關文章
相關標籤/搜索