天天看點

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

寫在前面

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

  閱讀目錄:

  • 問題根源是什麼?
  • 《領域驅動設計-軟體核心複雜性應對之道》分層概念
  • 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(業務邏輯層)?認清本質很重要,後續探讨。

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

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

  關于以上的問題,本篇博文隻是做一些解讀,希望可以對那些癡迷于領域驅動設計的朋友們一些啟示,寫得有不當之處,也歡迎指出。

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

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

  但如果拿這些概念去實踐呢?卻根本不是那麼回事,現在使用領域驅動設計去開發的企業實在是太少了,原因有很多種,下面大緻列出一些:

  • 開發成本太高,換句話說,就是如果使用領域驅動設計開發,需要聘請進階程式員、進階架構師和模組化專家,一般這種開發人員薪資都比較高,老闆真的舍得嗎?
  • 開發周期長,花在需求分析的時間比較長,甚至比程式實作還要長,這個對老闆來說是要命的,開發周期長,一般會意味着公司的利潤降低,公司利潤降低,老闆的錢包就癟了,老闆會願意嗎?
  • 開發思維轉變問題,使用領域驅動設計開發,需要公司裡的程式員懂得領域驅動設計,要對面向對象(OO)設計有一定的了解,現實情況是,大部分的程式員雖然使用的是面向對象語言(比如 Java、C#),卻做着面向過程的事(類似 C 語言函數式的開發)。現在讓公司的程式員使用領域驅動設計開發,就好比以前是用手直接吃飯,現在讓你使用筷子吃飯,你會習慣嗎?這需要一種轉變,很多程式員會很不習慣,這也是領域驅動設計推行難的主要原因。
  • 關于領域驅動設計實踐經驗實在太少,大家腦子中隻有模模糊糊的概念,卻沒有實實在在的實踐,像 dax.net 這樣去完成幾個完整基于領域驅動設計項目的大神實在太少了,很多都是像我一樣,了解一些概念後,放出一個簡單的示例 Demo,然後就沒有然後了。

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

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

  還有一點就是,有朋友指出簡單的業務邏輯是展現不出領域驅動設計的,關于這一點首先我是比較贊同的,但如果去拿一些大型業務場景去做領域驅動設計的示例,我個人覺得也不太現實,畢竟時間成本太高了。我個人認為小的業務場景和大的業務場景都可以使用領域驅動設計實作,隻是業務邏輯的複雜度不同,還有就是适用度也不同,小的業務場景用腳本驅動模式去實作,可能會比領域驅動設計區實作更簡單、快速,但是但凡是業務場景(不論大小),必然包含業務邏輯(CRUD 除外),那也就可以使用領域驅動設計去開發,還是那句話,隻是不太适合,但做示範示例還是可以的。

  業務邏輯的複雜度主要展現在領域模型中,複雜性的業務邏輯,領域模型也就越複雜,但與簡單性的領域模型實質是一樣的。關于如何真正了解領域驅動設計?這一點我個人覺得方式就是“疊代”,隻有不斷的去實踐,不斷的去體會,才能真正的去了解領域驅動設計,就像 MessageManager 項目,每一次有些體會我就會覺得這樣做不合理,那就推倒重建,可能這樣做又不合理,那就推倒再重建。。。

  閑話少說,來看這一次的“疊代”:

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

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

  在面向對象的程式中,使用者界面(UI)、資料庫和其他支援代碼,經常被直接寫到業務對象中去。在UI和資料庫腳本的行為中嵌入額外的業務邏輯。出現這種情況是因為從短期的觀點看,它是使系統運作起來的最容易的方式。當與領域相關的代碼和大量的其他代碼混在一起時,就很難閱讀并了解了。對UI的簡單改動就會改變業務邏輯。改變業務規則可能需要小心翼翼地跟蹤UI代碼、資料庫代碼或者其他的程式元素。實作一緻的模型驅動對象變得不切實際,而且自動化測試也難以使用。如果在程式的每個行為中包括了所有的技術和邏輯,那麼它必須很簡單,否則會難以了解。

  将一個複雜的程式進行層次劃分。為每一層進行設計,每層都是内聚的而且隻依賴于它的下層。采用标準的架構模式來完成與上層的松散關聯。将所有與領域模型相關的代碼都集中在一層,并且将它與使用者界面層、應用層和基礎結構層的代碼分離。領域對象可以将重點放在表達領域模型上,不需要關心它們自己的顯示、存儲和管理應用任務等内容。這樣使模型發展得足夠豐富和清晰,足以抓住本質的業務知識并實作它。

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

  一個對象所代表的事物是一個具有連續性和辨別的概念(可以跟蹤該事物經曆的不同的狀态,甚至可以讓該事物跨越不同的實作),還是隻是一個用來描述事物的某種狀态的屬性?這就是實體與值對象最基本的差別。明确地選用這兩種模式中的一種來定義對象,可以使對象的意義更清晰,并可以引導我們構造出一個健壯的設計。

  另外,領域中還存在很多的方面,如果用行為或操作來描述它們會比用對象來描述更加清晰。盡管與面向對象模組化理念稍有抵觸,但這些最好是用服務來描述,而不是将這個操作的職責強加到某些實體或值對象身上。服務用來為客戶請求提供服務。在軟體的技術層中就有許多服務。服務也會在領域中出現,它們用于對軟體必須完成的一些活動進行模組化,但是與狀态無關。有時我們必須在對象模型中釆取一些折衷的措施——這是不可避免的,例如利用關系資料庫進行存儲時就會出現這種情況。本章将會給出一些規則,當遇到這種複雜情況時,遵守這些規則可以使我們保持正确的方向。

  最後,我們對子產品(Module)的讨論可以幫助了解這樣的觀點:每個設計決策都應該是根據對領域的正确了解來做出。高内聚、低關聯這種思想往往被看成是理想的技術标準,它們對于概念本身也是适用的。在模型驅動的設計中,子產品是模型的一部分,它們應該能夠反映出領域中的概念。

  言歸正題。

  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,而且隻限服務于他(防止小三),他要什麼你要給什麼,為什麼?因為他是你大爺,跟着他有肉吃。

一縷陽光:DDD(領域驅動設計)應對具體業務場景,如何聚焦 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(領域服務)的概念,可以參照: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(工作單元)的事務送出,這個工作是領域服務要做的嗎?關于這一點是有一些疑問,在下面節點中有解讀。

  關于單元測試可以參考: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(應用層)的應用,正如 Eric Evans 所說:不包括處理業務規則或知識,隻是給下一層中互相協作的領域對象協調任務、委托工作。重點就是:不包含業務邏輯,協調任務。

  如果按照自己的了解去設計應用層,很可能會像我一樣把它變成業務邏輯層,是以在設計過程中一定要謹記上面兩點。不包含業務邏輯很好了解,前提是要了解什麼才是真正的業務邏輯(上面有說明),後面一句協調任務又是什麼意思呢?在說明中後面還有一句:在這個層次中不反映業務情況的狀态,但反映使用者或程式的任務進度的狀态。也就是工作流程的控制,比如一個生産流水線,應用層的作用就像是這個生産流水線的控制器,具體生産什麼它不需要管理,它隻要可以裝配零件然後進行組合展示給使用者,僅此而已,畫了一張示意圖,以便大家的了解:

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

  另外,應用層因為要對表現層和領域層進行任務協調,這中間會涉及到資料的對象轉換,也就是 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(工作單元)的概念可以參考:http://www.cnblogs.com/xishuai/p/3750154.html。

Unit Of Work:維護受業務事務影響的對象清單,并協調變化的寫入和并發問題的解決。即管理對象的 CRUD 操作,以及相應的事務與并發問題等。Unit of Work 是用來解決領域模型存儲和變更工作,而這些資料層業務并不屬于領域模型本身具有的。

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

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

點選檢視大圖

  從示意圖中可以看出,工作單元的範圍是限于 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 項目解決方案目錄:

一縷陽光:DDD(領域驅動設計)應對具體業務場景,如何聚焦 Domain Model(領域模型)?
  • 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

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

  這篇博文不知不覺寫兩天了(周末),左手也有點不那麼靈活了,如果再寫下去,大家也該罵我了(看得太費勁),那就做一下總結吧:

  關于領域模型的設計,我個人感覺是領域驅動設計中最難的部分,你會看到目前我在 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

作者:田園裡的蟋蟀

微信公衆号:你好架構

出處: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空間

新浪微網誌

騰訊微網誌

微信

更多

繼續閱讀