寫在前面
關于“Repository 倉儲,你的歸宿究竟在哪?”這個系列,本來是想寫個上下篇,但是現在覺得,很有多東西需要明確,我也不知道接下來會寫多少篇,所以上一篇的標題就改成了《Repository 倉儲,你的歸宿究竟在哪?(一)-倉儲的概念》,在這篇博文中,主要講了倉儲的概念,并沒有探討有關倉儲歸宿的任何東西,但你發現,后面評論中的探討會比博文內容更有價值,這也是我所堅持寫博文的目的之一,也就是分享的價值。
上一篇博文評論中,大部分內容是我和 czcz1024 探討“Specification-規約”,我也不知道怎么會扯到這個話題上了,就像 netfocus 兄最后所說:有點偏離主題了。確實如此,本來我覺得這一篇博文也就這樣了,但是最后劉標才給我回復:
我想問下,領域模型里面到底要不要去引用倉儲接口呢,好像ddd那個關系圖里面是可以調用的,如果實體不引用的話,一些復雜的邏輯在實體里面根本無法實現,或者只能被分割成2部分,一部分在實體,一部分在appcation中,appcation中的一般是查詢方法;比如有一個登記的業務方法(其實就是insert了),在登記前需要判斷當前年份的金額是否大于登記實體的金額,大于那么可以登記,否則異常,類似這樣的業務,判斷查詢那個到底是在實體里面判斷還是在appcation里面判斷呢,如果是實體的話,那么倉儲接口怎么注入到實體,實體是new出來的????
如果不嫌多,我再貼出一段:
這個問題我已經糾結很久了,還沒有看到具體是怎么樣實現的,stackoverflow里面也有很多老外在討論,但也沒有最終的答案,大部分人認為沒有必要在實體或者聚合中使用倉儲(或者注入倉儲);我個人認為是一定要的,先不說這樣做對不對,我們可以根據項目的實踐去看問題,很多業務都需要判斷一下是否唯一,面積或者金額之類的是否足夠,這些都是需要查詢才可以得到值的,如果是放在application去實現,就等于把這些業務轉移到了上層,而且隨著業務的復雜度增加,這樣的實現就會很多,如果放在領域層實現,看起來是很不錯的選擇,但是實體或者聚合里面怎么去注入倉儲接口呢,實體和聚合都是被new出來的,看起來是沒有辦法注入了,只能在實體里面調用XXX注入類.GetInstance();這樣的話領域層就要依賴某個注入框架了,所以這個倉儲問題,不管放哪都不那么完美。
倉儲接口>
我當時看到這段回復的時候,我覺得我找到知音了,為什么?因為只有實踐過,你才會感同身受,大道理說一大堆,不去實踐應用,這種問題你不會發現的,更不會去思考怎么解決?
我為什么認死理,非要探討倉儲的歸宿?
其實關于這個問題,netfocus 兄看到,應該會非常無語(哈哈),因為早在《設計窘境:來自 Repository 的一絲線索,Domain Model 再重新設計》這篇博文中,我和他就曾探討過,當然還有之后其他的一些交流,他的意思是:為什么非要糾結在倉儲這一塊?如果職責劃分的比較明確及正確,那該怎么使用就怎么使用,不管是應用層或是領域中,只要符合,那它就是正確的。探討的問題是“領域服務中能不能使用倉儲?”,這個也是我之前一直糾結的地方,其實 netfocus 兄的意思我都懂得,這也是倉儲設計的大前提之一,那就是職責或邊界劃分清楚。
后來 Luminji 兄發表了一篇博文《面向對象架構模式之:領域模型(Domain Model)》,看完博文內容,再看評論,你會覺得這完全沒有相干性(還是有一點的),評論中主要探討的還是倉儲的歸宿(調用問題),但是到評論結束,還是沒有一個準確的結論,為什么?因為大家都沒有去實踐應用,也就是沒有針對一個具體的業務場景進行探討,比如針對某一個業務用例,把倉儲的歸宿放在領域服務中,那這個倉儲具體該怎么設計實現?怎么調用?怎么配合領域服務完成一個具體的業務用例?應用層的代碼又該是怎樣的?IOC 容器怎么去注入?。。。雖然是一個“很小”的問題,實踐應用過后,你會發現,其實這是一個很大的問題,當然前提是,你要去實踐,去應用。
最近,Jesse Liu 兄在小組中發布了一個話題《討論一下領域驅動設計》,我覺得這種探討非常棒,因為大家都是針對同一個具體的業務用例,而不是各個不同的業務用例,而且這種探討會讓你學到,別人在這種業務用例下是怎么進行領域驅動設計的?不自覺會糾正你的一些錯誤觀點,當然前提是,你不是偏執的人。
以上我所敘述的一些東西,我個人覺得都是停留在理論階段,就像 Jesse 兄的那個話題,如果針對購物車這個業務用例,接下來的設計會是怎樣?因為之前的探討內容都是職責和邊界,其實并沒有去實踐與應用,如果實踐了,你會發現這其中的一些其他問題,“倉儲的歸宿”,只不過是這些問題的其中之一。
這樣的應用層代碼,你能接受嗎?
言歸正題,關于“倉儲,你的歸宿究竟在哪?”這個問題,這篇博文我想曬一下,我現在應用層的代碼,業務場景還是短消息系統,業務用例是發送短消息,代碼如下:
public OperationResponse SendMessage(string title, string content, string senderLoginName, string receiverDisplayName)
{
using (IRepositoryContext repositoryContext = new EntityFrameworkRepositoryContext())
{
IContactRepository contactRepository = new ContactRepository();
IMessageRepository messageRepository = new MessageRepository(repositoryContext);
ISendMessageService sendSiteMessageService = new SendSiteMessageService();
Contact sender = contactRepository.GetContactByLoginName(senderLoginName);
if (sender == null)
{
return OperationResponse.Error("抱歉!發送失敗!錯誤:發件人不存在");
}
Contact receiver = contactRepository.GetContactByDisplayName(receiverDisplayName);
if (receiver == null)
{
return OperationResponse.Error("抱歉!發送失敗!錯誤:收件人不存在");
}
try
{
Message message = new Message(title, content, sender, receiver);
if (messageRepository.GetMessageCountByIP(System.Web.HttpContext.Current.Request.UserHostAddress) > 100)
{
return OperationResponse.Error("一天內只能發送100條短消息");
}
if (messageRepository.GetOutboxCountBySender(sender) > 20)
{
return OperationResponse.Error("1小時內只能向20個不同的用戶發送短消息");
}
if (sendSiteMessageService.SendMessage(message))
{
messageRepository.Add(message);
return OperationResponse.Success("發送成功");
}
else
{
return OperationResponse.Error("發送失敗");
}
}
catch (Exception ex)
{
if (ex.GetType().Equals(typeof(ArgumentException)))
{
return OperationResponse.Error(ex.Message);
}
CNBlogs.Infrastructure.Logging.Logger.Default.Error("Application_Error: SendMessage", ex);
throw ex;
}
}
}
Are you kidding me?沒錯,你沒看錯,這就是現在短消息項目中應用層中的一段代碼,對于 DDD 的狂熱愛好者來說,我覺得他們看到這段代碼,肯定會抓狂的。。。
雖然短短幾行的代碼,但這其中所暴露出來的問題,實在太多了(比如倉儲上下文設計、自定義異常處理、倉儲的定義等等),其實我覺得你最不能接受的應該是,中間那兩個發送消息之前的業務驗證:
- 一天內只能發送100條短消息。
- 1小時內只能向20個不同的用戶發送短消息。
這個是屬于業務規則,怎么會放在應用層?難道我腦袋銹掉了?當然沒有,這個我原來是想放在 SendSiteMessageService 領域服務中的,但是我原來的設計是領域服務中不進行倉儲的調用(為了保持領域的純潔),包含業務用例描述,所以,針對這兩個業務驗證,是沒辦法放在領域服務中的,因為這種涉及到到領域對象的讀取,而所有的領域對象讀取接口都設計在倉儲中,領域服務想進行業務驗證,又不想進行領域對象讀取,你覺得可能嗎?
其實這種問題,有兩種解決方案:
- SendSiteMessageService 領域服務中實現倉儲的調用。
- 領域對象的讀取放在應用層中,獲取之后交由領域服務進行驗證。
我個人覺得,第二種實現方式只能針對一定的業務場景下,如果在業務驗證過程中,又涉及到領域對象的讀取,這個實現方式就有點不合理了,而且獲取領域對象的操作,其實也是業務的一種體現。
代碼設計是一方面,代碼重構又是另一方面,后一個過程要比前一個過程困難百倍。
寫在最后
這篇博文,我不希望寫的太長,核心內容就是那段應用層中的代碼,我知道兄臺你已經發現問題了,那就請兄臺大聲的說出來吧。
領域驅動設計中,我再列一下有關倉儲的一些探討博文:
文章列表