【吐血推薦】領域驅動設計學習輸出

我沒有三顆心臟發表於2019-06-13

【吐血推薦】領域驅動設計學習輸出

一、Hello DDD


剛開始接觸學習「DDD - 領域驅動」的時候,我被各種新穎的概念所吸引:「領域」、「領域驅動」、「子域」、「聚合」、「聚合根」、「值物件」、「通用語言」.....總之一大堆有關的、無關的概念從我的腦海經過,其中不乏讓我陷入思考的地方,我原以為我會很開心地 “享用” 這些新知識帶給我的營養(參照下圖)

【吐血推薦】領域驅動設計學習輸出

可事實上,我為學習「DDD - 領域驅動」付出了很多的精力,我嘗試用「DDD CRUD」、「DDD vs CRUD」、「Domain-Driven Design」、「DDD CQRS」、「領域驅動設計」等等一系列的關鍵字蒐集我想要的資料(翻遍了 Google 前排的所有文章&手動感謝谷歌讓我能獲得一些精彩的文章),可似乎都不太近人意,一方面這個「新概念」我對它的困惑太多了,另一方面真正「落地」並實踐起來的經驗有很少是可以直接借鑑的,再結合一些實際的場景(沒有人解答),我感到更加困惑。

【吐血推薦】領域驅動設計學習輸出

傳統開發面臨的問題

我們先來討論一下傳統開發面臨的一些問題吧,就先從傳統開發中被廣泛應用於 Web 開發的傳統三層框架:「MVC」 開始說起吧。

【吐血推薦】領域驅動設計學習輸出

傳統的「MVC」模型把框架分成了三層:顯示層、控制層、模型層,而傳統的模型層又被拆分成了業務層(Service)和資料訪問層(DAO,Data Access Object)。

顯示層負責顯示使用者介面、控制層負責處理業務邏輯、而模型則負責與資料庫通訊,對資料進行持久化的操作。這樣的結構不僅結構鬆散,而且各個模組職責分離,有什麼問題呢?

讓我們來看一個實際的例子吧。

假設我們做了一個會議室預定系統,我們的一個裝置壞了。我們需要通知預定這個會議室的所有人,於是我們需要發郵件,虛擬碼如下:

@Service
public class EquipmentServiceImpl implements EquipmentService {
    @Autowired private EmailService emailService;
    @Autowired private EquipmentRepository equipmentRepository;

    public void setEquipmentBroken(Long id) {
        Equipment equipment = equipmentRepository.findById(id);
        equipment.setStatus(Equipment.StatusEnum.BROKEN);

        emailService.sendEmail();
    }
}

問題來了,如果我們後來發現裝置壞了並且需要更改可用庫存的數量,這時候我們是不是要在這裡加入 InventoryService 庫存服務的程式碼呢?後來如果經理說裝置壞了應該通知他才對啊,所以我們要不要加入 emailService.sendEmailTo(Manager) 這樣的程式碼呢?

就算不考慮職責單一原則和關注分離原則,程式設計師也會瘋掉的,這樣做 Service 太重了,並且糟糕的是它可能還不止考慮這些,還有許可權、事務等等一系列的事情等著 Service 層去做,如此產生了大量的依賴和迴圈依賴,當業務複雜度上升時,直接導致了服務層所含的程式碼過於龐大和複雜、測試成本直線上升,並且各個 Service 的邏輯散落在各處,維護的成本也非常大。

我相信很多公司正在經歷這樣的事情,並且問題還遠不止於此。

最近我就經歷過另一種問題。作為實習生剛入公司的我接到產品了一個需求,雖然有正規的需求文件可以供我閱讀,但對於業務還不熟悉的我讀起來就感覺是:摁,我想要這個頁面這樣。

當產品經歷耐心的過來給我解釋的時候,我仍然感到無奈,因為他儘可能詳細地在描述他想要在哪一個頁面的哪一個地方加上什麼東西的同時,我看著眼前螢幕上的一堆模型和程式碼,感到無從下手,只能找來大佬幫忙充當一下 “翻譯”。

