天天看點

No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計

寫在前面

上聯: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的消息發送服務把消息發送出去嗎?和領域層有啥

關系呢?是以,我想,你必定是希望消息不僅僅是被發送出去就好了的,肯定還要把已發送的消息持久化下來,或者你是先持久化再發送,發送完之後在修改消息的

狀态;這些業務場景樓主好像沒提到吧,我也不好發揮了;

看到這個代碼,我覺得不太舒服的是,message.Send方法不合理,message自己如何send自己;現實生活着我們的信是自己send自己的?

另外,如果message自己要Send自己,那Send方法裡隻是設定下收件人資訊?這個不叫Send,而是叫“寫收信人”;目前你的Send方法裡我看不到發送消息的邏輯;

看到你最後注釋的這兩句,我知道了你發送完消息後是要持久化消息的,這個我可以了解。挺好,呵呵

小蟋蟀:

再次感謝netfocus兄的指 點,netfocus兄在那洋洋灑灑的評論中主要說明兩點内容,消息系統的模組化排除以人為中心;還有就是最重要的一點,消息可以自己發送消息?也就是 Message領域模型中的Send方法,關于第一點,現在的消息系統确實是這樣做的,并未把使用者作為核心,使用者隻是一個參與者,或者稱之為觸發者,由他的參與會激發這個消息領域,然後完成消息領域中的核心業務-發消息,也就是像netfocus兄所說的應用層相當于系統,人使用系統,系統提供外部功能,系統内部有領域模型,換句話說就是領域模型可以獨立于使用者存在,領域模型抽象的描述業務,但并不是真實的業務場景,比如現實生活中,駕車這個動作,如果以這個為領域,人就是驅動者,車就是領域模型,相當于消息系統中的消息模型,那車這個領域模型在駕車這個業務場景中描述的是什麼樣的業務邏輯?我覺得應該是 相對于車來說,駕車這個動作對其所造成的影響,比如車的狀态會變成形式狀态,燃油減少等等,但是在這個領域模型中并不包含人,他相對于領域模型隻是一個驅 動者,也可以沒有人駕駛,但是駕車這個動作照樣可以完成(自動駕駛),車這個領域模型中描述的就隻是駕車這個業務,和人沒有半毛錢關系。

關于第二點,和 netfocus兄一樣,我也是有些疑點了,然後就看了借書換書這個業務場景的讨論,希望可以找到些靈感,首先,在這篇文章中幾位大神關于這個話題的讨論非常精彩,在借書換書這個業務場景中,有點和發消息這個業務類似,隻不過是多了一層媒體-借書卡,其實讨論的和使用者是一個意思,隻不過是另一種方式的展現 罷了,關于借書換書這個業務場景的讨論,最後看完了,也把自己整蒙了,我是這樣了解的,在這個場景中,借書換書是核心業務,什麼時候發生呢,就是使用者在刷卡的時候,至于刷卡的身份驗證,不屬于這個借書換書這個領域中,或者準确的說不在借書換書這個聚合之内,為什麼?因為比如學生健身要刷卡,也是一個身份驗 證,其實是和借書換書這個場景一樣,刷卡的身份驗證是另外一個聚合或是領域,當然也可以進行重用,隻不過在借書換書這個場景之内,加了一些屬于這個場景的規則(比如這個卡有沒有開通借書功能等),借書還書這個業務所造成的影響是對圖書館而言的(比如書少了等等),使用者隻是一個驅動者,和消息系統中一樣,發消息這個業務也是對消息所造成的影響,那這個發這個動作誰來完成,或者Send這個業務邏輯的方法誰來定義?消息本身?還是應用層?還是使用者?首先說下應用層,我覺得不可能,因為它是提供給使用者的調用者,協調者,不可能會定于這些業務邏輯,這也不在它的管轄範圍之内,它隻是接受UI傳過來的一個請求,然後協調處理,至于發消息的業務實作不可能,那使用者呢?我覺得更不可能,如果是的話就是使用者消息系統了,所有的業務實作都是在使用者中,這就偏離了主題。消息本身?消息發送消息?首先這句話就有問題,如果是這樣說的話,那消息就看作是一個已死的東西,也就是躺在桌上的一封信,它能發送它自己嗎?顯然不可能,這就像讓已死的人起來走兩步一樣,回到消息發送消息這段話,這裡面的消息我認為是消息模型,它不隻是消息的一些屬性狀态,它還包含消息業務的所有描述(比如發 消息),也消息業務的一種展現,不是說消息發送消息,應該說消息模型中的消息業務描述這個發消息,不知道我這樣說,對不對?就好像老子提出的:“有之以為 利,無之以為用”的觀點,消息模型就像是這個“無”,隻能講到這了,後面探讨,再次感謝netfocus兄。

