DDD 之於業務支撐的意義

《阿甘正傳》中,阿甘開始了不停地跑步,一段時間後,後面就有了很多追隨者一起跑,他們爲什麼跑哪?

類似地,一開始我也不知道 DDD 是什麼,但當發現大家都在提 DDD、都在學 DDD 的時候,我也像跟跑者一樣不由自主地加入了前行:既然有大牛提出了 DDD,既然那麼多人趨之若鶩,那麼肯定有可取的地方。

然而,有一天,阿甘停止了跑步,他不想跑了,追隨者遇到了一個問題:我們還要跑麼?當我們在學習 DDD 的過程中,感覺學而不得的時候,可能也會問:我們還要學麼?這的確引人深思。

本文基於工作經驗,嘗試談談對 DDD 的一些理解,希望能夠更好地探尋學習 DDD 的意義。

關注 DDD 的價值

無論做業務,還是做平臺、中臺,大家常常會被交錯複雜的業務邏輯、晦澀耦合的業務代碼搞得心力憔悴。我想,大家對 DDD 的追求,也是對輕鬆支撐業務發展的訴求,在探尋有沒有合適的理論可以改善現狀。畢竟,美好生活,共同嚮往。

現狀:分層支撐機制

我們選擇各種框架、進行各種組織設計,核心是爲了提高生產效率。但如果業務邏輯都是 case by case 地進行實現、缺少複用,那麼研發成本是非常高的、投入週期也會非常長。

爲了增加複用、縮短業務的落地時間,就需要很多通用的能力、產品。在我們的交付過程中,主要有兩個層次:

  1. 基礎能力:相對原子的能力是基礎(域)能力,這個可以較好地支持業務定製。由於比較基礎,表達的產品能力範圍也是很大的。但是,一個完善的產品需要串聯的基礎能力是非常多的,串聯的成本也是非常高的。

  2. 平臺產品:基礎能力的通用性,意味着缺少對場景的理解,缺少了進一步提升生產效率的 “基因”。所以在交付的時候,會基於一些高頻場景進行抽象,形成平臺的產品能力,爭取做到“拆箱即用”。業務基於“平臺產品” 這層進行定製的時候,理解成本會大大減少。

腐化:業務邏輯滲透

這樣的層次,看上去很美好:在起步階段,由於缺少歷史包袱,的確可以提升一定的生產效率,這是能力本身的收益。但是,越往後,隨着業務接入的增多,業務之間開始互相影響,研發的阻力也越來越大。

研發效能降低的重要原因在於:更多的時候,我們還是按照 “業務能跑起來,怎麼快怎麼來 “的邏輯去做相關工作,遇水搭橋,遇山開洞,然後直達目的地,進行信息的傳達、數據庫字段的操作。

這樣的過程,違背了我們”希望通過業務場景,豐富平臺能力,同時保證內核乾淨 “的初衷。能力應該是基於相對多的用例、相對完善的思考進行抽象,是橫向統一看,有更深刻的理解,但是垂直的交付,讓我們更加縱向地處理問題,往往只是“窺探” 了鏈路,在交付時長和業務節點的限制下,很難想得更加全面、深刻,難以做出更通用的設計。

業務抵禦:平臺框架守衛

那麼,爲什麼關注 DDD?如果說 DDD 直擊了軟件複雜度的核心——“問題域”,那可能還是比較抽象。具體來說,因爲這的確符合我們追求的價值觀【提升長期的生產效率】:

  1. 細分領域,培養專業的人、事:因爲 DDD 的核心是要求讓各個領域做好理解和封裝,把一個業務需求拆解、安放在各自合理的地方,通過這樣的分解與沉澱,保證了領域的輸入,能夠得到長期可持續的發展,形成競爭力。

  2. 機制保障,不依賴易變的事物:DDD 其實在總結很多通用的技巧和經驗,能夠讓這樣的實施更具有確定性。無論是聚合根對領域實體的管控能力、限界上下文的交互策略、領域內核的抽象地位... 等等,一旦選擇尊奉,確定下來,就能夠落在代碼結構、組織關係、團隊文檔中,形成共識,不會因爲人員等因素的變化而劇烈波動。