【吐血推薦】領域驅動設計學習輸出

必須承認自己對業務的生疏是主要的原因,但根本原因還是:開發與產品之間的「溝通」不能保持一致,雙方對於同一事物的「表達和理解」有很大的區別。產品看到的是實際的「業務場景」,而開發則更關注背後的「實現邏輯」。

CRUD 的各種問題

上面或許有說得不對的地方,但這樣的現象確實的存在。(例如我審視了一下我之前寫過的程式碼,突然感慨幸好自己之前都是獨立開發且功能簡單,嘻嘻嘻)

另一個想要討論的問題是關於後端開發者常常拿來自嘲的「CRUD」。經常有開發人員苦著臉說:每天除了寫「BUG」,就一直在寫「CRUD」程式碼,沒有很大長進。當然這只是一種自嘲,但可能寫「BUG」是真的,也可能沒有長進是真的,當然兩個都可能都是真的。

【吐血推薦】領域驅動設計學習輸出

「CRUD」 其實對應的是資料庫中的增刪改查的操作。現實的情況中,只有極少有企業不用到資料庫,資料庫就像是現代軟體開發的一劑靈丹妙藥,不僅提供可靠、快速、大容量的儲存服務,還支援強大的事務管理機制,滿足了大部分場景中對資料的一致性需求。

資料庫如此的強大,以至於我們從接觸軟體開發開始就一直使用「CRUD」的模式進行開發。我們的「潛意識」中就形成了「以資料為中心」的開發模式,這沒有什麼不好,並且大多數情況下是適用的,這裡只是討論:「CRUD」有什麼問題?

問題一:物件導向和資料庫天然阻抗

物件導向程式設計的語言和資料庫都是我們幾乎“最熟悉”的東西了,我們甚至使用他們編織出了絕大多數複雜多樣的網路應用,為什麼說它們之間存在著天然阻抗?

當然我覺得這裡有一點「強行找不同」的味道,但也不失為一種思考和討論。並且我覺得還是有點道理的。

A1:物件和關聯式資料庫累贅轉換

在一個物件導向的系統中,物件是資料的承載方式,每一個 DAO 物件都對應著關聯式資料庫中的一條資料。

但通常檢視層只顯示完整實體物件的一小部分資料,那麼其餘的「無關資料」你準備怎麼處理呢?

  • 是否要把物件中包含的「所有資料」一起返回給檢視層?
  • 是否需要建立一個新的專用的「資料傳輸物件」?
  • 或者你想直接把「無關資料」欄位設定成 null

顯然,在絕大多數應用中都採取了第二種方案,於是我們看到各種冗餘、繁多的「傳輸層物件」,隨著時間的推移,系統中堆積的「傳輸層物件」越來越多,不僅增加了系統的「複雜度」,而且還降低了我們的「開發效率」。我猜這也是人們說 Java 複雜的一方面原因吧。

更重要的是,萬一有一個欄位發生變化,更改量就很大。(當然這也有解決方案)

A2:繼承關係的尷尬實現

繼承是物件導向的一個重要特性,而關聯式資料庫卻難以復現物件世界中的繼承關係。

【吐血推薦】領域驅動設計學習輸出

我們來試著還原一下上面的繼承關係吧。

如果我們按照把 StudentProfessor 建成兩張表,問題就是:關聯式資料庫分割了兩個物件的共性 Person從語義上說:也就是將一個物件分割成兩個部分了;而且當你要獲取這個物件時,需要兩次Select。同樣道理增刪改查都要兩次。

如果我們把 StudentProfessor 合併成一張表,問題就是:會產生許多空白欄位。這很容易理解。

這些都反映了物件導向和關聯式資料庫天然不匹配,只能一方作出妥協,並且大部分情況是面相物件作出妥協。

A3:類的複雜關係實現

當我們需要建立一個部門(Department),而一個部門將擁有多個教授(Professor)這樣一個模型的時候,我們發現物件導向和關聯式資料庫「表達方式」的是兩種不同的形式:

  • 物件導向:
    我是一個部門,在我裡面有很多的教授;
  • 關聯式資料庫,由於外來鍵會在 Professor 上:
    我是一個教授,我屬於那個部門。