總之,不管你如何解釋,message對象有一個send方法,我是無法接受的。因為你已經承認消息就是聚合根了,不是嗎?

消息發送的含義是讓消息發送到外部,比如發送到消息隊列,或者調用外部系統的接口,總之要和領域外部互動。那你聚合裡要如何實作發送這個動作?難道你的發送隻是設定下收消息的人?

前面的意思是,發送消息是一個用例場景,對應應用層的一個方法,就是你上面應用層的SendMessage方法。這個方法裡的所有實作就是在做消息發送,

而領域模型,即Message對象,隻是承載了消息的資訊而已。我一直認為,消息是被發送的;消息就像一個信封,是被投遞來投遞去的;

No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計

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這個方法,在空杯子的展現就是這杯水裝滿了。

1.是不是表示這封信已經寄出?不是,建立信隻是寫好信并在信封上寫好收件人資訊。

2.寄信的概念:現實生活中,寄信是一個過程,不是一個簡單的

動作,你得先把心交到郵局,然後郵局的人幫你寄送,要經過很長的路才把信寄到收信人手上;而我們系統中,不可能設計的這麼複雜,我們也不會關心這麼多東西,對我們來說,就是應用層建立“信”,然後把“信”交給基礎服務即可;基礎服務通過消息管道,把信傳出去,然後後面信怎麼到收信人的收件箱裡,那是另一個話題了;

3.你了解的寄信的概念和我完全不同,如果你認為寄信就是寫上收件人,那我會覺得無法了解。現實生活中,你把信寫好,你在信封上寫上收件人就表示信寄出去了,信會自己飛出去到目标收件人那裡?你必定需要把信送到郵局或啥的地方;對應到代碼,就相當于我上面調用基礎服務把message對象傳給基礎服務,讓其把message發送出去;

4.我覺得你對領域模型了解還是不夠精确,你把領域模型的職責想的太多了。領域模型不是為了充血!我們不需要考慮目前領域模型是否太貧血了;領域模型表達的是我們所關心的領域内的資訊的結構以及各種業務規則的封裝;上面我的message的構造函

數中為什麼要有這四個參數是因為我們所關心的消息有且必須有這四個資訊,是以,我們通過構造函數來實作這個業務規則(true

invariants),實際上還要把消息設計為隻讀,因為消息一旦生成就不能修改,是以不能不加思索的加上get;set,這種代碼都是不加思索的展現;

5.應用層裡的代碼本來就不是業務邏輯,邏輯分兩種:1)業務邏輯;2)流程控制邏輯;領域層負責實作業務邏輯,應用層負責實作流程控制邏輯;我上面的代碼中做了以下三步,是一個業務流程,展現的是流程控制邏輯;

1)建立Message對象,就是讓領域層幫我實作建構消息的業務邏輯(領域層負責消息的合法性);

2)調用基礎服務發送消息;

3)調用領域中的倉儲儲存消息;

你仔細對比下ddd原著上的轉賬的例子吧,是不是和我上面的思路一樣呢。

這個過程中,領域層、應用層的代碼各司其職;至少我覺得比消息對象自己發送自己要自然的多。

有之以為利,無之以為用。這個的意思就是隻有空杯子才能裝水。回到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,訓示我可以裝水,水杯裝水這個展現就是要有水,然後倒進水杯,對消息而言,這個水就是收件人。

