寫在前面
上聯:no zuo no die why you try 下聯:no try no high give me five 橫批: let it go
上聯:no zuo no die why you cry 下聯:you try you die don't ask why 橫批: just do it
閱讀目錄:
上面那幅對聯前段時間在網上還蠻火的,意思大家意會就可以了,這邊就不翻譯了,我個人理解,所表達的個性就是:in my life,no zuo no die。
為什么會引入這個流行語?因為在應對具體業務場景,進行領域驅動設計的時候,整個項目的設計實現過程,所表達的就是這個意思:不作不死。
- 我的“第一次”,就這樣沒了:DDD(領域驅動設計)理論結合實踐:偽領域驅動設計,只是用 .NET 實現的一個“空殼”,僅此而已。
- 一縷陽光:DDD(領域驅動設計)應對具體業務場景,如何聚焦 Domain Model(領域模型)?:只是聚焦領域模型(認清各個部分的職責,讓設計的焦點集中在領域模型中),文中關于領域模型的實現就是一個“渣”,僅此而已。
- 死去活來,而不變質:Domain Model(領域模型) 和 EntityFramework 如何正確進行對象關系映射?:走了個彎路,ORM 的映射關系及倉儲的實現,應該是在本篇內容之后探討,原因都是腳本驅動模式惹的禍,如果說腳本驅動模式是惡魔(特定的環境,也有好處,不能一概而論,這邊只是一個比喻),那領域驅動設計可以看作是天使,心里想的是天使,卻聽了惡魔的話,為什么?因為它在你心中已根深蒂固,僅此而已。
-
撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?:在迷霧森林迷失那么久,自以為走了出來,其實是又走進了另一個迷霧森林,評論中和 netfocus 兄的討論就證實了這一點。
自作自受
我曾在上一篇博文的最后這樣寫道:“可能幾天或者幾周后,看現在的這篇博文就像一坨屎一樣”。這篇博文指的是《撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?》,現在看來,正被我說中了。
先回顧一下,上一篇博文所探討的內容:Domain Model 到底如何設計?毫無疑問,領域模型的設計是領域驅動設計最重要的部分,關于領域模型的設計,從一開始的不理解,把領域模型設計的很貧血,然后業務邏輯都實現在了應用層,后來經過反思,把造成這種設計誤區的元兇,懷疑到了 Repository(倉儲)身上(后來證實,人家是無辜的),然后針對倉儲,引入了 Domain Service(領域服務),把業務邏輯轉移到了領域服務中(后來證實,完全錯誤的引用),只是把 Application 單詞變成了 Domain Service 這個單詞,其他無任何變化,以至于工作流程邏輯和業務邏輯完全分不開。
造成以上的主要原因都是為了領域而領域,并沒有實實在在的去思考業務邏輯和領域模型,后來認識到這個根本問題后,就拋開一切外在因素,比如領域服務、倉儲、應用層、表現層等等,這些統統不管,只做領域模型的設計,讓真正的設計焦點集中在領域模型上,然后再針對領域模型做單元測試。
上面的思路聽起來是還蠻不錯的,至少聽起來是不錯,在上一篇博文中,后來,我是這樣“忽悠”大家的:
回到短消息系統-MessageManager,需要注意的是,我們做的是消息系統,一切的一切都應該圍繞 Message 領域模型展開, 在這個系統中,最重要的就是發送消息這個業務邏輯,什么叫發消息?不要被上面的面向對象所迷惑,只考慮發消息這個具體的業務,我們來分析一下:比如在現實生活中,我們要給女朋友寫信,首先我們要寫信的內容,寫完之后,要寫一下女朋友的地址信息及名字,這個寫信才算完成,郵遞員郵遞并不在這個業務邏輯之內了,因為這封信我寫上收件人之后,這封信相對于我來說就已經發出了,后面只不過是收件人收不收得到的問題了(即使我寫好,沒有寄出去)。也就是說郵遞員郵遞這個工作過程相當于數據的持久化,寫信的這個過程就是郵遞(發消息的業務邏輯),just it。
觀點富有辯證性,會讓你認為“的確是這樣啊”,呵呵。其實有一點我是說的不錯,就是我們做的是短消息系統,一切的一切都應該圍繞 Message 領域模型展開,消息領域模型中最重要的一個業務邏輯就是發消息(其他業務規則暫不考慮),那發消息的業務邏輯是什么?僅僅是我所固執的認為,往這條消息貼上一個收件人?你能接受嗎?至少 netfocus 兄就不接受(具體請看上一篇博文評論),按照這種設計思路,消息領域模型的設計代碼如下:
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 }
我們按照之前的思路來分析一下這個消息領域模型,首先,我們發送一條短消息,需要填寫標題、內容、發件人,也就是創建一個消息對象需要在構造函數中傳入這些必要值,然后進行為空驗證,如果驗證不通過,則消息對象就創建失敗,也就沒有了下面發送的操作,驗證成功,進入發送流程。看下消息領域模型中的 Send 方法,也就是我所認為的發送業務邏輯,就是一個簡單的收件人賦值操作(可能在賦值之前,有對收件人的驗證),收件人賦值,就是消息發送業務邏輯?還有就是 Message 領域模型中的 Send 方法,Send 是動詞,Message 對象可以發送自己?
關于以上兩個疑問,請看下面摘自我和 netfocus 兄的一些討論內容。
迷霧中的探照燈
netfocus 兄:
領域建模,切記不要以人為中心,要區分什么是系統的使用者,什么是模型中業務邏輯的參與者;如果認為某某行為是某個人做的,就把這個行為設計到人這個模型上,那就犯了以人為中心的錯誤了;在模型中,我們應該表達的是消息被創建并發送出去了,然后人只是這個消息模型的一部分,就是作為發送者,同樣,消息接受者也是消息的一部分;實際上,消息是一個聚合根,聚合根的最根本意義是封裝業務規則,那么消息的業務規則是什么呢?就是,一個消息必須至少要有一個發送者,以及接受者信息,還要有消息的標題內容等;不然這個就不是消息了;另外,發送消息顯然是應用層的一個職責;那么應用層代表什么,就是代表系統;所以, 我們要明白人與系統的關系;人使用系統,系統提供外部功能,系統內部有領域模型;
另外一點也非常重要,就是我們做領域建模,不是要對整個現實的對象交互過程建模。也就是說,假如你現實生活中是先拿起筆寫信,寫完后寫上收件人和收信地址,然后投遞員幫你去投遞,然后你女朋友接收到信;這是一
個過程,而我們的系統,不可能把每一個步驟都建模出來,領域建模只對我們所關心的信息和業務規則進行建模;
那么假如我們要設計一個系統,
要為系統使用者提供發送消息的功能,那要怎么做呢?先要問問我們自己,我們關心什么?首先,消息肯定要建模出來,不然也談不上發送消息了;那消息要包含什么信息呢?就是至少包含我上面說到的信息;而且重點是,我認為這些信息應該從消息的構造函數傳入,然后構造函數中檢查各種必要的信息是否傳入了;否則就意
味著我們能構建一個非法的消息,試想一個沒有收件人的消息如何發送?然后消息的所有屬性都應該只讀,否則怎么保證消息的狀態是合法的,當然如果你的業務允
許修改消息,那也要在剛方法上確保消息修改后也是合法的;總之,你既然設計了消息這個模型,就要充分考慮清楚這個模型的業務規則是什么;這個就是DDD一
直強調的聚合根要有true invariants,也就是把真正的不變性封裝到模型中;
然后消息需要有什么行為嗎?消息自己有一個Send方法?消息自己能發送自己?這個就好比聚合根能自己保存自己,那我們要repository來干嘛?
消息本身只定義了消息模型以及封裝了一些不變性規則,然后發送消息我上面說了,是一個系統行為;只有系統才知道消息要如何發送,應用層應該有一個SendMessage的方法;
對了,那發送消息是什么呢?發送消息難道不就是先創建消息,然后把消息通過某個Infrastructure的消息發送服務把消息發送出去嗎?和領域層有啥
關系呢?所以,我想,你必定是希望消息不僅僅是被發送出去就好了的,肯定還要把已發送的消息持久化下來,或者你是先持久化再發送,發送完之后在修改消息的
狀態;這些業務場景樓主好像沒提到吧,我也不好發揮了;
netfocus 兄:
看到這個代碼,我覺得不太舒服的是,message.Send方法不合理,message自己如何send自己;現實生活著我們的信是自己send自己的?
另外,如果message自己要Send自己,那Send方法里只是設置下收件人信息?這個不叫Send,而是叫“寫收信人”;目前你的Send方法里我看不到發送消息的邏輯;
看到你最后注釋的這兩句,我知道了你發送完消息后是要持久化消息的,這個我可以理解。挺好,呵呵
小蟋蟀:
再次感謝netfocus兄的指 點,netfocus兄在那洋洋灑灑的評論中主要說明兩點內容,消息系統的建模排除以人為中心;還有就是最重要的一點,消息可以自己發送消息?也就是 Message領域模型中的Send方法,關于第一點,現在的消息系統確實是這樣做的,并未把用戶作為核心,用戶只是一個參與者,或者稱之為觸發者,由他的參與會激發這個消息領域,然后完成消息領域中的核心業務-發消息,也就是像netfocus兄所說的應用層相當于系統,人使用系統,系統提供外部功能,系統內部有領域模型,換句話說就是領域模型可以獨立于用戶存在,領域模型抽象的描述業務,但并不是真實的業務場景,比如現實生活中,駕車這個動作,如果以這個為領域,人就是驅動者,車就是領域模型,相當于消息系統中的消息模型,那車這個領域模型在駕車這個業務場景中描述的是什么樣的業務邏輯?我覺得應該是 相對于車來說,駕車這個動作對其所造成的影響,比如車的狀態會變成形式狀態,燃油減少等等,但是在這個領域模型中并不包含人,他相對于領域模型只是一個驅 動者,也可以沒有人駕駛,但是駕車這個動作照樣可以完成(自動駕駛),車這個領域模型中描述的就只是駕車這個業務,和人沒有半毛錢關系。
關于第二點,和 netfocus兄一樣,我也是有些疑點了,然后就看了借書換書這個業務場景的討論,希望可以找到些靈感,首先,在這篇文章中幾位大神關于這個話題的討論非常精彩,在借書換書這個業務場景中,有點和發消息這個業務類似,只不過是多了一層介質-借書卡,其實討論的和用戶是一個意思,只不過是另一種方式的體現 罷了,關于借書換書這個業務場景的討論,最后看完了,也把自己整蒙了,我是這樣理解的,在這個場景中,借書換書是核心業務,什么時候發生呢,就是用戶在刷卡的時候,至于刷卡的身份驗證,不屬于這個借書換書這個領域中,或者準確的說不在借書換書這個聚合之內,為什么?因為比如學生健身要刷卡,也是一個身份驗 證,其實是和借書換書這個場景一樣,刷卡的身份驗證是另外一個聚合或是領域,當然也可以進行重用,只不過在借書換書這個場景之內,加了一些屬于這個場景的規則(比如這個卡有沒有開通借書功能等),借書還書這個業務所造成的影響是對圖書館而言的(比如書少了等等),用戶只是一個驅動者,和消息系統中一樣,發消息這個業務也是對消息所造成的影響,那這個發這個動作誰來完成,或者Send這個業務邏輯的方法誰來定義?消息本身?還是應用層?還是用戶?首先說下應用層,我覺得不可能,因為它是提供給用戶的調用者,協調者,不可能會定于這些業務邏輯,這也不在它的管轄范圍之內,它只是接受UI傳過來的一個請求,然后協調處理,至于發消息的業務實現不可能,那用戶呢?我覺得更不可能,如果是的話就是用戶消息系統了,所有的業務實現都是在用戶中,這就偏離了主題。消息本身?消息發送消息?首先這句話就有問題,如果是這樣說的話,那消息就看作是一個已死的東西,也就是躺在桌上的一封信,它能發送它自己嗎?顯然不可能,這就像讓已死的人起來走兩步一樣,回到消息發送消息這段話,這里面的消息我認為是消息模型,它不只是消息的一些屬性狀態,它還包含消息業務的所有描述(比如發 消息),也消息業務的一種體現,不是說消息發送消息,應該說消息模型中的消息業務描述這個發消息,不知道我這樣說,對不對?就好像老子提出的:“有之以為 利,無之以為用”的觀點,消息模型就像是這個“無”,只能講到這了,后面探討,再次感謝netfocus兄。
netfocus 兄:
總之,不管你如何解釋,message對象有一個send方法,我是無法接受的。因為你已經承認消息就是聚合根了,不是嗎?
消息發送的含義是讓消息發送到外部,比如發送到消息隊列,或者調用外部系統的接口,總之要和領域外部交互。那你聚合里要如何實現發送這個動作?難道你的發送只是設置下收消息的人?
我
前面的意思是,發送消息是一個用例場景,對應應用層的一個方法,就是你上面應用層的SendMessage方法。這個方法里的所有實現就是在做消息發送,
而領域模型,即Message對象,只是承載了消息的信息而已。我一直認為,消息是被發送的;消息就像一個信封,是被投遞來投遞去的;
netfocus 兄:
小蟋蟀:
netfocus兄的代碼我看到了,有幾個問題想請教下:var message=new message(…);這段代碼,傳個發件人和收件人,標題和內容,這些東西就是一封信的所有概念,代表是一個完整的信,既然這封信已經創建成功,那就說明這封信的狀態已經存在,是不是表示這封信已經寄出?寄信的概念是什么?我認為的寄信的概念是寫上收件人,至于郵寄不郵寄那不是我考慮的范圍之內,信既然已經寄出,收不收到那是收件人的問題,也就是倉儲的持久話成不成功,如果這封信已經創建成 功,_messageService.Send(Message);這段代碼中實現的是什么?發送到消息隊列,還是其他的,如果是這樣我覺得這就不是業務邏輯了,那是基礎層所做的事。
netfocus兄的意思我明白,消息是一個載體,它不能自己發送自己,只能別人來驅動發送,也就是應用層
的工作,不知道我理解netfocus的意思對不對,這樣造成的工作就是,消息領域模型就變得貧血,就像netfocus兄說的一樣,只是一些承載了一些消息的信息而已,我覺得這樣就失去了領域模型的意義,領域模型是抽象的業務邏輯,它并不只是一些實體值,它描述的是一種業務的具體抽象,就比如消息這個領
域模型,它描述的是整個消息系統的所有抽象業務邏輯(比如發消息),雖然看起來它的意思就像是消息發消息,很不合理,它只是描述這個業務邏輯,但并不真正
的業務邏輯,這一點我記得好像在借書還書的討論中有提到,不知道我這樣理解,對不對?
小蟋蟀:
現在有點明白,為什么當時有人在netfocus兄那一篇關于DDD理論的超詳細講解上這樣評論:
地址:http://www.cnblogs.com/netfocus/archive/2011/10/10/2204949.html#2872854
內容:《老子》書中有個觀點:有之以為利,無之以為用。這個理解傳統道的模塊化思想很到位。
注:我也是因為看了netfocus這篇文章才開始接觸DDD的,也是看了這個評論才開始探究道德經的,再次感謝netfocus兄。
老子的這段話的意思大家都懂得,我再解釋一遍字面意思:“有”給人便利,“無”發揮它的作用。記得園中有位兄臺的簽名:只有把自己置空,才能裝更多的東西。
其實就是這個道理,那老子的這個理論,如何和領域驅動設計聯系起來呢?我當時是很不理解的,不知道我現在這樣理解對不對?“無”代表的是領域模型抽象的業務,只是描述業務邏輯,你可以看不到,但它并不是不存在,就像是一個空杯子,雖然它是空的,但是它可以裝一杯水,這就是它的價值,也就是“無”所代表的意義,我可以裝一杯水。“有”代表的是什么呢?就是具體的業務邏輯實現了,也就是Send這個東西,用來和外部協調,就不是具體的抽象了,也就是說領域模型
它所描述的抽象業務邏輯具體化了,體現在應用層的SendMessage這個方法,在空杯子的體現就是這杯水裝滿了。
netfocus 兄:
1.是不是表示這封信已經寄出?不是,創建信只是寫好信并在信封上寫好收件人信息。
2.寄信的概念:現實生活中,寄信是一個過程,不是一個簡單的
動作,你得先把心交到郵局,然后郵局的人幫你寄送,要經過很長的路才把信寄到收信人手上;而我們系統中,不可能設計的這么復雜,我們也不會關心這么多東西,對我們來說,就是應用層創建“信”,然后把“信”交給基礎服務即可;基礎服務通過消息管道,把信傳出去,然后后面信怎么到收信人的收件箱里,那是另一個話題了;
3.你理解的寄信的概念和我完全不同,如果你認為寄信就是寫上收件人,那我會覺得無法理解。現實生活中,你把信寫好,你在信封上寫上收件人就表示信寄出去了,信會自己飛出去到目標收件人那里?你必定需要把信送到郵局或啥的地方;對應到代碼,就相當于我上面調用基礎服務把message對象傳給基礎服務,讓其把message發送出去;
4.我覺得你對領域模型理解還是不夠精確,你把領域模型的職責想的太多了。領域模型不是為了充血!我們不需要考慮當前領域模型是否太貧血了;領域模型表達的是我們所關心的領域內的信息的結構以及各種業務規則的封裝;上面我的message的構造函
數中為什么要有這四個參數是因為我們所關心的消息有且必須有這四個信息,所以,我們通過構造函數來實現這個業務規則(true
invariants),實際上還要把消息設計為只讀,因為消息一旦生成就不能修改,所以不能不加思索的加上get;set,這種代碼都是不加思索的體現;
5.應用層里的代碼本來就不是業務邏輯,邏輯分兩種:1)業務邏輯;2)流程控制邏輯;領域層負責實現業務邏輯,應用層負責實現流程控制邏輯;我上面的代碼中做了以下三步,是一個業務流程,體現的是流程控制邏輯;
1)創建Message對象,就是讓領域層幫我實現構建消息的業務邏輯(領域層負責消息的合法性);
2)調用基礎服務發送消息;
3)調用領域中的倉儲保存消息;
你仔細對比下ddd原著上的轉賬的例子吧,是不是和我上面的思路一樣呢。
這個過程中,領域層、應用層的代碼各司其職;至少我覺得比消息對象自己發送自己要自然的多。
netfocus 兄:
有之以為利,無之以為用。這個的意思就是只有空杯子才能裝水。回到DDD,那就是用戶的業務需求就是水,領域模型就是杯子,領域模型可以容納用戶的業務需求;
那用戶的業務需求是什么呢?1)用戶關心什么,這個就是數據、信息;2)用戶對這些數據有一定的業務規則定義在里面;這就是領域內的invariants,按照DDD的術語來說就是不變性,你可以理解為數據一致性;
但
是領域模型自己無法驅動自己,領域模型就像杯子,它只能被別人使用;所以對一個系統來說,那就是應用層使用領域模型;應用層接收controller過來
的command即用戶需求,經過一些簡單的參數轉化(將DTO轉化為領域層要的數據),調用領域層實現需求;當然用戶的需求不是光靠領域層可以實現的,
用戶的需求還包含了流程控制邏輯,以及一些非業務功能性需求,比如事務強一致性,并發控制,發送消息,記錄日志等,這些東西統一由應用層進行協調。
所以不要把領域模型想的太強大了,認為要充血。最后,我在用Craig Larman提出的GRASP九大模式中的第一個模式,信息專家模式。希望對你有用:將職責分配給擁有執行該職責所需信息的類;
當
你的方法名稱和你方法里做的事情不一致時,說明你沒有理解該方法所表示的職責該分配給誰。你文章中的Message對象的Send方法,只是在設置收件人
而已(所以按照方法里做的事情來定義方法名,那應該叫SetReceiveUser才對),我沒辦法認同設置收件人就是等于發送消息。
小蟋蟀:
首先,為什么這么久回復,因為我對我自己也有點懷疑了,netfocus兄的意思我懂得,我再簡單描述一下,主要是第五點,發消息這個動作主要體現在應用 層中,首先創建一個Message對象,在構造函數中傳入相應的必要值(發送人,收件人,標題,內容等等),在創建這個消息對象之前,會在領域模型中有一些驗證,比如用戶的存在性,這些都是業務邏輯的體現,驗證不成功的話,消息對象也就創建不成功,也就沒有了下面的發送操作,如果創建成功,就進入發送這個環節(這邊我不說是業務邏輯,netfocus兄認為是工作流程),這個發送工作是由基礎層去完成的,至于它怎么發送,我們并不關心,然后就是倉儲保存消 息,大概就是這個過程,代碼實現起來也很簡單,就是上面應用層的代碼。
消息系統中的發送消息過程就是上面所描述的,簡單明了,也就像netfocus兄所說的,領域層和應用層各司其職。
但是有幾個疑問:
1)這個消息系統中的發消息這個動作不是業務邏輯,是工作流程?
2)領域模型所涉及的只是去驗證消息必要值的真實性,消息領域模型所抽象出來的業務只是去驗證信息值的必要性?
netfocus兄對我的幾個疑問:
1)主要是消息對象中的Send,所不能接受,也就是對象不能發送對象本身,就像對象保存對象自己一樣。
2)還有就是設置收件人就是等于發送消息這個觀點。
小蟋蟀:
我所理解的:
首先關于那幾個疑問并不是說我為了領域而領域,然后就把領域模型看的很重要(其實是很重要),先不說發消息這個場景,回到那個空杯裝水的話題上,空杯子就是代表的“無”,所蘊含的意義就是可以裝水,具體體現就是水杯的水裝滿了,也可能在這個裝水之前我要對水進行驗證,比如茶杯就不能裝飲料,這就是空杯子所具有的規則。我是把空杯子看作是領域模型,它所描述的是裝水這個“無”,也就是消息領域模型中的發送消息,并不是說它自己就驅動自己裝水,而是說它描述這個東西,對消息領域模型而言,水杯水裝滿了的具體體現就是這條消息已經賦予了收件人,賦予了收件人的消息就相當于這個水杯的水滿了,
才具有“有”的這個含義,這個業務操作才能具體的體現出來,至于基礎層的發送消息或者是倉儲的消息持久話,就相當于我在郵局寫了這封信,把信息填寫之后,
郵遞員要讓我填寫收件人,寫好之后,這封信就相對于我來說就已經發出了(也就是水杯的水裝滿了),那個郵遞員郵遞消息就像持久話一樣,這就不是我所關心的問題了,因為水杯的水已經滿了,至于你看不看得見是另一個問題。
關于對象發送對象,這個問題,我覺得是理解上面的偏差,領域模型是業務的抽象描
述,我只是描述我可以發,message.send是有點誤解,當一個消息具有發送的前提時(發件人,標題,內容),發送這個業務描述就是在這個消息上貼上收件人。理解這個可能有點問題,再回到空杯裝水這個話題,裝水這個定義就相當于消息領域中的send,指示我可以裝水,水杯裝水這個體現就是要有水,然后倒進水杯,對消息而言,這個水就是收件人。
netfocus 兄:
哎,好吧。你如果一定要認為設置收信人就是表示信已經發送了,那我也沒辦法和你交流了。我看你應用層的代碼,message.send方法只是設置消息的收件人,那你的消息發送的功能就這樣好了?那你的消息怎么體現被發出去了呢?比如我用outlook發送一封郵件,按照你的實現,你只要創建一個郵件實 例,然后設置下郵件的收件人,然后其他啥都不用做了,這樣你就實現了郵件發送?那你不調用發送組件去發送你的郵件了?那我真的很好奇你的郵件是如何發到目 標收件人那里的。你說真正發送你不關心,那誰去關心呢?如果是經典DDD,那就是由應用層去發送的,如果是domain event+event sourcing,那可以實現為通過響應事件然后發送郵件;但這個事件也不是在設置收件人的時候產生的事件,而是在message被構造時,構造函數中所產生的事件。
模型其實是活動的結果,計算機本質就是在幫我們記錄活動的交互結果;我之所以一次性new Message的時候,就傳入這4個參數,是因為用戶在調用應用層時,就已經通知系統說,我要把某個消息發送給誰,然后系統生成這個消息,幫他發送這個消息;僅此而已。
既然你也認為message.send是有點誤解,那你為何不起一個更好的名稱呢?我很好奇你會取一個什么名稱,呵呵。
發送消息不是業務邏輯,而是一個用例場景!發送消息是一個復雜的過程,領域能參與的只是這個過程中的一部分環節。而你相當于是讓領域完成整個發送消息的過程了。這就是我們本質的理解差別。
netfocus 兄:
LZ,可能我沒有好好看你關于消息發送的定義。我所理解的發送是要類似像分布式消息隊列那樣,一個消息要被從一臺電腦傳遞到另一臺電腦的。可能你所理解的 消息發送對我來說也許只是創建消息而已。因為我創建完了的消息就已經有接收人信息了,而你的消息沒有,然后你通過一個send方法給消息設置接收人,然后 這個設置動作對你來說就是發送。這就是我們對消息發送的理解的區別吧。
我覺得發送是一個動詞,一個動作,這個動作由系統使用者(用戶)產生,然后這個動作通過http請求傳遞到web
server,然后到controller,然后controller調用應用層的sendMessage方法,意思就是通知系統幫我發送消息,然后傳入
上面我說的這四個信息給這個方法,意思就是通知系統,麻煩你幫吧我標題為subject,內容為body,發件人為sender,收件人為
receiver的消息發送一下。然后系統就先通過領域層構造這個消息,同時檢查合法性,然后如果合法,就發送(發送到消息隊列),發送成功,就持久化
(是否需要持久化看需求)。
我所理解的就是這樣一個人與系統,系統與模型的交互過程。
小蟋蟀:
確實如netfocus兄所說的一樣,理解上面有些偏差,是我對領域驅動設計的理解不深入,你懂我的意思,我也懂你的意思,只是理解不同,我比較側重于領域模型這一塊,所以就像netfocus兄所說,讓領域去完成整個發送消息的過程,至于后面所說的:如果按照你的說法,那運輸貨物是否也要在cargo上設計一個運輸的方法呢?應聘人投遞簡歷是否也要在簡歷上設計一個投遞方法呢?借書人借書是否要在書上面設計一個被借的方法呢?關于這一觀點,其實很多人會認為這是瞎扯,也就是,貨物怎么能運輸自己?簡歷怎么能投遞自己?書怎么能自己被借?想想是不可能,我所理解的消息模型,并不只是包含消息本身,它是整個消息業務場景的抽象,并不只是表示一個已死的消息,已死的消息怎么能發送自己呢?這是不可能的,可能是因為在這個消息領域模型中只有發送這個業務邏輯,所以就會認為它怎么可能發送自己,在這個領域模型中圍繞的是消息這個概念,也就包含發送消息,或者以后的處理消息,消息變更等等一些業務邏輯,描述的是抽象 的具體業務,也就是我一直所說的“無”。
小蟋蟀:
哈哈,其實在與netfocus兄的討論中,我也有點意識到我們各自所理解的發消息的概念不同,我所說的發消息就是像現實生活中的郵遞信件一 樣,netfocus兄所說的應該是人機交互的短消息發送,其實一開始我就有點納悶,發消息為什么要涉及到基礎層(發送消息隊列),然后倉儲持久化,如果像我描述的發消息是應該不涉及到基礎層的(最多也就是發個郵件提醒下,誰誰給你發送了一個消息),對于netfocus兄所說的發消息這個概念,很大層次 上是工作流程的控制,也就是應用層所實現的,但是我所說的發消息就是領域模型中的概念,就是這樣。
netfocus 兄:
終于清楚了,呵呵。
那建議你不要把這個模型直接叫做Message,因為這個名字大家就認為這是一個被發送的死的消息了。而不是你說的代表整個消息發送業務的模型了。是不是叫SendMessageService這個領域服務更好呢?然后領域服務有一個Send方法。這個我是針對LZ38
樓的回復內容。
netfocus 兄:
假如你做一個簡單的消息發送系統,支持系統用戶與用戶之間相互發消息。就類似博客園里的消息發送一樣啊。
就是你在系統UI上填好消息的標題和內容和收件人,然后點擊發送。
這 種場景的,和工作流層無關的。然后我上面的代碼就是應用層的實現代碼了。剛想到這,其實我的那個方法實現里,調用基礎服務的send方法時不需要的。因為調用倉儲持久化消息就表示消息發送完成了。我之所以上面加上調用基礎服務的send方法,是因為那時我腦子里可能想的是分布式系統之間的消息發送了。不好意思,你現在可以忽略那句話。但即便這樣,我和你的理解還是有本質不同呀,因為我還是不會有message.send這樣的設計,呵呵。
小蟋蟀:
是這樣,如netfocus兄所說,這一塊確實需要再考慮下,我曾經看到《領域驅動設計-軟件核心復雜性應對之道》這本書中的貨物運輸系統的示例,其實作者在設計領域模型的時候,就類似netfoucs兄所說的SendMessageService,在這個領域模型中,Message只能算是一個消息領域模型的一個實體,就像是Cargo一樣,領域模型就像是一個具體業務場景內的聚合一樣,包含實體也包含事件等,這一方面內容需要更深入的理解一下,因為至少現在的消息領域模型只包含實體。
設計是我出現了問題,多謝netfocus兄不厭其煩的指點,真心感謝。
我的錯,我承認
上面我和 netfocus 兄的討論,主要包含兩個內容:
-
Message 對象可以發送自己(消息領域模型中的 Send 方法)?
-
發送消息的業務邏輯是賦值收件人(Send 方法中的代碼)?
關于這個兩個疑問,其實我自己也知道,只不過當時觀念有點固執,然后就和 netfocus 兄一頓瞎扯,沒有的也說成有了,這一點我必須得承認錯誤。
趁現在頭腦清醒點,我們先分析一下第一個問題,對象可以發送自己?或者對象可以保存自己?答案是:當然不可以(有點面向對象思想的朋友都知道)。那為什么我還要在消息領域模型中寫 Send 方法呢?主要是對領域模型的認知出現了嚴重偏離,在上面我貼了一段“消息領域模型”中的代碼(消息領域模型打上了引號),你可以看到,那其實并不是真正的領域模型,領域模型是什么?它的組成部分是什么?
懂一點領域驅動設計思想的朋友都知道,領域模型包含實體(Entity)、值對象(Value Object)和領域服務(Domain Service),這三個模塊組成一個整體,才能稱為真正的領域模型,那再看一下消息領域模型的代碼,只不過打著領域模型的旗號,干著偽領域模型的事,它最多充其量只是個實體而已,事實上,它就僅僅只是一個實體,實體能發送實體自己?除非腦袋銹掉了,才會這樣認為(不得不承認,我就是這樣)。
我曾在和 netfocus 兄在討論的過程中,引入了老子《道德經》中的一段話“有之以為利,無之以為用”,然后根據杯子盛水的例子來說明發送消息這個業務場景,具體如下:
先不說發消息這個場景,回到那個空杯裝水的話題上,空杯子就是代表的“無”,所蘊含的意義就是可以裝水,具體體現就是水杯的水裝滿了,也可能在這個裝水之前我要對水進行驗證,比如茶杯就不能裝飲料,這就是空杯子所具有的規則。我是把空杯子看作是領域模型,它所描述的是裝水這個“無”,也就是消息領域模型中的發送消息,并不是說它自己就驅動自己裝水,而是說它描述這個東西,對消息領域模型而言,水杯水裝滿了的具體體現就是這條消息已經賦予了收件人,賦予了收件人的消息就相當于這個水杯的水滿了,才具有“有”的這個含義,這個業務操作才能具體的體現出來,至于基礎層的發送消息或者是倉儲的消息持久話,就相當于我在郵局寫了這封信,把信息填寫之后,郵遞員要讓我填寫收件人,寫好之后,這封信就相對于我來說就已經發出了(也就是水杯的水裝滿了),那個郵遞員郵遞消息就像持久話一樣,這就不是我所關心的問題了,因為水杯的水已經滿了,至于你看不看得見是另一個問題。
關于對象發送對象,這個問題,我覺得是理解上面的偏差,領域模型是業務的抽象描 述,我只是描述我可以發,message.send是有點誤解,當一個消息具有發送的前提時(發件人,標題,內容),發送這個業務描述就是在這個消息上貼上收件人。理解這個可能有點問題,再回到空杯裝水這個話題,裝水這個定義就相當于消息領域中的send,指示我可以裝水,水杯裝水這個體現就是要有水,然后倒進水杯,對消息而言,這個水就是收件人。
今天我就再當個被告人,來反駁一下當時的“原告”。首先,我是這樣認為的,領域模型是描述抽象的業務邏輯(在領域模型中具體怎么抽象的,并沒有描述清楚),在空杯子倒水的比喻中,我把空杯子看作領域模型,它所描述的是裝水這個“無”,也就是消息領域模型中的發消息,水看做是收件人,當水倒進空杯子,這個空杯子就滿了,體現的的就是“有”,倒水這個過程就是發消息的操作。一切聽起來還蠻合理的,但其實實際想一想就覺得有些不對,我當時把領域模型想的太抽象,也就是把它當作是業務邏輯的抽象描述,其實這個觀點是不對的,領域模型是業務邏輯的體現,也就是說它是解決實際問題的,比如領域模型中的 Send 方法,在當時我硬說成是消息領域模型對發送消息的一種描述,把那個消息領域模型并不僅僅看作是一個對象,而是一種對消息業務邏輯整體的一種描述,其實這種觀點是有有些正確點的,但是實際實現的時候卻并沒有體現出這種思想,體現出什么?Message 實體中放一個 Send 自己的方法,然后硬說成業務邏輯的抽象描述。
其實說實話,關于忽悠這一點,我還是蠻佩服我自己的,呵呵。
再次出發
承認了錯誤,就要有所改正,這才是好孩子,不是嗎?
在上一篇中,其實主要是 Message 這個偽領域模型設計的有問題,就比如命名,Message 對象中有個 Send 方法發送自己,聽聽感覺也不對。除了命名之外,就是對領域模型的理解偏差,領域模型是實體、值對象和領域服務所組成的一個整體,體現出來的就是所具體的業務邏輯,但是在之前的設計中卻僅僅是一個實體,然后自己操作自己,其實說實話,有時候這樣設計有點為了充血而充血,總想把領域模型設計的行為多些,讓它看起來并不是那么貧血,其實這就有點設計過度了。有時候想想,一個消息實體,自己有行為嗎?其實是沒有的,只有外部可以更改自身的狀態,它不像別的實體,比如用戶,可以更改自己的狀態,也可以擁有自己的一些行為,也就是說把消息實體設計看起來貧血是正常的,因為這就是它的正常狀態。
還有就是關于領域服務:
所謂服務,它強調與其他對象的聯系。不像實體和值對象,服務完全是根據能夠為客戶做什么來定義的。服務往往代表一種行為,而不是一個實體,是一個動詞而不是一個名詞。服務可以有一個抽象的、有意圖的定義,這與一個對象的定義有所不同。服務應該還有一個定義好的職責,它的職責和接口被定義為領域模型的一部分。操作名應該來自通用語言,如果通用語言中還沒有這個操作名,則應該把它添加進去。調用的參數和返回的結果應該是領域對象。
以上關于領域服務的概念來自《領域驅動設計-軟件核心復雜性應對之道》,其實這一段描述只是這本書中的一點,還有很多精彩的講解,朋友們感興趣的話可以參考下,關于領域服務的描述,主要有幾個注意點:
- 強調與其他對象的管理;
- 代表一種行為,是動詞而非名詞;
- 職責和定義是領域模型的一部分;
- 操作名來來自通用語言(比如發消息,那就可以定義為 SendMessage,誰都可以看得懂);
- 調用的參數和返回結果應該是領域對象(比如實體)
在領域驅動設計中,除了領域服務外,還包含基礎層服務和應用層服務,有時候我們如果處理的不小心的話,容易把他們三個搞混掉,在領域驅動設計這本書中講了一個轉賬事例,用來區分這三者之間的職責關系,如下:
應用層-資金轉賬應用服務
- 讀取輸入(例如XML請求)
- 發送消息給領域服務,要求處理
- 監聽確認消息
- 決定用基礎結構層的服務發送通告
領域層-資金轉賬領域服務
- 必要的賬戶和分類賬對象的相互作用,完成正確的提取和存入
- 確認轉賬結果(轉賬是否被允許或拒絕等)
基礎結構層-發送通告服務
- 由應用選擇通告方法,發送電子郵件、信件或者通過其他通信途徑
通過這個事例,我們可以很清晰的分辨出這三個服務所對應的職責:應用服務管流程;領域服務管業務;基礎服務管后勤。在這個轉賬事例中,我們在設計領域服務的時候就可以設計為:FundsTransferService,代表的是資金轉賬服務,轉賬就是一個動詞。可以想一想,如果按照我們之前的那種設計思想,肯定會在資金實體中,定義一個轉賬方法,用來表示資金對象可以轉自己?還真是蠻可笑的。在設計領域服務的時候可以這樣考慮,當一種業務邏輯,在實體中所不能表述的時候,或者表述的所不合理的時候,就要考慮一下領域服務設計的必要了。
回到我們的消息領域模型,來看一下充血設計前后解決方案圖:
前 后
可以看到在設計之前,我對文件名的命名就有問題,而且 MessageState 是值對象,應該和實體是區分開來的,設計后,這三者組成才可以稱之為領域模型,具體的實現代碼如下。
消息實體:

1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 using MessageManager.Domain.ValueObject; 7 using System; 8 9 namespace MessageManager.Domain.Entity 10 { 11 public class Message : IAggregateRoot 12 { 13 public Message(string title, string content, User sendUser, User receiveUser) 14 { 15 if (string.IsNullOrEmpty(title)) 16 { 17 throw new ArgumentException("title can't be null"); 18 } 19 if (title.Length > 20) 20 { 21 throw new ArgumentException("標題長度不能超過20"); 22 } 23 if (string.IsNullOrEmpty(content)) 24 { 25 throw new ArgumentException("content can't be null"); 26 } 27 if (content.Length > 200) 28 { 29 throw new ArgumentException("內容長度不能超過200"); 30 } 31 if (sendUser == null) 32 { 33 throw new ArgumentException("sendUser can't be null"); 34 } 35 if (receiveUser == null) 36 { 37 throw new ArgumentException("receiveUser can't be null"); 38 } 39 this.ID = Guid.NewGuid().ToString(); 40 this.Title = title; 41 this.Content = content; 42 this.SendTime = DateTime.Now; 43 this.State = MessageState.NoRead; 44 this.SendUser = sendUser; 45 this.ReceiveUser = receiveUser; 46 } 47 public string ID { get; private set; } 48 public string Title { get; private set; } 49 public string Content { get; private set; } 50 public DateTime SendTime { get; private set; } 51 public MessageState State { get; private set; } 52 public virtual User SendUser { get; private set; } 53 public virtual User ReceiveUser { get; private set; } 54 55 //public OperationResponse Send(User receiveUser) 56 //{ 57 // if (receiveUser == null) 58 // { 59 // return OperationResponse.Error("收件人規則驗證失敗"); 60 // } 61 // this.ReceiveUser = receiveUser; 62 // return OperationResponse.Success("發送消息成功"); 63 // ///to do... 64 //} 65 66 //public Message Read(User readUser) 67 //{ 68 // if (readUser.Equals(this.ReceiveUser) && this.State == MessageState.NoRead) 69 // { 70 // this.State = MessageState.Read; 71 // } 72 // return this; 73 //} 74 } 75 }
發送消息領域服務:

1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 using MessageManager.Domain.Entity; 7 using MessageManager.Infrastructure; 8 using System; 9 using System.Linq; 10 namespace MessageManager.Domain.DomainService 11 { 12 /// <summary> 13 /// SendMessage領域服務實現 14 /// </summary> 15 public class SendMessageService 16 { 17 public static OperationResponse<Message> SendMessage(Message message) 18 { 19 if (message.SendUser == message.ReceiveUser) 20 { 21 return new OperationResponse<Message>(false, "發件人和收件人不能為同一人"); 22 } 23 if (message.SendUser.SendMessages.Where(m => m.SendTime == DateTime.Now).Count() > 100) 24 { 25 return new OperationResponse<Message>(false, "發件人一天之內只能發送一百個短消息"); 26 } 27 return new OperationResponse<Message>(true, "發送消息成功", message); 28 } 29 } 30 }
消息應用服務:

1 /** 2 * author:xishuai 3 * address:https://www.github.com/yuezhongxin/MessageManager 4 **/ 5 6 using MessageManager.Domain.DomainService; 7 using MessageManager.Domain.Entity; 8 using MessageManager.Domain.Repositories; 9 using MessageManager.Infrastructure; 10 11 namespace MessageManager.Application.Implementation 12 { 13 /// <summary> 14 /// Message管理應用層接口實現 15 /// </summary> 16 public class MessageServiceImpl : ApplicationService, IMessageService 17 { 18 #region Private Fields 19 private readonly IMessageRepository messageRepository; 20 private readonly IUserRepository userRepository; 21 #endregion 22 23 #region Ctor 24 /// <summary> 25 /// 初始化一個<c>MessageServiceImpl</c>類型的實例。 26 /// </summary> 27 /// <param name="context">用來初始化<c>MessageServiceImpl</c>類型的倉儲上下文實例。</param> 28 /// <param name="messageRepository">“消息”倉儲實例。</param> 29 /// <param name="userRepository">“用戶”倉儲實例。</param> 30 public MessageServiceImpl(IRepositoryContext context, 31 IMessageRepository messageRepository, 32 IUserRepository userRepository) 33 : base(context) 34 { 35 this.messageRepository = messageRepository; 36 this.userRepository = userRepository; 37 } 38 #endregion 39 40 #region IMessageService Members 41 /// <summary> 42 /// 發送消息 43 /// </summary> 44 /// <param name="title">消息標題</param> 45 /// <param name="content">消息內容</param> 46 /// <param name="senderLoginName">發件人-登陸名</param> 47 /// <param name="receiverDisplayName">收件人-顯示名</param> 48 /// <returns></returns> 49 public OperationResponse SendMessage(string title, string content, string senderLoginName, string receiverDisplayName) 50 { 51 User sendUser = userRepository.GetUserByLoginName(senderLoginName); 52 if (sendUser == null) 53 { 54 return OperationResponse.Error("未獲取到發件人信息"); 55 } 56 User receiveUser = userRepository.GetUserByDisplayName(receiverDisplayName); 57 if (receiveUser == null) 58 { 59 return OperationResponse.Error("未獲取到收件人信息"); 60 } 61 Message message = new Message(title, content, sendUser, receiveUser); 62 OperationResponse<Message> serviceResult = SendMessageService.SendMessage(message); 63 if (serviceResult.IsSuccess) 64 { 65 return serviceResult.GetOperationResponse(); 66 //messageRepository.Add(message); 67 //return messageRepository.Context.Commit(); 68 } 69 else 70 { 71 return serviceResult.GetOperationResponse(); 72 } 73 } 74 #endregion 75 } 76 }
這邊再簡單描述下發送消息這個業務流程實現,其實看下應用層的代碼就清楚了,首先,UI 發送一個發消息的請求給應用層(相當于系統),參數為:標題、內容、發送人登錄名、收件人顯示名,應用層服務接到請求之后,先根據發送人和收件人的名稱去倉儲中查找相對應的用戶,如果用戶不存在,直接越過下面的發送操作,如果用戶存在,則創建一個消息對象,在消息實體的構造函數中去驗證這些參數的規則(比如參數不為空、字符串長度限制等等),如果驗證成功則創建消息對象成功,首先這這一方面的改進之處就是,把收件人的賦值操作放在這邊了,發送消息這個業務邏輯的體現其實并不是簡單的賦值操作,其實這種實現更符合實際生活,比如我寫一封信給女朋友,寫好標題、內容、收件人和發件人之后,我并沒有寄出,但是這封信已經存在了(符合信存在的標準),但是沒有寄出,也就是說這個消息對象已經存在,只是現在這個對象的狀態是未寄出,關于這一點,其實是和之前的設計是完全不同的,具體不同我也就不說了。
換個行,要不然看著太費勁。我們接著說,消息對象創建成功之后(狀態是未發),調用發送消息領域服務,進行業務規則驗證(比如發送人不能和收件人相同,發送人一天之內不能發送超過100個的短消息等等),其實這才是真正的發送消息業務邏輯,正如領域服務所定義的那樣,參數和返回值都是領域對象,也就是消息實體,發送驗證成功后進入持久化或者基礎服務發送郵箱,整個發送消息的工作流程就是這樣。
在上述發送消息工作流程描述中,需要注意的最重要的一點,就是消息狀態的體現,也就是消息對象的未發狀態和已發狀態,這兩個狀態確定的前提這個消息對象是存在的,也就是創建成功的。在以前的設計中,如何體現這個發送狀態的確定?答案就是收件人的賦值,之前認為,只有填寫了收件人,那這個消息的狀態就是已發送,其實這種邏輯有點天馬星空。我現在個人感覺,消息對象的發送狀態不能由它自身確定,也就是說不能由它自己的某一個屬性確定,它應該是一個動態的過程,也就是在驗證發送業務規則成功后,retrun 之后的那個消息對象,表示這個消息對象的狀態是已發送的,因為它是符合發送消息業務規則并驗證通過,那它的狀態就是已發送。
開源地址
- 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/小菜
后記
關于領域驅動設計實踐的博文,也寫了幾篇,但是說句實在話,是有點對不住大家,因為下一篇都在為上一篇做一些解釋或更正,希望大家在看得過程中保留一下自己的想法,不要被我給忽悠了。關于這一篇的內容,其實我現在已經做好下一篇更正的準備了,呵呵。
下一步的計劃是我是這樣想的:現在一個發送消息用例基本上差不多了(可能還存在其他問題),然后接下來按照這種模式把其他消息用例加進來(比如消息回復、查看等等),看看會發生什么情況,可能會出現一大堆問題,這也是我想要的,與其有針對性的解決問題,總比苦思冥想的思考要好很多。
MessageManager 項目設計到現在是沒有數據庫的(no datebase),在下面的開發設計過程中也會堅持這一原則。以前開發模式都是先根據需求建立表結構,然后再圍繞數據庫用面向對象語言做 SQL 的搬運工。可以幻想下,如果開發一個項目,在開發設計的過程中,完全沒有數據庫的概念(數據庫在開發完成之后生成,只是數據存儲的一種方式),會是什么感覺呢?我想那應該很奇妙。
如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^
參考資料:
文章列表