對 DDD 的關注,可以類比於我們對 “工匠精神” 的關注,對 DDD 的重視,也是我們對業務理解的重視。貼近業務,更要理解業務,不僅要理解業務,更需要理解大多數的業務。這樣的追求,讓我們往上看了一個層次,迴歸了最本質的問題:我們要解決什麼問題?如何能夠解決得更好?

學習 DDD 的困難

不知道大家是否有這樣困惑:DDD 的學習過程好像是”大海撈針 “的過程。即使能夠撈到點東西,使用起來,還是會有種“東施效顰” 的感覺,並不是很自然。爲什麼學習 DDD 那麼困難呢?

感受:不得其門

論語中的下面這個場景,和我們的困惑是比較相似的:

叔孫武叔語大夫於朝曰:“子貢賢於仲尼。” 子服景伯以告子貢,子貢曰:“譬之宮牆,賜之牆也及肩,窺見室家之好;夫子之牆數仞,不得其門而入,不見宗廟之美、百官之富。得其門者或寡矣,夫子之雲不亦宜乎!”

正如要感受到孔子達到的境界,自己的學問也需要有一定的積累。我們要感受到 DDD 的力量,自己本身就要成長到一定程度(如:經歷了一些成功或者失敗的設計,有自己的經驗或者教訓),才能形成共鳴和認同。

工作中,的確很少看到 DDD 的最佳實踐。在複雜的業務面前,誰也沒有勇氣說,哪個軟件結構是理想的設計:

  1. 因爲這不是一個確定性的問題分解,你的設計會被放在顯微鏡下研究,總能找到各種反例。

  2. 而且,我們深知,最佳的實踐,一定是做得足夠的 “軟”,對擴展留有設計,能夠隨着業務發展而迭代,不是一個靜態的結果。

因此,打開學習的大門不是幾個案例就能一蹴而就的,需要結合我們自己的工作,慢慢累積、體會。

困難:模式發散

我們有一種困惑,到底怎麼樣算是 DDD:

  1. 實踐的個例,難以信服:當我們看到”DDD 實踐 “的時候,可能會發問:這也算 DDD?不就是一個正常的服務端框架與方案,也無法涵蓋其它的場景或者部門系統。

  2. 抽象的理論,覺得空洞:當我們看到” 抽象的 DDD 定義與策略 “的時候,可能會發問:這也算 DDD?不就是一些軟件設計的共識,然後強加了一些名詞定義,有些策略與我們手上的系統也並不匹配。

無論往抽象看,還是往具體實現看,都很難找到令人信服的理論與實踐(能夠有確定性的落地能力)。因爲這不像 23 個設計模式那樣,可以通過 N 個模版就能涵蓋大多數的模式。

爲什麼不能產生特定的模式呢?可以結合下圖進一步來看:

  1. 抽象理論:如同抽象的接口一樣,"DDD 理解" 最上面的學習主要是理論定義,比如:聚合根、值對象、資源庫、核心域、支撐域等各種定義,這些是易於理解掌握的。

  2. 通用實踐:如同相對具體的抽象類一樣,"DDD 理解" 中間層次是一些通用原則和技巧,比如:上下文的映射策略、架構的選擇等。這些因素是確定的,但需要自主進行取捨與選擇,並且需要與時俱進,增量的部分需要你自己學習補充。

  3. 具體實踐:如同具體的類實例一樣,"DDD 理解" 中下面層次是一系列的具體實踐,結合各自的業務場景,進行了不同因素的設計、取捨與補充。因爲涉及的選項較多,造成最終的選擇結果往往是發散的,令人感覺 “千人千面”。

兩者不同的地方是:

  1. “代碼抽象層次” 中層次關係是比較明確的,且有約束。

  2. “領域驅動理解層次” 中無法提供明確的約束,都是多個策略的取捨、一些關鍵的建議。

因此,由於問題的抽象層次較高,各種策略的不確定性較高,很難在 DDD 中產生像”23 個設計模式 “那樣精煉的模式。一定要有的話,也是一系列的模式,比較發散。

