理解 DDD:應用和服務分離

良好組織代碼的關鍵不是將方法劃得足夠小,而是對象各司其職。

架構的本質就是將各種庫、業務代碼、基礎設施等架構的組成部分良好的組織到一起,這是在成爲架構師的路上必須想通的一環。企業架構框架把信息架構分爲四層:業務架構、應用架構、數據架構和技術架構。如何把業務系統中的代碼良好的組織起來,就是我們應用架構中的內容。

應用和服務分離是一個非常簡單的原則,在各個地方都有體現,但是沒有編程大師像 SOLID 原則一樣明確的表述出來,但它又很重要,能給我們一個如何複用代碼的準則。

  1. 複用還是複製

“複用就一定好嗎?”

當我向同事問出這個問題的時候,同事一臉茫然,好像軟件開發本來就應該這樣,所有的代碼都應該儘可能的複用。

複用,在多數人的眼裏已經是理所當然了,但有時候還是忍不住提醒一下,複用只是手段而非目的。

複用是通過消除重複代碼的方式,得到一系列可以重用的代碼片段,在需要的地方組合使用即可,提高開發速度的同時,也可以提高整體的一致性。

顯然,組合組件用的膠水代碼是不需要複用的,因爲組合本身就是爲了解決場景中的事情,不再具有複用價值。強行復用的後果有兩個:

  1. 場景特有的東西被納入組件,導致組件的複用性降低。信息被泄露到組件中,組件和場景中的代碼職責不清晰

  2. 響應業務變化的能力反而降低了,說白了就是不好改。

有時候兩段代碼雖然看起來只有細微的差異,但是也不要複用它們。對於全棧開發者來說,這個原則對我們設計前後端的代碼都有好處。在後端,我們可以使用 DDD 分層中的 application 讓代碼變得更清晰;在前端,我們可以將業務組件分爲 pages 和 components 提升設計。

  1. DDD 中的 application

我們知道,在 Eric DDD 的分層架構中,將系統分爲了 4 層:

  1. 接入層(Interface)

  2. 應用層(Application)

  3. 領域層(Domain)

  4. 基礎設施層(Infrastructure)

在 DDD 社區 《DDD 概念參考》中,對應用層的定義是:

應用層,組織業務場景,編排業務,隔離場景對領域層的差異。

應用層的目的是處理不同應用場景的差異,它被用於不同場景的關注點分離中。例如,用戶下單可能會涉及多個原子的操作,訂單、支付、積分累積等邏輯。

思考一個問題,爲什麼 DDD 中引入了一個應用層。沒有它我們會面臨什麼問題?

如果缺乏應用層(在很多微服務系統中都是這樣的),導致領域服務和場景綁定,複用性大大降低。例如系統接受用戶自己註冊,也可以使用微信登錄完成一個隱藏的用戶註冊。另外一個例子,對於新用戶,系統會爲他贈送一些積分,在沒有應用層的情況下,服務被前端直接調用,於是服務不得不定義來自不同渠道的 API。在下面的示例中,微信自動登錄會比瀏覽器註冊多好一些內容。

隨着對接的業務越來越多,微服務中的代碼的分支條件越來越多。這時,大多數架構師開始思考,添加一層來隔離差異。大多數情況下,我們把這層看待爲編排層,但是這個 “編排層” 着實讓人難以理解。

在一些情況下,大家只是把這層當做一個簡單的代理,大量的和場景相關的邏輯進入了領域層,依然會爲系統帶來麻煩。

我們重新思考應用層,它到底解決了什麼問題呢?

有一個典型的場景,就是管理員和普通用戶,在使用場景的差異非常大,看似是具有不同的權限的同一個操作其實未必是同一個用例。例如,用戶能通過 API 獲得商品列表,管理員能看到未發佈的產品列表。對於沒有經驗的工程師往往會編寫一個 API 然後通過一些權限機制來限制它們的訪問。

注意,這不是權限的區別!

這是用例的區別。

管理員查看商品列表是一個用例,用戶查看商品列表是另外一個用例。

當我們不再把用例混淆的時候,就能理解應用層了。我們重新看待應用層和領域層兩個層次的定位:

領域層,實現具體的業務邏輯、規則,爲應用層提供無差別的服務能力。

應用層,組織業務場景,編排業務,隔離場景對領域層的差異。

當我們能把每層的的職責弄清楚之後,代碼的組織變的如此清晰,而在此之前我們還在靠把代碼劃分的更小來實現的。

  1. 前端中的 pages 和 component

在前端開發中,隨着工程化的發展,開發者把組件劃分的越來越小的時候,也會有類似的問題。

下圖表達了 Store 模式的數據流動關係,對應的實現有 Redux、Vuex。

從技術的角度看,它的邏輯非常清晰,但是在實際的工程項目中會有一點小問題。

Action 的發生是從 Menu 等這些基礎組件中發出的,也就意味着,Menu 組件和全局的狀態聯繫到一起,這個時候 Menu 組件的複用性就降低了。

換個例子,設計一種彈窗組件,這個彈窗組件和全局的 store 數據聯繫到一起的話,如果想要做到基礎的組件在各個地方乾淨的使用,那麼狀態的承接工作就不應該由基礎組件來完成。

我經歷過幾個項目,設計者沒有意識到這個問題,帶來的後果就是,組件爲了複用不得不寫很多條件語句。比如模態彈窗不得不使用枚舉來區分是那個用途的彈窗。

問題的關鍵同 “應用和服務分離” 類似。如果頁面用於承載狀態,組件用於複用,那麼兩種組件具有了清晰地定位:

pages,用於承接頁面狀態,和後端通信等業務邏輯。

component,用於承載 UI、交付邏輯,需要通過參數、事件和 pages 傳遞數據。

  1. 總結

複用是一門藝術,需要敏銳的眼光從變化中找到不變,這些不變的纔是真正需要複用的。在創業性質的項目中,這點尤爲重要。我們會發現,在業務成熟的公司裏,他們的代碼複用程度更高。

考慮到業務變化很難預測,對於開發人員可以做到的是:當業務劇烈演進時,分而治之;當業務成熟穩定時,抽象統一。

參考鏈接

  1. [1] DDD 社區. DDD 概念參考 [EB/OL].https://domain-driven-design.org/zh/ddd-concept-reference.html.
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/iBNJinKCnm7SxsRHM4FGZg