寫在前面
閱讀目錄:
- 問題根源是什么?
- 《領域驅動設計-軟件核心復雜性應對之道》分層概念
- Repository(倉儲)職責所在?
- Domain Model(領域模型)重新設計
- Domain Service(領域服務)的加入
- MessageManager.Domain.Tests 的加入
- Application Layer(應用層)的協調?
- Unit Of Work(工作單元)工作范圍及實現?
- 版本發布
- 后記
在上一篇《我的“第一次”,就這樣沒了: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、以及清培兄的部分博文(小弟先在此謝過各位大神的無私奉獻),還有就是解道中的領域驅動設計專題,當然還有一些來自搜索引擎的部分資料,再加上自己的一些揣摩和理解,也就成為了屬于自己的“領域驅動設計”。
MessageManager 項目是對自己所理解領域驅動設計的檢驗,如果你仔細看過上一篇博文,你會發現 MessageManager 其實只是領域驅動設計的“外殼”,就像我們種黃瓜,首先要搭一個架子,以便黃瓜的生長,MessageManager 項目就相當于這個架子,核心的東西“黃瓜”并不存在,當時在設計完 MessageManager 項目的時候,其實已經發現問題的存在,所以在博文最后留下了下面兩個問題:
- Domain Model(領域模型):領域模型到底該怎么設計?你會看到,MessageManager 項目中的 User 和 Message 領域模型是非常貧血的,沒有包含任何的業務邏輯,現在網上很多關于 DDD 示例項目多數也存在這種情況,當然項目本身沒有業務,只是簡單的“CURD”操作,但是如果是一些大型項目的復雜業務邏輯,該怎么去實現?或者說,領域模型完成什么樣的業務邏輯?什么才是真正的業務邏輯?這個問題很重要,后續探討。
- Application(應用層):應用層作為協調服務層,當遇到復雜性的業務邏輯時,到底如何實現,而不使其變成 BLL(業務邏輯層)?認清本質很重要,后續探討。
另外再貼一些園友們在上一篇的問題評論:
關于以上的問題,本篇博文只是做一些解讀,希望可以對那些癡迷于領域驅動設計的朋友們一些啟示,寫得有不當之處,也歡迎指出。
問題根源是什么?
出現上述問題的原因是什么?需求簡單?設計不合理?準確的來說,應該都不是,我覺得問題的根源是沒有真正去理解領域驅動設計,或者說沒有真正用領域驅動設計的理念去設計或實現,領域驅動設計的概念網上一找一大堆,你看過幾篇文章之后也能寫出來之類的文章,為什么?因為都是泛泛之談,諸如:領域模型是領域驅動的核心;領域驅動基本分為四層(用戶層、應用層、領域層和基礎層);領域包含實體、值對象和服務;還有一些聚合和聚合根之類的概念等等,文中也會給你列出一些關于這些概念的代碼實現,讓你瞬間感覺原來領域驅動設計是這么的高大上。
但如果拿這些概念去實踐呢?卻根本不是那么回事,現在使用領域驅動設計去開發的企業實在是太少了,原因有很多種,下面大致列出一些:
- 開發成本太高,換句話說,就是如果使用領域驅動設計開發,需要聘請高級程序員、高級架構師和建模專家,一般這種開發人員薪資都比較高,老板真的舍得嗎?
- 開發周期長,花在需求分析的時間比較長,甚至比程序實現還要長,這個對老板來說是要命的,開發周期長,一般會意味著公司的利潤降低,公司利潤降低,老板的錢包就癟了,老板會愿意嗎?
- 開發思維轉變問題,使用領域驅動設計開發,需要公司里的程序員懂得領域驅動設計,要對面向對象(OO)設計有一定的理解,現實情況是,大部分的程序員雖然使用的是面向對象語言(比如 Java、C#),卻做著面向過程的事(類似 C 語言函數式的開發)。現在讓公司的程序員使用領域驅動設計開發,就好比以前是用手直接吃飯,現在讓你使用筷子吃飯,你會習慣嗎?這需要一種轉變,很多程序員會很不習慣,這也是領域驅動設計推行難的主要原因。
- 關于領域驅動設計實踐經驗實在太少,大家腦子中只有模模糊糊的概念,卻沒有實實在在的實踐,像 dax.net 這樣去完成幾個完整基于領域驅動設計項目的大神實在太少了,很多都是像我一樣,理解一些概念后,放出一個簡單的示例 Demo,然后就沒有然后了。
Eric Evans 在2004年提出 DDD(領域驅動設計)的理念,距今已經十年了,推廣卻停滯不前,確實值得我們程序員去反思。
扯得有點遠了,回到這個副標題:問題的根源是什么?答案或許會不令你滿意,就是沒有真正理解領域驅動設計。那你或許會問:那真正的領域驅動設計是什么?這個我想只有 Eric Evans 可以回答,但也不要把領域驅動設計看得這么絕對,領域驅動設計只是一種指導,具體的實現要用具體的方法,正如有句古話:師傅領進門,修行在個人。每個人有每個人的具體悟道,但再變化也不要忘了師出同門。
還有一點就是,有朋友指出簡單的業務邏輯是體現不出領域驅動設計的,關于這一點首先我是比較贊同的,但如果去拿一些大型業務場景去做領域驅動設計的示例,我個人覺得也不太現實,畢竟時間成本太高了。我個人認為小的業務場景和大的業務場景都可以使用領域驅動設計實現,只是業務邏輯的復雜度不同,還有就是適用度也不同,小的業務場景用腳本驅動模式去實現,可能會比領域驅動設計區實現更簡單、快速,但是但凡是業務場景(不論大小),必然包含業務邏輯(CRUD 除外),那也就可以使用領域驅動設計去開發,還是那句話,只是不太適合,但做演示示例還是可以的。
業務邏輯的復雜度主要體現在領域模型中,復雜性的業務邏輯,領域模型也就越復雜,但與簡單性的領域模型實質是一樣的。關于如何真正理解領域驅動設計?這一點我個人覺得方式就是“迭代”,只有不斷的去實踐,不斷的去體會,才能真正的去理解領域驅動設計,就像 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認為服務是一個很自然的范式用來對應這種跨多個對象的操作,所以就有了領域服務這個模式。
- 我覺得模型(實體)與服務(場景)是對領域的一種劃分,模型關注領域的個體行為,場景關注領域的群體行為,模型關注領域的靜態結構,場景關注領域的動態功能。這也符合了現實中出現的各種現象,有動有靜,有獨立有協作。
- 領域服務還有一個很重要的功能就是可以避免領域邏輯泄露到應用層。
另外還有一個用來說明應用層服務、領域服務、基礎服務的職責分配的小示例:
應用層服務
- 獲取輸入(如一個XML請求);
- 發送消息給領域層服務,要求其實現轉帳的業務邏輯;
- 領域層服務處理成功,則調用基礎層服務發送Email通知;
領域層服務
- 獲取源帳號和目標帳號,分別通知源帳號和目標帳號進行扣除金額和增加金額的操作;
- 提供返回結果給應用層;
基礎層服務
- 按照應用層的請求,發送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 項目解決方案目錄:
- 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/小菜
注:ASP.NET WebAPI 暫只包含:獲取發送放消息列表和獲取接收方消息列表。
調用示例:
- GetMessagesBySendUser(獲取發送方):http://www.xishuaiblog.com:8082/api/Message/GetMessagesBySendUser/用戶名
- GetMessagesByReceiveUser(獲取接受方):http://www.xishuaiblog.com:8082/api/Message/GetMessagesByReceiveUser/用戶名
WebAPI 客戶端調用可以參考 MessageManager.WebAPI.Tests 單元測試項目中的示例調用代碼。
注:因為 GitHub 中對 MessageManager 項目進行了更新,如果想看上一版本,下載地址:http://pan.baidu.com/s/1gd9WmUB,可以和現有版本對比下,方便學習。
另外,《領域驅動設計.軟件核心復雜性應對之道》Word 版本,下載地址:http://pan.baidu.com/s/1bnndOcR
后記
這篇博文不知不覺寫兩天了(周末),左手也有點不那么靈活了,如果再寫下去,大家也該罵我了(看得太費勁),那就做一下總結吧:
關于領域模型的設計,我個人感覺是領域驅動設計中最難的部分,你會看到當前我在 MessageManager 項目中只有兩個方法,一部分原因是業務場景太簡單,另一部分原因可能是我設計的不合理,復雜性業務場景的領域模型是多個類之間協調處理,并會有一部分設計模式的加入,是相當復雜的。
需要注意的一點是,本篇以上內容并不是講述 Domain Model(領域模型)到底如何實現?而是如何聚焦領域模型?只有聚焦在領域模型上,才能把領域模型設計的更合理,這也正是下一步需要探討的內容。
還是那句話,真正理解和運用 DDD(領域驅動設計)的唯一方式,個人感覺還是“迭代”:不斷的去實踐,不斷的去體會。不合理那就推倒重建,再不合理那就推倒再重建。。。
如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^
參考資料:
- http://www.cnblogs.com/dudu/archive/2011/05/25/repository_pattern.html
- http://www.cnblogs.com/1-2-3/category/109191.html
- http://www.jdon.com/ddd.html
- http://www.cnblogs.com/daxnet/archive/2009/03/31/1686984.html
- http://www.cnblogs.com/netfocus/archive/2011/10/10/2204949.html
- http://www.oschina.net/question/12_21641
文章列表