因此,我們漸漸明白:DDD 面向的是軟件的 “軟”,涵蓋方方面面。DDD 的深入理解,需要“千錘百煉” 後,才能明白那些深入簡出的建議,才能體會那句 “師傅領進門,修行靠個人” 的真言,才能感受到 “衆裏尋他千百度,那人卻在燈火闌珊處” 的美妙瞬間。

基於設計原則看 DDD

雖然 DDD 本身的實踐可能千人千面,但是一些核心主題的思考應該是聚焦的,這些高頻主題的理解,能讓我們更好地進行設計,討論的性價比也是較高的。接下來,會基於 “6 大設計原則”(solid 原則)爲引子,去看看 DDD 中的一些關鍵理解。

單一職責原則:領域劃分

單一職責是說:一個類應該只有一個發生變化的原因。職責的單一,可以更好地內聚,減少耦合,方便演化。

DDD 裏面的領域劃分可以類比思考。對領域的劃分,無論是按領域實體,還是按照功能模塊,還是按照服務等劃分,其實都想盡量保證領域的正交,能夠獨立演化和發展。



Single Responsibility Principle:單一職責原則

領域怎麼切分比較合適?剛進入業務平臺的時候,瞭解到領域切分是按 “一個或多個實體對象” 的邊界來切分。這的確比較合理,因爲領域的核心職責就是對領域實體進行管理。但這是果,還是因?在切分的時候,是因爲我們有了對領域的判斷,所以某些實體被分在一起比較合適;還是因爲某些實體有明顯邊界,所以可以形成一個領域?就比如下面的圖:

  1. 可以整體作爲 1 個部分。

  2. 可以按豎着的正、負切分 2 個部分:上面 1 個(紅),下面 2 個(黃、綠)。

  3. 可以按橫軸的正、負切分 2 個部分:左邊 1 個(黃),右邊 2 個(紅、綠)。

  4. 可以切分成 3 個部分(紅、黃、綠)。

聚類的例子

這的確引入深思。切分比較容易的時候,往往是因爲已經有了行業的標準(如:電商系統有訂單、支付、物流、庫存等領域是比較合理的)。那行業的標準來自哪裏?是來自於演化:

  1. 開始的時候,可能只是一個大交易,比如:支付開始的時候只是買賣雙方自己協議,也不需要建模。

  2. 後面支付發展了,也就獨立出來一個域。原來不需要專人維護,後面會漸漸拉出一個團隊來承接相關研發。

所以,領域的演化和劃分,很類似 “啓發式算法”(一個基於直觀或經驗構造的算法,在可接受的花費下給出待解決組合優化問題每一個實例的一個可行解):

  1. 初始化:按照的經驗初步的劃分,也可以是行業標準(沒有行業標準的時候,就只能靠經驗了)。

  2. 花費評價:生產、交付過程的人力成本度量,關注理解成本、開發效率、系統穩定性、運維成本等因素。

  3. 更優解:在業務發展過程中,計算花費評價,分析影響評價的 “好因素”、“壞因素”,進行進一步調整。

往往到最後,我們會發現:

  1. 調整的內容:其實是匹配生產關係。

  2. 調整的原則:追求職責的內聚,精細化分工。

  3. 不斷調整的原因:業務在發展,內聚的標準需也要與時俱進。

此外,從關聯的角度看,往往我們看組織架構,就能看到領域的邊界,核心原因還是組織架構也是要適應生產關係,follow 更優解的結構,是相輔相成的,也就能互相窺探。

開閉原則:實體行爲

開閉原則是說:軟件中的對象(類、模塊、函數等)應該對於擴展是開放的,但是對於修改是封閉的。也就是說,對擴展區塊是要有設計的,擴展的部分不應該影響穩定邏輯。

在 DDD 中,實體的行爲,在保證對外封閉的情況下,也是需要考慮擴展能力的。

Open Closed Principle:開閉原則

在剛開始學習 DDD 的時候,我們可能會強行把一些邏輯放到實體中,進行控制和收斂。但後面隨着業務的變化,會發現在實體中承擔行爲邏輯很難受:

  1. 影響較大:很難有勇氣去頻繁地修改一個核心類。

  2. 過於集中:隨着方法和邏輯的增多,實體越來越臃腫。

  3. 場景較多:很多邏輯並不是正交的,不是 if 這樣 else 那樣的,充滿着交集與疊加。

