撥開迷霧,找回自我:DDD 應對具體業務場景,Domain Model 到底如何設計?

發表於2014-07-05

迷霧森林

迷霧森林中,切勿迷失自我,不幸的是,我就這樣迷失了:

具體的業務場景還是短訊息系統-MessageManager,存在 Message 和 User 兩個領域模型,業務邏輯:一個使用者給另一個使用者傳送訊息,就是這麼簡單,可以看作是一個最簡單的業務邏輯,當然在傳送訊息這個過程中會有其他的業務邏輯,先不探討 Message 領域模型和 User 領域模型如何協調完成這個業務邏輯,我們先看以下,我在第一篇《我的“第一次”,就這樣沒了:DDD(領域驅動設計)理論結合實踐》博文中,關於領域模型的實現:

Are you kidding me?不,你沒看錯,以上就是 Message 領域模型的實現程式碼,User 領域模型的程式碼我就不貼了,比這個還要簡單,只包含 ID 和 Name 兩個欄位屬性,領域驅動設計主張的是充血模型,只包含欄位屬性的領域模型是極其貧血的,像上面的 Message 領域模型,充血的領域模型實現的是業務邏輯。上面我們說的傳送訊息這個業務邏輯,在領域模型中為什麼沒有體現?既然是基於領域驅動設計,那為什麼我還要這樣設計呢?這是為什麼呢?當時設計完之後,我也在思考這個問題,難道腦袋有問題?不可能吧?看下應用層的程式碼就知道了:

可以看到應用層的程式碼真是不忍直視,撇開其他操作,我們看下 SendMessage 這個方法,首先 MessageDTO 這個引數就不應該存在,下面用 AutoMapper 進行物件轉化,然後再進行賦值操作,這個過程就是典型的過程思維模式,沒有體現出一點的 OO 思想,賦值完之後,使用 Repository(倉儲)進行持久話,傳送訊息的這個業務邏輯體現在哪?如果硬要說體現的話,那就是:messageRepository.Add(message) 這段程式碼了,想想當時無知的認為,傳送訊息的業務邏輯體現就是持久化資料庫,還真是可笑。

在這篇博文發表後,很多園友也都意識到了這個問題,什麼問題?主要是以下兩個:

  • Domain Model(領域模型):領域模型到底該怎麼設計?你會看到,MessageManager 專案中的 User 和 Message 領域模型是非常貧血的,沒有包含任何的業務邏輯,現在網上很多關於 DDD 示例專案多數也存在這種情況,當然專案本身沒有業務,只是簡單的“CURD”操作,但是如果是一些大型專案的複雜業務邏輯,該怎麼去實現?或者說,領域模 型完成什麼樣的業務邏輯?什麼才是真正的業務邏輯?這個問題很重要,後續探討。
  • Application(應用層):應用層作為協調服務層,當遇到複雜性的業務邏輯時,到底如何實現,而不使其變成 BLL(業務邏輯層)?認清本質很重要,後續探討。

簡而言之就是:領域模型太貧血;應用層變成了業務邏輯層。意識到問題,那就找問題所在,經過一番探查,把 Repository 作為了重點懷疑物件,為什麼?主要是我當時以為 Repository 的職責有問題,也就有了下面的這篇博文《一縷陽光:DDD(領域驅動設計)應對具體業務場景,如何聚焦 Domain Model(領域模型)?》,在這篇博文中,關於上面原因的分析,主要講到了以下兩個節點的內容:

雖然博文中也講到了領域模型的重新設計,但是設計之後還是一坨屎,這邊就不拿出來誤導大家了。回到上面的問題,關於 Repository 的職責問題,我當時是這樣分析的:

Repository 應用在應用層,這樣就致使應用層和基礎層(我把資料持久化放在基礎層了)通訊,忽略了最重要的領域層,領域層在其中起到的作用最多也就是傳遞一個非常貧血的領域模型,然後通過 Repository 進行“CRUD”,這樣的結果是,應用層不變成所謂的 BLL(常說的業務邏輯層)才怪,另外,因為業務邏輯都放在應用層了,領域模型也變得更加貧血。