問題二:是一種資料模型,與業務脫節

沒有一個「真實的人」會在支付一筆訂單的時候說:(大概意思..)

=> 先通過我這個訂單的編號找到原始在系統上的記錄;
=> 把支付金額改成我實際支付的金額;
=> 把這個訂單的狀態修改成已支付
=> ........

而一個「真實的人」會直接說:我為這筆訂單付了xxx錢。

關係型資料庫(Relational Database)的核心實體就是資料表,核心操作就是在定義好的資料表上的「CRUD」操作。這套東西實在是太好用了,也太深入人心了,以至於你能在好多地方都能看到這種將關係模式直接用作業務模式的系統:

比如我之前寫的所有東西。(就拿我寫的個人部落格為例吧:https://github.com/wmyskxz/MyBlog

問題出在:我的「Entity層」只是資料庫表結構的一種對映用於承載資料,我的「DAO層」只是封裝了對「Entity層」的增刪改查,我的「Controller層」只是簡單的把地址和對應「Service層」的對應方法做了關聯返回結果給「檢視層」,而我的「Service層」則大部分工作也只是在做一些「查詢」、「拼接資料」的工作,這樣的系統是聲稱套上了業務的外衣,而實則只是「皇帝的新衣」,幾乎無法保證業務邏輯的正確性、完整性。

【吐血推薦】領域驅動設計學習輸出

我還記得朋友問過我一個問題,大意就是有一部分的系統其實只是對資料庫的簡單封裝,感覺就像是系統只是資料庫的「簡單代理」一樣。我一開始有點兒感同身受,但現在回過頭想,只是我們當時做的東西太簡單了而已。簡單的系統也就是對資料庫的「CRUD」。

但這還不是重點,重點是大部分的「CRUD工程師」對「業務理解」出了問題。

讓我們拿國際象棋舉個例子:

【吐血推薦】領域驅動設計學習輸出

作為一枚「CRUD工程師」,在完成了左邊部分的資料庫設計和右邊的資料展現之後,往往就認為已經萬事大吉了。但這樣的產品交付之後,對現實中使用它的使用者提出了很多的潛在要求。「CRUD工程師」從來不會提示你這些潛在需求,誰會對自己並不知道的事情加以說明呢?

簡而言之,這樣的一個國際象棋程式,自身對國際象棋規則完全是一竅不通的。就是拿出個表格給你,隨你填成啥樣。在這件事情上,完全指望使用者不犯錯,這是何等的心大!

【吐血推薦】領域驅動設計學習輸出

於是,這個國際象棋程式完全有可能出現 Bad case 的這種詭異情況:黑色騎士(knight)走出一個華麗的斜線,和其中一個白色兵(pawn)共處一室(什麼鬼?!)「國際象棋填表系統」並不會阻止你這樣做,因為它並沒有正確與錯誤之分。

這時候,「CRUD工程師」被客戶、老闆抓出來收拾殘局了。經過一番調研,原來客戶是想把黑色騎士走到 6d,並吃掉(capture)另一個白色兵。“產品已經夠簡單的了,客戶怎麼都這麼蠢?”「CRUD工程師」嘀咕道,“哎,這工作坑真是多啊”。

問題三:CRUD 缺少意圖(intent)

事實上我們可以使用「CRUD」架構很好的服務絕大多數的應用。但是正如上面提到的問題所說的那樣,當系統的「複雜度」上升的時候,「CRUD」可能會缺少一件事:意圖(intent)。

例如:

我們想要改變一個 Customer 的地址,在「CRUD」體系中,我們只需要發出更新語句就能實現。但是我們無法弄清楚這種變化是由不正確的操作引起的,還是客戶真的轉移到了另一個城市。也許我們有一個業務場景,需要再重新定位時觸發對外部系統的通知。在這種情況下,「CRUD」顯得有所缺失。

問題四:實施協作“困難”

在大多數的「CRUD」應用中,最新的更改將覆蓋其他使用者並行執行的其他更改。也就是說如果一個團隊中的兩個人同時對同一個檔案的同一行進行修改,那麼合併程式碼的時候就會產生「衝突」。

在上面我們論述了在傳統「CRUD」這樣的矛盾是如何產生的:散落在各處分散的邏輯程式碼。

問題五:被人詬病的「U」

「CRUD」中的「U」指的是「更新」操作。通常在我們的系統中「U」作為一種通用的方法可以更新資源的任何欄位,然後使用新版本覆蓋掉舊版本。

並且現在由於「REST」的流行,大多數的「API」都是圍繞「資源模型」來進行「CRUD」操作的,這樣做不僅確實極大地方便了開發人員的工作,並且藉由「HTTP動詞」和「資源URI」結合起來有很好的可讀性。

但這有什麼問題呢?

我們考慮一個簡單的「銀行賬戶」資源的問題。當我們需要把賬戶的餘額更新為想要的數量的時候,我們應該允許客戶端直接呼叫更新方法嗎?任何餘額調整的動作都應該作為某種型別的交易事務被記錄下來才對,例如「充值」、「取錢」,還是「轉賬」?另外賬戶是否存在?可能變更嗎?等等一系列問題都可能使你的通用「U」變得臃腫難以維護。

基於上述的多種多樣的「場景」,我們的通用「U」方法被推向了尷尬的境地。事實上這可能屬於設計的問題,不知道一般的公司中是如何解決的,至少在我之前寫的程式碼中,我是這樣實現的。(並且可能覺得沒有什麼問題)

【吐血推薦】領域驅動設計學習輸出

另外也有的人說「CRUD」限制了描述業務的語言的問題。因為增刪改查只有四個動詞,而我們實際的業務場景可能更加複雜。

問題六:提供變更歷史記錄的操作很複雜

還有一個問題:「CRUD」會丟失應用程式的歷史記錄。例如,如果使用者在一段時間內多次變更記錄,我們則無法再跟蹤單個更改。更糟糕的是,甚至無法確定該條目是否曾經被改變過。

當然,這可以通過為最後更新的時間戳新增欄位來處理,但這隻會幫助我們能夠獲得最新的更新。如果你對整個歷史感興趣,事情就會變得複雜:你必須從一開始就額外引入一組欄位or一張新表。

這裡的問題是:由於你不知道將來會詢問哪些關於你資料的問題,因此你無法針對相應的情況對錶做出優化。因為你收集太多或者太少的資料,似乎都存在一定問題。

總結

現在早已經不再是 PC Web 的時代了,原生 APP、移動 Web 等等多種客戶端技術在近幾年爆發(IOS、Android、JavaScript、...),青出於藍而勝於藍。原先「MVC」中的檢視(Web頁面)渲染工作,面臨被新技術的完全替代。「CRUD工程師」手中的系統們,面臨向「SOA」的轉型。

夜深人靜,四下無人的時候,「CRUD工程師」再次陷入深深的困惑:一邊是臃腫不堪的模型和控制器層,另一邊是逐漸收縮和服務化的檢視層,難道建表、寫表、讀表就要成為我的唯一主題了嗎?

「CRUD工程師」認為自己沒有創造任何東西,他們只是資料庫表的搬運工。而如果不是「CRUD」,業務系統後端工程師的價值在哪裡?

理解並抽象出業務邏輯,建立滿足需求的業務模型,以此設計實現出可靠的系統,並有效地控制複雜性。這才是大部分業務系統後端工程師的工作重點,也是解決他們工作中遇到的問題和難點的關鍵。

愛因斯坦說:“如果給我 1 個小時解答一道決定我生死的問題,我會花 55 分鐘來弄清楚這道題到底是在問什麼。一旦清楚了它到底在問什麼,剩下的 5 分鐘足夠回答這個問題。”

雖然目前為止我們還不太瞭解「DDD」是如何幫助我們解決傳統開發中的各種問題,但是聽說「DDD - 領域驅動設計」似乎是能夠用來設計和實現業務邏輯的一劑良藥。

所以「Hello - DDD」

二、DDD 是什麼?


【吐血推薦】領域驅動設計學習輸出

「DDD」的全稱是「Domain-driven Design」,即「領域驅動設計」。是由「Eric Evans」最早提出的綜合軟體系統分析和設計的物件導向建模方法,如今已經發展為一種針對大型複雜系統的領域建模與分析方法。

它完全改變了傳統軟體開發工程師針對資料庫進行的建模方法,從而將要解決的業務概念和業務規則轉換為軟體系統中的型別以及型別的屬性與行為,通過合理運用物件導向的封裝、繼承、多型等設計要素,降低或隱藏整個系統的業務複雜性,並使得系統具有更好的擴充套件性,應對紛繁多變的現實業務問題。

  • 總結: 目前為止,您只需要知道「DDD」是一種致力於降低或隱藏整個系統業務複雜性,讓系統具有更好擴充套件,應對紛雜繁多的現實也問題的架構方法就行了。

DDD 簡史

【吐血推薦】領域驅動設計學習輸出

  • 圖片引自:https://www.jianshu.com/p/e1b32a5ee91c

領域驅動設計這個概念出現在 2003 年,那個時候的軟體還處在從 CS 到 BS 轉換的時期,敏捷宣言也才發表 2 年。但是「Eric Evans」作為在企業級應用工作多年的技術顧問,敏銳的發現了在軟體開發業界內(尤其是企業級應用)開始湧現的一股思潮,他把這股思潮稱為領域驅動設計,同時還出版了一本書,在書中分享了自己在設計軟體專案時採用的建模方法,併為設計決策者提供了一個框架。

但是從那以後「DDD」並沒有和「敏捷」一樣變得更加流行,如果要問原因,我覺得一方面是這套方法裡面有很多的新名詞新概念,比如說「聚合」,「限界上下文」,「值物件」等等,要理解這些抽象概念本身就比較困難,所以學習和應用「DDD」的曲線是非常陡峭的。另一方面,做為當時唯一的“官方教材”《領域驅動設計》,閱讀這本書是一個非常痛苦的過程,在內容組織上經常會出現跳躍,所以很多人都是剛讀了幾頁就放下了。

雖然入門門檻有些高,但是對於喜歡智力挑戰的軟體工程師們來說,這就是一個難度稍為有一點高的玩具,所以在小範圍群體內,逐漸有一批人開始能夠掌控這個玩具,並且可以用它來指導設計能夠控制業務複雜性的軟體應用出來了。雖然那時候大部分的軟體應用都是單體的,但是使用「DDD」依然可以設計出來容易維護而且快速響應需求變化的單體應用出來。

【吐血推薦】領域驅動設計學習輸出

到了 2013 年,隨著各種分散式的基礎設施逐漸成熟,而「SOA架構」應用在實踐中又不是那麼順利,Martin Fowler 和 James Lewis 把當時出現的一種新型分散式架構風潮總結成微服務架構

然後微服務這股風就呼呼的吹了起來,這時候軟體工程師們發現一個問題,就是雖然指導微服務架構的應用具有什麼特徵,但是如何把原來的大單體拆分成微服務是完全不知道怎麼做了。

然後熟悉「DDD」方法的工程師發現,由於「DDD」可以有效的從業務視角對軟體系統進行拆解,並且「DDD」特別契合微服務的一個特徵:圍繞業務能力構建。所以用「DDD」拆分出來的微服務是比較合理的而且能夠實現高內聚低耦合,這樣接著微服務「DDD」迎來了它的第二春。

DDD 思辨

從計算機發明以來,人類用過表達世界變化的詞有:電子化,資訊化,數字化。這些詞裡面都有一個 “化” 字,代表著轉變,而這些轉變就是人類在逐漸的把原來在物理世界中的一個個概念一個個工作,遷移到虛擬的計算機世界。

但是在轉變的過程中,由於兩個世界的底層邏輯以及底層語言不一致,就必須要有一個翻譯和設計的過程。這個翻譯過程從軟體誕生的第一天起就天然存在,而由於有了這個翻譯過程,業務和開發之間才總是想兩個對立的階級一樣,覺得對方是難以溝通的。

【吐血推薦】領域驅動設計學習輸出

於是乎有些軟體工程界的大牛就開始思考,能不能有一種方式來減輕這個翻譯過程呢。然後就發明了「面嚮物件語言」,開始嘗試讓計算機世界有物理世界的物件概念。物件導向還不夠,這就有了「DDD」,「DDD」定義了一些基本概念,然後嘗試讓業務和開發都能夠理解這些概念名詞,然後讓「領域專家」(這裡你可以理解為熟悉業務的人)使用這些概念名詞來描述業務,而由於使用了規定的概念名詞,開發就可以很好的理解領域業務,並能夠按照領域業務設計的方式進行軟體實現。

這就是DDD的初衷:讓業務架構繫結系統架構。

【吐血推薦】領域驅動設計學習輸出

後來發現這個方法不僅僅可以做好翻譯,還可以幫助業務劃分領域邊界,可以明確哪個領域是自己的核心價值所在,以後應該重點發展哪個領域。甚至可以作為組織進行戰略規劃的參考。而能夠做到這點,其實背後的原因是物理世界和虛擬世界的融合。

三、為什麼使用 DDD?


DDD 幫助解決微服務拆分困境

上面介紹了使用DDD可以做到繫結業務架構和系統架構,這種繫結對於微服務來說有什麼關係呢。所謂的微服務拆分困難,其實根本原因是不知道邊界在什麼地方。而使用DDD對業務分析的時候,首先會使用「聚合」這個概念把關聯性強的業務概念劃分在一個邊界下,並限定「聚合」和「聚合」之間只能通過「聚合根」來訪問,這是第一層邊界。

然後在「聚合」基礎之上根據「業務相關性」「業務變化頻率」「組織結構」等等約束條件來定義「限界上下文」,這是第二層邊界。有了這兩層邊界作為約束和限制,微服務的邊界也就清晰了,拆分微服務也就不再困難了。

【吐血推薦】領域驅動設計學習輸出

DDD 幫助應對系統複雜性

解決複雜和大規模軟體的武器可以被粗略地歸為三類:「抽象」、「分治」和「知識」。

  • 分治: 把問題空間分割為規模更小且易於處理的若干子問題。分割後的問題需要足夠小,以便一個人單槍匹馬就能夠解決他們;其次,必須考慮如何將分割後的各個部分裝配為整體。分割得越合理越易於理解,在裝配成整體時,所需跟蹤的細節也就越少。即更容易設計各部分的協作方式。評判什麼是分治得好,即高內聚低耦合。

  • 抽象: 使用抽象能夠精簡問題空間,而且問題越小越容易理解。舉個例子,從北京到上海出差,可以先理解為使用交通工具前往,但不需要一開始就想清楚到底是高鐵還是飛機,以及乘坐它們需要注意什麼。

  • 知識: 顧名思義,「DDD」可以認為是知識的一種。

「DDD」提供了這樣的知識手段,讓我們知道如何抽象出「限界上下文」以及如何去「分治」。

【吐血推薦】領域驅動設計學習輸出

另外一個感受就是我們可以使用「領域事件」來應對多樣的變化。參考上面提到發郵件的例子,我們可以把它改造成這樣:

public void setEquipmentBroken(Long id) {
    Equipment equipment = equipmentRepository.findById(id);
    equipment.broken();

    eventBus.publish(new EquipmentBrokenEvent(equipment.id));
}

這樣,通知會議室預訂者的模組就會去通知相應的人員,而不用我們自己操心了。

更為重要的是,「DDD」架構區別於傳統的方式。

【吐血推薦】領域驅動設計學習輸出

我們需要先了解一個概念:「貧血模型」。也就是隻有屬性的類,貧血的意思就是沒有行為,像木乃伊一樣。這種模型唯一的作用就是將一些 ORM 對映到對應的資料庫上,而我們的「服務層」通過「DAO層」載入這些「貧血模型」進行一些拼接之類的操作,功能越複雜,這種操作就越頻繁,這是我們的軟體複雜度上升的直接原因。

而「DDD」則把大多數的業務邏輯都包含在了「聚合」、「實體」、「值物件」裡面,簡單理解也就是實現了物件自治,把之前暴露出來的一些業務操作隱藏進了「域」之中。每個不同的區域之間只能通過對外暴露的統一的聚合根來訪問,這樣就做了收權的操作,這樣資料的定義和更改的地方就聚集在了一處,很好的解決了複雜度的問題。

DDD 幫助統一語言

在UML作為建模主流的時代,軟體設計被明確分為物件導向分析(OOA),物件導向設計(OOD)和麵向物件編碼(OOP)階段。實際操作中OOD的工作往往被OOA和OOP各自承擔一部分,並同時存在分析模型和設計模型兩個割裂的模型。

【吐血推薦】領域驅動設計學習輸出

領域驅動設計的核心是建立統一的領域模型。領域模型在軟體架構中處於核心地位,軟體開發過程中,必須以建立領域模型為中心,以保障領域模型的忠實體現。

【吐血推薦】領域驅動設計學習輸出

簡單理解起來的話,也就是把業務人員和開發人員的語言統一起來,用程式碼來感受一下大概就是:

userService.love(Jack, Rose)  =>  Jack.love(Rose)
companyService.hire(company,employee)  =>  Company.hire(employee)

四、領域驅動設計過程


領域驅動設計強調領域模型的重要性,並通過模型驅動設計來保障領域模型與程式設計的一致。從業務需求中提煉出統一語言(Ubiquitous Language),再基於統一語言建立領域模型;這個領域模型會指導著程式設計以及編碼實現;最後,又通過重構來發現隱式概念,並運用設計模式改進設計與開發質量。這個過程如下圖所示:

【吐血推薦】領域驅動設計學習輸出

這個過程是一個覆蓋軟體全生命週期的設計閉環,每個環節的輸出都可以作為下一個環節的輸入,而在其中扮演重要指導作用的則是“領域模型”。這個設計閉環是一個螺旋上升的迭代設計過程,領域模型會在這個迭代過程中逐漸演進,在保證模型完整性與正確性的同時,具有新鮮的活力,使得領域模型能夠始終如一的貫穿領域驅動設計過程,闡釋著領域邏輯,指導著程式設計,驗證著編碼質量。

如果仔細審視這個設計閉環,我們發現在針對問題域和業務期望提煉統一語言,並通過統一語言進行領域建模時,可能會面臨高複雜度的挑戰。這是因為對於一個複雜的軟體系統而言,我們要處理的問題域實在太龐大了。在為問題域尋求解決方案時,需要從巨集觀層次劃分不同業務關注點的子領域,然後再深入到子領域中從微觀層次對領域進行建模。巨集觀層次是戰略的層面,微觀層次是戰術的層面,只有將戰略設計與戰術設計結合起來,才是完整的領域驅動設計。

戰略設計 (Do Right Things)

Ubiquitous language

領域驅動開發讓業務專家(Domain Expert)和開發人員一起來梳理業務,而雙方有效溝通的方式是使用通用語言,在這個專案裡,一開始我們就定義了很多詞彙表, 就是我們自己的通用語言。

Bounded Context 和 Domain

有了通用語言,詞彙表 每一個詞彙一定是有邊界的,不同的邊界內是不一樣,比如你愛人在你家這個 Bounded Context 是你的 Wife, 但是如果她是一個老師,那麼在學校這個邊界裡就是一個 Teacher. 我們經過多次討論,採取的方法是拆成多個子系統(Bounded Context,是不是很像現在的微服務?),每個子系統進行自治。

隨後我們把一個個業務抽象為領域物件(Domain Model), 每一個 Domain 對領域進行自治。而模型裡的屬性和行為表達為業務專家都可以理解的程式碼,用比如Job.Publish(). 雖然這裡面最終產生了聚合根、實體、值物件等,但是我們和業務專家溝通的時候儘量不要說這些詞彙,比如我們可以說, 在招聘這塊兒,職位是不是必須經過公司進行管理,那樣我們就知道 Job 是屬於公司這個聚合根。 對領域進行“通用”(類名,方法名等都用自然語言表達)建模,業務人員可以直接讀懂我們的程式碼,從而可以知道是否表達了業務需求。

【吐血推薦】領域驅動設計學習輸出

戰術設計 (Do Things Right)

在戰術設計方面,由於業務行為和規則都在領域裡,而且系統被拆分成多個子系統,這對技術實現上帶來了非常大的挑戰,尤其是大部分人都是有牢固的基於資料驅動開發的思想。 技術上有不同實現方式。

Event Sourcing

【吐血推薦】領域驅動設計學習輸出

Event Sourcing 就是我們不記錄資料的最終狀態,我們記錄對資料的每一次改變(Event),而讀取的時候我們把這些改變從頭再來一遍來取得資料狀態,比如你有100塊錢,現在剩下10塊了,我們記錄的不是money.total=10, 而是記錄你每一次取錢的記錄,然後從100塊開始一步步重放你取錢的過程,來得到10.

一開始,我們寫的過程中,時常回想起資料驅動的好,(每次開始一個新東西的時候,是不是很熟悉的感覺?),覺得用Event Sourcing各種麻煩,直到後來隨著系統的複雜性不斷增加,我們才感覺到帶來了非常大的好處, 這個隨後單獨來說。

CQRS

由於使用了 Event Sourcing, 對資料查詢,尤其是跨業務(Aggregate)的查詢非常麻煩,很難像關係資料那樣有查詢優勢,CQRS是解決這一問題非常好的方法,CQRS讓查詢和寫入分開,把介面需要查詢的資料進行原樣寫入,原樣的意思就是介面顯示什麼樣的,就提前儲存成什麼樣的,類似於原來的快取,沒有任何join操作,這樣查詢是非常高效的。

【吐血推薦】領域驅動設計學習輸出

演進的領域驅動設計過程

戰略設計會控制和分解戰術設計的邊界與粒度,戰術設計則以實證角度驗證領域模型的有效性、完整性與一致性,進而以演進的方式對之前的戰略設計階段進行迭代,從而形成一種螺旋式上升的迭代設計過程,如下圖所示:

【吐血推薦】領域驅動設計學習輸出

面對客戶的業務需求,由領域專家與開發團隊展開充分的交流,經過需求分析與知識提煉,獲得清晰的問題域。通過對問題域進行分析和建模,識別限界上下文,利用它劃分相對獨立的領域,再通過上下文對映建立它們之間的關係,輔以分層架構與六邊形架構劃分系統的邏輯邊界與物理邊界,界定領域與技術之間的界限。之後,進入戰術設計階段,深入到限界上下文內對領域進行建模,並以領域模型指導程式設計與編碼實現。若在實現過程中,發現領域模型存在重複、錯位或缺失時,再進而對已有模型進行重構,甚至重新劃分限界上下文。

兩個不同階段的設計目標是保持一致的,它們是一個連貫的過程,彼此之間又相互指導與規範,並最終保證一個有效的領域模型和一個富有表達力的實現同時演進。

總結


結合自己的學習經過,本篇有意識的避免了繁雜紛亂的「新概念」。如果有興趣詳細瞭解「DDD」中的那些概念,可以參照這篇文章:http://qinghua.github.io/ddd/

借大佬的總結來收個尾吧:領域驅動開發好處多多,概念比較多,門檻相對較高,對人員要求較高,團隊裡至少需要有領路人,不然代價會比較大。 尤其慎用Event Sourcing, 而領域驅動尤其適合業務相對複雜的專案。 對那些很小的專案,CRUD仍然是好的選擇。

參考文章



按照慣例黏一個尾巴:

歡迎轉載,轉載請註明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微訊號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加qq群:3382693

相關文章