拋棄 POJO 的 get、set,走向實體的豐富行爲,讓我們編寫代碼更加困難了麼?其實,我們的煩惱來自於,太關注實體行爲的收口,忽略了擴展的設計:

  1. 原來 get set 寫法很舒服的本質在於,很多的擴展被放在了業務腳本中,業務腳本雖然千瘡百孔,但是是在應用層,遠離核心邏輯。底層模型、通用組件等基礎邏輯還是比較乾淨的。

  2. 應用 DDD 的時候,把一些行爲下沉到領域層之後,也是要考慮擴展的。如果只關注收口,不關注擴展,那的確是 “畫地爲牢”、“撿了芝麻,丟了西瓜”。

但是,要突破這一個困境,能夠在實體行爲中設計擴展,其實要有這樣的認同:要往上看一個層次,就是實體行爲的表達,不一定只有一個類完成,可以通過策略模式等方式的路由,由一個模塊中的一些類進行完成,只要對外有封裝和管控其實就可以了。

突破一個類的限制,走向更多的類的協作設計,也是我們進階的方向。

里氏替換原則:資源庫

里氏代換原則是說:任何基類可以出現的地方,子類一定可以出現。講究的是合理的抽象和複用。引人深思的一個例子是:正方形是特殊的長方形,正方形如果作爲長方形的子類,那麼當設置長度的時候就會出現衝突,長方形的長和寬可以獨立設置,正方形的長和寬是有約束的,使用繼承的關係就比較彆扭。

在 DDD 中,關於可替代性,想聊一聊資源庫。

Liskov Substitution Principle:里氏替換原則

在原來的分層的架構中,數據庫等存儲能力作爲一種底層基礎設施,是被視爲穩定的下層服務的。但在實際的交付過程中,往往要遇到不同場景:

  1. 本地部署:線下零售交易爲了服務穩定性,期望可以具備本地保存數據的能力。

  2. 雲上產品:售賣給外部企業的交易產品,成本的要求也不盡相同,期望在雲上採購不同的存儲服務。

這些場景,讓大家逐漸深信:當面向更廣闊的市場,基礎設施也是充滿着不確定性,需要具備可替換的能力。

在具體實現的過程中,並不是每個領域都會獨立部署,有些領域因爲組織、性能的因素會一起部署。往往這些領域的代碼也是在一個項目模塊中的。出於橫向效率的考慮,會設計統一的存儲框架。

不同設施的存儲能力不同,但整個存儲流程又是類似的(協議轉換,存儲語句生成、執行與事務,返回結果),這樣在不同存儲能力的過程複用方式上需要進行取捨(數據庫、Tair 等是分開抽象還是統一抽象):

  1. 如果 “大一統” 爲主,那麼針對關係型存儲、KV 存儲等不同存儲進行抽象的時候,就會和 “長方形與正方形的問題” 一樣猶豫:

    收益:如果你長期維護,瞭解裏面的特殊性,的確是可以省略一些主體代碼,提高開發效率。

    代價:但大多數人要做擴展的時候,會感到很多不解,有很多本不需要的適配,充滿迷惑。

  2. 如果以組合爲主,那麼可以通過多套模版,更好地進行自主選擇。這樣分而治之,減少了大家的理解成本,也能獨立演化,更能適合存儲的能力特性。但是需要沉澱多套理解,往往也缺少人力支持。

我想,“基於不同的存儲能力,設計不同的模版框架” 應該是首選。大一統的抽象,開始時,人力成本可能低一點,但因爲抽象層次較高,在理解與維護上將是一個 “人力成本黑洞”,隨着時間的推移,會降低整體收益,長期看是得不償失的。反之,不同的模版複用,最終可以獲得更好的整體收益。

迪米特法則:領域協作

迪米特法則,又稱最小知識原則,是說:一個軟件實體應當儘可能少的與其他實體發生相互作用。應該和一些 “關鍵類” 進行溝通。