哎,好吧。你如果一定要認為設定收信人就是表示信已經發送了,那我也沒辦法和你交流了。我看你應用層的代碼,message.send方法隻是設定消息的收件人,那你的消息發送的功能就這樣好了?那你的消息怎麼展現被發出去了呢?比如我用outlook發送一封郵件,按照你的實作,你隻要建立一個郵件實 例,然後設定下郵件的收件人,然後其他啥都不用做了,這樣你就實作了郵件發送?那你不調用發送元件去發送你的郵件了?那我真的很好奇你的郵件是如何發到目 标收件人那裡的。你說真正發送你不關心,那誰去關心呢?如果是經典DDD,那就是由應用層去發送的,如果是domain event+event sourcing,那可以實作為通過響應事件然後發送郵件;但這個事件也不是在設定收件人的時候産生的事件,而是在message被構造時,構造函數中所産生的事件。

模型其實是活動的結果,計算機本質就是在幫我們記錄活動的互動結果;我之是以一次性new Message的時候,就傳入這4個參數,是因為使用者在調用應用層時,就已經通知系統說,我要把某個消息發送給誰,然後系統生成這個消息,幫他發送這個消息;僅此而已。

既然你也認為message.send是有點誤解,那你為何不起一個更好的名稱呢?我很好奇你會取一個什麼名稱,呵呵。

發送消息不是業務邏輯,而是一個用例場景!發送消息是一個複雜的過程,領域能參與的隻是這個過程中的一部分環節。而你相當于是讓領域完成整個發送消息的過程了。這就是我們本質的了解差别。

LZ,可能我沒有好好看你關于消息發送的定義。我所了解的發送是要類似像分布式消息隊列那樣,一個消息要被從一台電腦傳遞到另一台電腦的。可能你所了解的 消息發送對我來說也許隻是建立消息而已。因為我建立完了的消息就已經有接收人資訊了,而你的消息沒有,然後你通過一個send方法給消息設定接收人,然後 這個設定動作對你來說就是發送。這就是我們對消息發送的了解的差別吧。

我覺得發送是一個動詞,一個動作,這個動作由系統使用者(使用者)産生,然後這個動作通過http請求傳遞到web

server,然後到controller,然後controller調用應用層的sendMessage方法,意思就是通知系統幫我發送消息,然後傳入

上面我說的這四個資訊給這個方法,意思就是通知系統,麻煩你幫吧我标題為subject,内容為body,發件人為sender,收件人為

receiver的消息發送一下。然後系統就先通過領域層構造這個消息,同時檢查合法性,然後如果合法,就發送(發送到消息隊列),發送成功,就持久化

(是否需要持久化看需求)。

我所了解的就是這樣一個人與系統,系統與模型的互動過程。

确實如netfocus兄所說的一樣,了解上面有些偏差,是我對領域驅動設計的了解不深入,你懂我的意思,我也懂你的意思,隻是了解不同,我比較側重于領域模型這一塊,是以就像netfocus兄所說,讓領域去完成整個發送消息的過程,至于後面所說的:如果按照你的說法,那運輸貨物是否也要在cargo上設計一個運輸的方法呢?應聘人投遞履歷是否也要在履歷上設計一個投遞方法呢?借書人借書是否要在書上面設計一個被借的方法呢?關于這一觀點,其實很多人會認為這是瞎扯,也就是,貨物怎麼能運輸自己?履歷怎麼能投遞自己?書怎麼能自己被借?想想是不可能,我所了解的消息模型,并不隻是包含消息本身,它是整個消息業務場景的抽象,并不隻是表示一個已死的消息,已死的消息怎麼能發送自己呢?這是不可能的,可能是因為在這個消息領域模型中隻有發送這個業務邏輯,是以就會認為它怎麼可能發送自己,在這個領域模型中圍繞的是消息這個概念,也就包含發送消息,或者以後的處理消息,消息變更等等一些業務邏輯,描述的是抽象 的具體業務,也就是我一直所說的“無”。

哈哈,其實在與netfocus兄的讨論中,我也有點意識到我們各自所了解的發消息的概念不同,我所說的發消息就是像現實生活中的郵遞信件一 樣,netfocus兄所說的應該是人機互動的短消息發送,其實一開始我就有點納悶,發消息為什麼要涉及到基礎層(發送消息隊列),然後倉儲持久化,如果像我描述的發消息是應該不涉及到基礎層的(最多也就是發個郵件提醒下,誰誰給你發送了一個消息),對于netfocus兄所說的發消息這個概念,很大層次 上是工作流程的控制,也就是應用層所實作的,但是我所說的發消息就是領域模型中的概念,就是這樣。