乍一看,上面的分析還真沒什麼問題(看來我還是蠻會忽悠人的,嘿嘿),Repository 服務於領域,所以就必須把 Repository 的呼叫放在領域層中,領域模型又不能直接和 Repository 通訊,所以我後來就把 Domain Service(領域服務)加了進來,讓領域服務和 Repository 進行協調,然後應用層和就和領域服務通訊了,然後的然後。。。

有朋友看到這,會覺得沒錯啊,就是這樣啊(如果你也這樣認為,那我就去幹傳銷了),先不討論對錯,我們看下領域服務究竟實現的是個什麼東西?領域模型變成了什麼?應用層又變成了什麼?

領域服務程式碼:

應用層程式碼:

領域模型程式碼:

其實上面程式碼,如果是 DDD 大神來看的話,他只要看領域模型中的程式碼就行了,因為領域驅動設計的核心就是領域模型,那領域模型變成了什麼?只是新增了 ReadMessage 和 LoadUserName 兩個不是業務邏輯的業務邏輯方法(因為只有他們兩個,如果把他們兩個去掉,就變回原來的貧血模型了,所以,你懂的),領域服務中的 SendMessage 方法變的和原來的應用層程式碼一樣,要說變化的話,只是把 Application 單詞變成了 Domain Service 這個單詞,其他無任何變化,應用層的程式碼也就變成了下面這樣:

在領域驅動設計中,應用層的定義是很薄的一層,可以看到,上面的應用層程式碼也未免太薄了吧,為什麼?因為原來它的工作讓領域服務做了,導致現在變成了一個呼叫外殼(也就是可有可無的東西,沒有任何意義)。

》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》分割線《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《《

為什麼會有分割線?因為在應對具體業務場景中,上面做的操作都是無用功,為什麼?因為設計的領域模型中什麼東西都沒有(指的是業務邏輯),沒有任何東西的領域模型,還是真正的領域驅動設計嗎?關於這個問題,傻子都知道,當然我也不傻,嘿嘿。

認識到這個根本問題後,下面就拋開一切外在因素,比如領域服務、倉儲、應用層、表現層等等,這些統統不管,只做領域模型的設計,讓真正的設計焦點集中在領域模型上,然後再針對領域模型做單元測試。

對,就是這麼簡單,至少聽起來的確簡單,事實真是這樣嗎?我卻不這樣認為,因為就是這個簡單的問題,我為此痛苦了兩三天的時間,說誇張點就是:吃不下飯,睡不著覺,在這個過程中,領域模型的一行程式碼我也沒有寫,不是不想寫,而是不知道如何寫?這是最最痛苦的,真是體會到了才知道。寫不出來怎麼辦?我就找遍網上所有關於領域模型設計的資料(大部分都是英文,只能很痛苦的看),還有《領域驅動設計》和《企業應用架構模式》這兩本書,希望能從中找到些靈感(就像畫畫,難點就在如何畫第一筆),並不是模仿,這個也模仿不來,因為每個業務場景都不相同,遺憾的是沒有找到任何的靈感,唯一找到的線索就是 OO 設計(大家都知道,我卻不知道,因為蒙了)。

既然是要物件導向,那就分析一下物件,主要包含兩個:使用者和訊息。物件擁有自身的屬性、狀態和行為,傳送訊息是使用者的一種行為,所以傳送訊息這個操作應該放在使用者中,那現在訊息只有自身的一些屬性值,因為在物件導向中,它是固定的,只有通過使用者來呼叫它,使用者領域模型程式碼如下:

按照物件導向設計,訊息是使用者的附屬物件,只有使用者存在,訊息才有意義,一個使用者物件擁有多個訊息的物件集合,那怎麼體現出傳送訊息這個動作呢?答案就是:this.SendMessages.Add(message) 這段程式碼,表示往使用者物件的訊息集合填充訊息物件,這樣就會相對於使用者物件來說,這條訊息的傳送動作就完成了。先不考慮這樣設計的合理或者不合理,我們看下訊息模型中的程式碼,就會發現裡面只有一些欄位屬性,沒有任何的操作,還有就是如果我們要新增訊息的其他動作,比如查詢,刪除等等,按照上面的分析,我們就會在使用者物件中新增這些操作,因為這些動作都是使用者所具有的,合理嗎?至少聽起來就不合理。