DDD 裏面,領域間的協作,也需要相關的規劃和設計。如果對領域之間的相互調用不做管理,那麼鏈路關係會膨脹到難以理解的地步。

Law of Demeter:迪米特法則

設計模式中,無論是中介者模式,還是外觀模式,都希望通過集中管理,讓多對多的複雜關係,簡化爲多對一、一對多這樣易於理解的結構。類似的,在領域協作與對外交付的過程中,往往可以增加一個協調層,去串聯各個域的交互。這樣即可以降低各域的協作成本,也可以降低外部的理解成本,有更好的研發體驗。

協調層該如何產生?好比上課:雖然老師可以教,但是老師不在時,可以指定學生代理上課。學生雖然可以幹,但教學技巧並不熟練,自己本身還有學習的職責,角色也很尷尬。下面將討論一下協調層和域的角色關係。

比較值得討論的是交易裏面 “交易域”、“訂單域” 這樣的概念:

  1. “交易域” 看上去是負責整個交易過程,可以協調各個域,邏輯上比較合適成爲協調者,但是主要還是在管理訂單,其它賦予的協調能力,這部分並沒有領域實體。

  2. “訂單域” 看上去只是負責訂單本身的服務,不太關心其他域,但是因爲訂單合約上有着所有合約信息(無論是直接還是間接持有),這意味着,“訂單域” 本身就有協調的潛力,只是職責看上去不夠單一而已。

沒有實體,爲什麼會有 “交易域” 一說,本人是這麼理解的:在交易流程等可以強管控的情況下,把交易的 API 服務當做域服務(如:下單),“交易域”在邏輯上是有邊界、可以成立的。但本質還是在管理訂單,靠訂單域成爲了域,同時想沉澱協調能力。

那麼,如果訂單的模型的管理不給交易管理呢,就是本人一直想的問題:如果沒有自己的數據庫實體,只有內存模型,純粹靠對下游業務活動理解、數據流轉的理解,能否成爲一個域?

答案大概率可能是不行的:

  1. 邏輯上個人是認可純靠理解作爲一個域,畢竟知識本身也是一種資產。

  2. 但實際上,沒有載體,就做不了特別多事情,包括狀態記錄,數據服務等,只能輔助,沒有核心競爭力,難以生存下來。

協調者的角色,想要成爲一個比較公認的 “域”,必定要自己持有數據模型,或者,藉助基礎域的一些數據模型,並享有管理的權限。

無論是域想承擔協調者的角色,還是協調者想發展成一個域,其實都不太符合職責單一的邏輯,但是這樣 “兼職” 的現象卻時常發生,核心還是開發人員角色的重疊。

既然協調層不太適合從域中選出,也不太適合成爲一個域,那麼介於業務活動、各個域能力之間的協調部分,應該稱之爲什麼呢?目前看 “商業能力”、“解決方案” 這樣的詞彙都是比較合適的。

接口隔離原則:業務活動

接口隔離原則是說:客戶端不應該依賴它不需要的接口。一個類對另一個類的依賴應該建立在最小的接口上。

DDD 裏面除了領域建設的學習,也需要關注應用層如何更好地承接業務請求,並研究業務邏輯分割的依據。

Interface Segregation Principle:接口隔離原則

之前在業務部門做業務的時候,並沒有業務活動、流程等相關概念,往往是基於業務需求寫業務腳本,經驗的多少會影響代碼的優雅。但除了經驗,大家並沒有比較好的結構框架、原則,去承接應用層的各種業務邏輯,因此也充滿疑惑:

  1. 對外服務接口應該如何切分?

  2. 類似服務之間是否可以共用流程?

  3. 業務執行過程如何進一步結構化切分?

  4. ......

沒有規範的結果是,往往各有各的看法,誰都想立一套結構,誰也不服誰起的一套,各有各的代碼區域。