終于清楚了,呵呵。

那建議你不要把這個模型直接叫做Message,因為這個名字大家就認為這是一個被發送的死的消息了。而不是你說的代表整個消息發送業務的模型了。是不是叫SendMessageService這個領域服務更好呢?然後領域服務有一個Send方法。這個我是針對LZ38

樓的回複内容。

我所說的發送消息就是:

假如你做一個簡單的消息發送系統,支援系統使用者與使用者之間互相發消息。就類似部落格園裡的消息發送一樣啊。

就是你在系統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,誰都可以看得懂);
  • 調用的參數和傳回結果應該是領域對象(比如實體)

  在領域驅動設計中,除了領域服務外,還包含基礎層服務和應用層服務,有時候我們如果處理的不小心的話,容易把他們三個搞混掉,在領域驅動設計這本書中講了一個轉賬事例,用來區分這三者之間的職責關系,如下:

  應用層-資金轉賬應用服務

  1. 讀取輸入(例如XML請求)
  2. 發送消息給領域服務,要求處理
  3. 監聽确認消息
  4. 決定用基礎結構層的服務發送通告

  領域層-資金轉賬領域服務

  1. 必要的賬戶和分類賬對象的互相作用,完成正确的提取和存入
  2. 确認轉賬結果(轉賬是否被允許或拒絕等)

  基礎結構層-發送通告服務

  1. 由應用選擇通告方法,發送電子郵件、信件或者通過其他通信途徑

  通過這個事例,我們可以很清晰的分辨出這三個服務所對應的職責:應用服務管流程;領域服務管業務;基礎服務管後勤。在這個轉賬事例中,我們在設計領域服務的時候就可以設計為:FundsTransferService,代表的是資金轉賬服務,轉賬就是一個動詞。可以想一想,如果按照我們之前的那種設計思想,肯定會在資金實體中,定義一個轉賬方法,用來表示資金對象可以轉自己?還真是蠻可笑的。在設計領域服務的時候可以這樣考慮,當一種業務邏輯,在實體中所不能表述的時候,或者表述的所不合理的時候,就要考慮一下領域服務設計的必要了。

  回到我們的消息領域模型,來看一下充血設計前後解決方案圖:

No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計
No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計

             前                           後

  可以看到在設計之前,我對檔案名的命名就有問題,而且 MessageState 是值對象,應該和實體是區分開來的,設計後,這三者組成才可以稱之為領域模型,具體的實作代碼如下。

  消息實體:

No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計
No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計
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 }      

View Code

  發送消息領域服務:

No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計
No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計
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 }      

  消息應用服務:

No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計
No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計
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/小菜

No zuo no die:DDD 應對具體業務場景,Domain Model 重新設計

  關于領域驅動設計實踐的博文,也寫了幾篇,但是說句實在話,是有點對不住大家,因為下一篇都在為上一篇做一些解釋或更正,希望大家在看得過程中保留一下自己的想法,不要被我給忽悠了。關于這一篇的内容,其實我現在已經做好下一篇更正的準備了,呵呵。

  下一步的計劃是我是這樣想的:現在一個發送消息用例基本上差不多了(可能還存在其他問題),然後接下來按照這種模式把其他消息用例加進來(比如消息回複、檢視等等),看看會發生什麼情況,可能會出現一大堆問題,這也是我想要的,與其有針對性的解決問題,總比苦思冥想的思考要好很多。

  MessageManager 項目設計到現在是沒有資料庫的(no datebase),在下面的開發設計過程中也會堅持這一原則。以前開發模式都是先根據需求建立表結構,然後再圍繞資料庫用面向對象語言做 SQL 的搬運工。可以幻想下,如果開發一個項目,在開發設計的過程中,完全沒有資料庫的概念(資料庫在開發完成之後生成,隻是資料存儲的一種方式),會是什麼感覺呢?我想那應該很奇妙。

  如果你覺得本篇文章對你有所幫助,請點選右下部“推薦”,^_^

  參考資料:

  • http://gorodinski.com/blog/2012/04/14/services-in-domain-driven-design-ddd/

作者:田園裡的蟋蟀

微信公衆号:你好架構

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

新浪微網誌

騰訊微網誌

微信

更多

繼續閱讀