身處這個迷霧森林,才知道它的恐怖之處,不斷的迷失自我,以致最後可能連自己都不相信,並懷疑自己。

找回自我

在迷霧森林之中,如何找回自我?而不迷失,沒有確切的答案,我只能尋覓那一縷陽光一步一步的往前行。。。

首先,Repository,和你說聲抱歉,非常抱歉,讓你蒙冤,是我誤會你了,因為我對業務邏輯的不理解,以致做出錯誤的做法。

回到短訊息系統-MessageManager,需要注意的是,我們做的是訊息系統,一切的一切都應該圍繞 Message 領域模型展開,在這個系統中,最重要的就是傳送訊息這個業務邏輯,什麼叫發訊息?不要被上面的物件導向所迷惑,只考慮發訊息這個具體的業務,我們來分析一下:比如在現實生活中,我們要給女朋友寫信,首先我們要寫信的內容,寫完之後,要寫一下女朋友的地址資訊及名字,這個寫信才算完成,郵遞員郵遞並不在這個業務邏輯之內了,因為這封信我寫上收件人之後,這封信相對於我來說就已經發出了,後面只不過是收件人收不收得到的問題了(即使我寫好,沒有寄出去)。也就是說郵遞員郵遞這個工作過程相當於資料的持久化,寫信的這個過程就是郵遞(發訊息的業務邏輯),just it。

理解上面的內容很重要,然後我們再來看 Message 這個領域模型,建立這個物件的時候,就說明我們已經把訊息的內容寫好了,也就是必要的東西,比如:訊息標題、訊息內容、傳送人、傳送時間等等,用程式碼實現就是在 Message 領域模型中的建構函式傳遞必要值,程式碼如下:

在例項 Message 領域模型之前要對必要值進行判斷,傳送訊息的關鍵程式碼就是:ReceiveUser = receiveUser,表示為這條訊息“貼上”收件人的標籤,指示這條訊息的傳送動作已經完成,當然在這個 Send 業務方法中可能還會有其他業務邏輯的加入,其實傳送訊息就是這樣,在 Message 這個領域模型中,沒有什麼資料庫的概念,只是描述這個業務功能,僅此而已。

在領域驅動設計的過程中,你會忘記資料庫的存在,使用介面注入,我們可以想怎麼操作就怎麼操作,資料庫只是業務場景中資料的儲存的一種方式,這個工作應該是你做完所有的業務設計之後執行,如果想進行單元測試,使用 IRepository 介面,我們甚至可以虛擬一切想要的物件(是物件,不是資料值)。

我們再來看下應用層的實現:

你會發現應用層中的 SendMessage 方法,所做的工作流程是多麼的行雲流水,一步一步的協調(不要誤讀為業務邏輯,我之前就是這樣),完美的完成這個傳送訊息的請求。應用層的核心是什麼?答案就是協調,什麼意思?就是說 UI 發出一個請求(比如發訊息),然後應用層接到這這個請求之後,進行一些協調處理(比如取想要的使用者值,完成傳送,以及傳送之後的流程-發郵件等等),完成這個工作流程,而並不是這個業務邏輯,業務邏輯是發訊息。

在程式碼註釋的地方,完成的是訊息的持久化操作,當然我們也可以不完成這個操作,因為業務邏輯是業務邏輯,持久化是持久化,並沒有半毛錢關係,我們描述的只是業務場景,僅此而已。

開源地址

後記

022139500437455

以上只是一個簡單業務場景用例,就讓我在迷霧森林中迷失自我這麼久,到現在只是看到了那一縷陽光而已。DDD 是我們共同的語言,寫這篇博文的目的就是,希望園友們也可以看到希望,不要再像我一樣,迷失自我。

DDD(領域驅動設計)的過程,從上面一系列的問題更加證明是個迭代過程,一次一次的否決自己,然後再找回自己,反反覆覆,複復反反,才能成就真正的自己。可能幾天或者幾周後,看現在的這篇博文就像一坨屎一樣,但是沒關係,因為我又離真相更進了一步。

我喜歡這個挑戰,我也會堅持的完成它,誰叫我熱愛它呢,just so so。

貼一下,當時尋找的領域模型設計資料,僅作參考:

相關文章