現在工作中,因爲在平臺,平臺思考和治理的時間也比較久了,也有比較穩定的共識。整體的設計,在業務入口和業務入口之間、業務入口和穩定邏輯之間,預留了空間和擴展能力去承接場景化的邏輯,結構也比較確定:

  1. 入口按業務活動切分:服務入口按照業務活動擴展,核心理解並關注用例,什麼角色要幹什麼事情。

  2. 流程獨立承接與編排:業務活動在到達複用層(如:領域服務、外部服務、業務擴展)前保持獨立,各自編排。

  3. 藉助流程文件編排執行過程:將執行過程切分爲執行節點,節點的切分可以以 “功能點”、” 涉及的域“、“涉及的實體“等爲依據。節點間的共同能力下沉到基礎能力中。

  4. ......

分而治之,縮小大家的關注點,更好地切分協作,這樣的確很容易理解並接受。但是要保證合理的切分,還是要有統一的共識和原則。

往往一致的形成,不是先一致共識,再切分,而是初步溝通後就嘗試切分,再 review 達成一致,在曲折中前進。如果大家的看法、衝突較大,那麼這個共識達成的過程就相對較慢。好在,這種切分也不是時常發生,也就大需求、大重構的時候。此時,預留的研討時間、開發資源也是充足的。

依賴倒置原則:六邊形架構

依賴倒置原則是說:程序要依賴於抽象接口,不要依賴於具體實現。

DDD 裏面提到的六邊形架構,也是進一步提升了抽象內核的地位,把領域建設作爲架構的核心目標。

Dependence Inversion Principle:依賴倒置原則

以領域爲中心,其實是一個比較重要的轉變:

  1. 原來以分層架構爲主:講究按層次去看,儘量將能力下層,進行更多工具複用,積累的是通用組件。

  2. 現在以領域爲中心:講究按抽象層次去看,儘量將理解融入到領域核心,進行更多 “理解” 複用,積累的是業務知識。

這樣的轉變,讓我們有意識地將 “領域理解” 作爲核心,形成行業競爭力,把 “知識” 作爲資產進行售賣。

爲了保證領域內核的抽象,需要定義好領域內核的邊界,有兩類接口:

  1. 對上游提供的能力:通過接口聲明,說明能承擔的職責,在領域內部進行實現支撐。

  2. 對下游的能力依賴:外部服務、業務擴展定製、存儲服務都可以作爲下游服務看待,通過接口聲明服務依賴。

可以看到,領域內核與外部之間通過接口進行解耦。對於更基礎的服務,會被視爲和業務入口一樣的外部端口,屬於應用層。比如,存儲服務:

  1. 原來更多的是基礎能力:數據框架 + DO,不需要理解轉換,轉換在上游完成,DO 也會作爲核心模型被上游使用,在採用遵循模型策略的時候,上游完全使用 DO 作爲核心對象進行流轉。

  2. 現在可以理解爲 “業務組件 “:需要實現領域的存儲接口,承擔協議轉換,將領域對象轉換爲數據存儲對象 DO,DO 也不會被領域直接理解,需要轉換爲領域對象再往外透,被領域內核定義了表現。

這樣的架構,奠定了領域理解的抽象核心地位,讓研發同學更加註重對業務問題的思考,建設更多 “有血有肉”、“貼近業務核心問題” 的軟件,不僅僅是“基礎骨架”,讓我們更加走近客戶的價值,是應對軟件複雜性過程中,大家比較認可的方向。

總結

對 DDD 的追求,來自我們渴望優雅地解決各種業務問題,希望有一套框架可以引導我們去分解問題,得到穩定、高效的生產效率。

但是這好比對 “永動機” 的追求,是一個難以有肯定答案的過程。能夠解決軟件複雜性的方案,必定是結合相關場景並且不斷演化的,單純追求 DDD 是得不到 “銀彈” 的。

不過正如對 “永動機” 的研究,能讓我們關注能量的轉換過程,可以引導我們製造出更加高效的能源機器。對 DDD 的研究與追捧,能夠讓我們更加關注對業務的深刻理解,可以引領我們寫出更加易於擴展的代碼實現。

我想,正是 “業務的不斷髮展”、“軟件的複雜性” 的存在,才讓編程充滿了挑戰,才讓大家對框架的研究充滿熱情,這何嘗不是一個美妙的事情。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/l5B8ERHZVajipHVwtyMDXg