前端領域驅動設計的一些思考

什麼是 DDD

領域驅動設計(Domain-Driven Design,簡稱 DDD)是一種面向對象軟件設計方法,其目的是將軟件系統的核心業務領域(Domain)抽象出來,並以此爲基礎進行設計和實現

領域驅動設計的核心思想是將領域模型作爲軟件設計的中心,通過對領域模型的深入理解和設計,提高軟件系統的可維護性、可擴展性和可重用性。領域模型是描述業務領域中重要概念、實體、關係和操作的一組對象和方法的抽象表示

DDD 主要解決什麼問題

DDD 旨在解決業務邏輯的複雜性,而業務邏輯大部分場景下是不存在於前端。業務邏輯往往包含大量的業務規則和約束。這些業務規則通常是在後端實現的,因爲後端需要處理數據的驗證、處理、計算和存儲等

DDD 適用於前端嗎

首先上面提到了 DDD 主要解決的是複雜業務場景邏輯問題,那麼 DDD 是否適用於前端的一個核心要素就在於:複雜的核心業務邏輯是否存在前端?

我認爲是大部分情況下複雜的業務邏輯是不在前端的,也就是說 DDD 大部分情況下是不適合前端業務

因爲業務邏輯是高層級的策略,其他所有東西都依賴於它。此外,一般來說我們需要保證業務的穩定性、可靠性、可擴展性、可維護性。如果將業務邏輯放在前端,可能會導致多端之間的數據不一致或者邏輯不同步,這可能會對用戶體驗和軟件的可靠性造成影響

然而,我們也不能完全排除在前端使用 DDD 的可能性。在一些複雜的應用中,前端可能需要處理一些業務邏輯,比如業務表單校驗規則、權限控制規則等。在這種情況下,DDD 的一些思想和方法可能有助於組織前端代碼,使其更易於理解和維護

前端是低層級細節

就電商系統軟件架構而言,前端通常被視爲一個低層級的細節,相對而言較易變。因此,前端在採用新的技術棧時相對容易廢棄原有技術體系(比如我們商家端的業務從低代碼語言轉換成 Pro-Code),而不是爲一個新的後端語言廢棄原有的後端,在這裏起作用的因素是穩定性和易變性。

什麼是細節

細節指的是如何實現原則,也就是執行原則的方式,細節是原則的實現。要確定你正在編寫的代碼是原則還是細節的一種簡單方法是問下自己:這段代碼是否是強制執行有關我的業務領域中規則的實現,還是隻是使一些事情得以執行?

什麼是策略

策略是指我們正在編寫的代碼應該遵循什麼樣的規則和原則。主要涉及在我們編寫代碼的領域中存在的業務邏輯、規則和抽象概念。

高層級策略

高層策略(high-level policy)通常指的是在應用程序中貫穿各個模塊和組件的核心業務邏輯和規則,這些邏輯和規則是應用程序的核心價值所在,而且通常是不會輕易改變的。

比如,在一個電商平臺中,核心的高層策略可能包括如何處理訂單流程、如何計算商品最終價格、如何管理庫存等等。這些規則是與具體實現無關的,而且可能需要與其他模塊進行協作來實現。

將高層策略放在後端,可以確保這些規則得到了保護和統一的執行,而且可以通過後端提供的接口和服務來保證數據和邏輯的一致性。與此同時,前端可以專注於展示和交互層面的處理,將高層策略與具體實現分離開來,使得應用程序更容易維護和擴展。

策略和細節的關係圖

對應到前端的策略和細節的一個結構圖如下所示:

在軟件架構中,我們可以將其分爲兩個層次(領域和基礎)

在領域層中,我們擁有所有重要的東西:實體、業務邏輯、規則和事件。這是我們軟件中不可替代的部分,無法簡單地使用另一個庫或框架替代。

而在基礎層中,則包含了所有用於執行領域層代碼的實際實現。

前端很難具備穩定性

從上文中我們可以知道領域層是具備了最高級別穩定性和**策略,**這是因爲領域層包含了能夠貼切描述你的應用系統業務邏輯和運行方式的領域模型代碼,通常來說當前業務模型不會發生重大的變化,這意味着描述這層業務的領域層代碼也不需要進行大的變化,所以一般來說領域層是穩定性最高的。

依據穩定依賴原則,穩定的模塊是我們可以依賴的,將不易變的模塊組織成依賴於穩定模塊的結構是有意義的,但永遠不要讓穩定模塊依賴於不穩定的模塊

然而,UI 層的複用性通常較差。前端 UI 需要在多樣化的設計稿中進行開發,導致代碼差異化無法收斂。不同的用戶心智、設計語言、業務背景、以及業務服務,都會對前端 UI 邏輯造成非常大的影響。舉個例子,不同業務線的後端服務請求響應數據結構差異化可能直接導致數據處理邏輯無法複用。在一些 C 端場景中,這種情況尤爲突出,比如電商、社交等。

針對面向 B 端的前端,目前業界已經有了一些常用的組件庫,例如 Antd、Fusion、MerlionUI 等等。這些組件庫已經具備了高穩定性,即它們已經定義了 B 端前端的基礎組件標準和基礎層,大部分情況下不會進行大的變更。然而,由於 B 端業務場景的差異性,前端在 UI 層上仍需要大量的業務組件和視圖層的工作量。例如,在電商網站下單的訂單模型中,面向買家用戶時展示的是以買家用戶爲中心的訂單處理狀態和履約進度信息,而在面向賣家用戶時則需要展示對這筆訂單的狀態流轉的標準操作流程。

前端很難複合開閉原則

通常而言當需要更改某個功能時,前端開發人員通常需要直接修改代碼,而不是添加新的功能或模塊。假設我們在開發一個商品詳情組件,可能需要展示商品的名稱、價格、描述、圖片、評論等信息。這些信息是所有商品都需要展示的,所以可以將它們定義爲穩定層的核心規則邏輯。

但是,在不同的業務場景中,可能需要對商品信息頁面進行一些定製化的展示,比如在大促活動期間需要展示大促標籤和氛圍圖,或者在跨境電商業務中需要展示關稅和物流信息,再或者當我們商品詳情展示在不同國家和地區的時候商品名稱和價格的位置會發生變化,而這些業務規則屬於易變的低層級細節,但是往往在業務量比較小、低層級的業務規則沒法隱藏到穩定層的時候,這部分工作量往往就會落在前端身上,最後前端視圖層和業務組件層會有大量的業務規則邏輯判斷。通常而言這種方式違反了開閉原則,因爲它需要修改現有的代碼來實現新的功能,而不是擴展功能模塊,這就是因爲我們將所有高層級策略放在後端並確保前端不包含高層級策略時所做的工作。

在這種情況下,前端可以通過配置文件或者運營控制檯等方式來配置這些低層級細節規則,而穩定層的商品信息組件可以通過這些配置來實現不同的業務場景的展示需求。

前端業務複雜度主要在哪

前端業務複雜度主要包括但不限於技術棧的複雜度業務邏輯的複雜度UI 交互的複雜度等。

技術棧的複雜度在哪

通常而言我們所說的前端技術棧泛指:Vue、React、Angular、JQuery 等基於 MVVM、操作 DOM 的技術棧。

爲什麼這麼說?因爲前端框架其實本質上是高策略層級的,每個前端框架的一般都是來解決以下問題:

所以當你選擇好一個框架之後,其實你就已經是在這個高層級策略下面執行低層級細節的編碼。舉個例子 Vue 和 React 實現狀態變化檢測和 Reactive 的策略是不一樣的,對於開發者而言在這個策略下的實際編碼思想也是差異巨大的,Vue 是基於 Proxy 來做雙向綁定,而 React 是基於調度更新算法來更新 vdom 樹

React 帶給我們的編程範式是函數式編程 *(函數式編程 Functional Programming 是一種編程範式,它的核心思想是使用純函數來進行編程),當我們選擇了 React 這套 UI 框架和生態之後,我們天然寫出來的代碼就是基於函數式的。爲什麼是函數式的?背後的原因實際上是因爲 React 原生的響應方案,也就是監測變量引用(reference)的變化,然後整個子樹去協調更新。

函數式編程具備幾個特點 純函數、不可變性、函數組合。 React 響應方案因爲要保證在輸入 (props) 是一致的情況下,輸出 (vdom) 的結果也是一致的。所以我們對 React 狀態邏輯的封裝大部分也需要滿足這個特性,這也是爲什麼我們在組件內部要通過 setState() 而不是 state.xxx 來變更狀態,這也就是我們通常所說的 ** 狀態不可變性。*

另外函數式編程又幫我們解決了組件的之前的組合問題,一般來講我們基於 React 來開發頁面的模式一般是:Page = Compose(ComponentA + ComponentB + Fusion/Antd)Component = Compose(Fusion/Antd + React hooks + Events + State) 而這種組合的特性在業務層如果沒有一個比較好的組件依賴原則的話,會導致組件之間耦合比較嚴重,又因爲組件內部的複雜度也是 compose 的各種 “組件”,所以當系統內的各種 "組件" 的依賴關係越來越複雜的時候,甚至“組件” 之間的依賴出現環的時候,業務系統的複雜度就跟着線性遞增了

總結一下在業務前端應用中技術棧的複雜度主要體現在以下方面:

  1. 組件和模塊的組織:在組件化和模塊化設計中,如何組織組件和模塊,使得它們的依賴關係合理、清晰,以保證代碼的可維護性和可擴展性。

  2. 狀態邏輯的組織和管理:在大型前端應用中,狀態管理是一個重要的問題。狀態管理需要考慮狀態的一致性和可變性,以及如何處理狀態的變化。

  3. 異步數據處理:現代前端應用需要處理大量的異步數據請求和處理。異步數據處理需要考慮異步數據的請求和響應、數據緩存、數據更新和狀態管理等問題。

業務邏輯的複雜度

業務邏輯的複雜度通常來自於業務需求本身,例如業務規則、流程、數據處理等。業務邏輯的複雜度可能因業務領域的不同而有所不同,例如電商、本地生活、直播等領域都有各自的業務邏輯和複雜性。

在前端開發中,業務邏輯複雜度可能表現爲需要進行大量的數據處理、業務規則的驗證、複雜的頁面流程設計等。在前端中,如果沒有一個清晰的業務邏輯劃分和抽象,代碼可能會變得非常複雜,難以維護和擴展。因此,在前端開發中,對於業務邏輯的劃分和抽象非常重要,這樣才能更好地應對業務邏輯的變化和複雜性。

UI 交互的複雜度

UI 交互的複雜度主要在於如何實現複雜的交互邏輯和動畫效果,以及如何處理用戶的輸入和反饋。具體來說,UI 交互的複雜度主要體現在:用戶體驗設計、跨端兼容性、性能優化

怎麼降低前端業務複雜度

這裏我們主要重點關注怎麼降低技術棧和業務邏輯的複雜性帶來的複雜度。

文章的開頭我們講了,領域驅動設計的主要目的是爲了解決業務邏輯的複雜性。 領域驅動設計的核心思想是將領域模型作爲我們業務架構設計的中心, 實際上來說我們只是需要藉助領域驅動設計的一些思想在前端業務開發中進行實踐,前面提到在我們前端業務工程會出現大量不滿足組件構建的無依賴環原則,這裏主要的原因是因爲前端開發者在以 UI 層爲中心進行開發,而如果我們切換視角以 ViewModel、Model 爲中心來構建我們的前端業務的話,整體系統設計思路會發生變化

領域模型

大部分前端代碼與實際業務領域無關,這部分前端主要專注在表單驗證、API 請求、事件響應、列表渲染等等,然而也有部分業務的前端也確確實實跟業務領域相關,領域業務模型也確實會影響到 UI 層。領域模型通常是一組具有業務含義的類或者對象,它們通過方法、屬性等方式封裝了系統的關鍵業務邏輯。無論如何,只要它能被系統中的其他不同應用複用就可以。

通常來說我們可以通過手動創建或者抽象工廠方法構造出模型數據,把這些數據響應式地映射到視圖層,再根據視圖層觸發的事件調用模型層裏的函數或方法來更新模型層數據。

狀態管理

狀態管理主要分兩個部分:一個是管理對業務模型層的可變狀態和不可變狀態,一個管理視圖層的可變狀態和不可變狀態。

視圖層上有些狀態不是從模型層數據裏來的,是純粹的頁面狀態,比如數據正在加載的標誌、下拉框的聯動,等等,這些和模型層無關,且隨着需求的變化而動態變化。

在基於 React 渲染方案中,我們既可以利用 React 原生的響應方案也可以藉助三方庫 (mobx) 的方案來實現這部分狀態管理,選擇的方案不同可能會帶來在編程範式的差異(FP、OOP)

視圖層

視圖層是最不穩定的一層,UI 組件的實現通常受到業務需求的影響,隨着需求的變化,UI 組件的實現也需要進行相應的調整和變更。同時,爲了保持軟件的穩定性和可維護性,需要遵循穩定依賴原則,確保其他基礎組件不會依賴於視圖層。

基於 React 的視圖層又會有伴隨着事件響應、生命週期等等副作用。Hooks 實際上只是視圖層的東西,背後都是依賴於 React 的響應原理,因此,在我看來,Hooks 會通過合併同類項進入視圖層。

架構分層

首先,在互聯網行業中,很難一開始就完成完美的系統設計。相反,系統往往需要逐步發展,通過不斷迭代和引入新的功能模塊來逐步成型。對於現有系統,通過一次大規模的重構難以解決所有問題。好的系統設計需要不斷投入工作並逐步積累細節,最終才能獲得完善的系統。因此,在日常工作中需要高度重視設計和細節改進。

其次,專業化分工和代碼複用是提高軟件生產率的重要手段,此外,同一領域服務可以支持不同的上層應用邏輯。這種分工和複用背後的思想是將系統分爲多個水平層,並明確定義每個層的角色和任務,以降低單個層的複雜性。同時,每個層只需向相鄰層提供一致的接口,可以使用不同的方法進行實現,這爲軟件重用提供了支持。因此,分層是解決複雜性問題的重要原則。

方案一

在這個方案中我們視圖層跟模型層的之間的接口是通過 ViewModel 來進行管理,視圖層依賴 Hooks 和 ViewModel 來進行生命週期、事件、響應方案, 而 ViewModel 中既包含當前視圖層自身的狀態管理也耦合領域服務和領域模型,這個架構設計中不穩定層包含:View、Hooks、Lifecycle、ViewModel,而穩定層包含:Service、Repository、Model。

方案二

在這個方案中我們視圖層跟模型層的之間是直接依賴關係,視圖層直接依賴 Hooks、Model、State。這個架構設計中不穩定層包含:View、Hooks、Lifecycle、State、Model,而穩定層包含:Service、Repository。

文件目錄設計

我們根據上述結構圖的分層思想,在實際項目中定義了以下的文件目錄:

├── shared
│   ├── components // 公用基礎組件, 組件之間不能互相耦合
│   ├── constants // 全局變量
│   │   ├── page.ts
│   ├── domains // 領域層
│   │   ├── page
│   │   │   ├── page.model.ts // 實體
│   │   │   └── page.service.ts // 領域Service服務
│   │   ├── ...
│   └── util // 公用函數
│       └── http.ts
├── components // 公共業務組件,業務組件之間不能互相耦合,但是可以依賴公共基礎組件
├── modules // 模塊視圖,模塊可以是compose(公共業務組件, 公共基礎組件)
└── page // 頁面視圖層
    ├── index
    │   ├── index.tsx
    │   ├── components
    ├── ...

常見問題

問題一: Modules 跟 Components 的區分實際上過於理想化,正常業務開發,很可能不好判斷我當前這個組件是要放 Modules 還是 Components 裏面,甚至使用者都會疑惑我到底是要去 Modules 裏面去找還是去 Components 去找

Module = compose(ComponentA + ComponentB + ComponentC)。如果 ModuleA 只是一個特殊的 ComponentA, 就放到 component 裏面,模塊裏面不耦合太多業務邏輯,純粹的 View 層的 compose。只是這個模塊需要被多個頁面引用,比如: PageA = compose(ModuleA + ComponentD + PageA logic ) PageB = compose(ModuleA + ComponentC + ComponentB + Hooks )

**問題二:**那你的 Components 的顆粒度到底是多細呢?我的 Components 裏面的 Component 能引用其他的 Component 嗎?

不行,關注點分離,架構分層,就是要讓依賴樹足夠清晰,Component 可以依賴 shared/components 也可以依賴 Fusion + MerlionUI,但是 components 之間最好不要互相耦合

在具體選擇方案時,需要考慮業務場景的差異,簡單的業務屬性要警惕把問題複雜化,警惕過度設計,複雜的業務要全面評估和判斷好方案,選擇適合自己的設計方案。

另外兩種設計方案中不穩定層並不意味着這是一個強耦合層,不穩定只是代表這一層中的結構會隨着業務的變更而頻繁變更,我們需要根據業務場景來判斷哪些部分需要轉化爲穩定層,並確保依賴關係結構的清晰和整潔。

總結

根據上文推導過程可知,如果我們要在前端業務工程上深度應用領域驅動設計的思想來實踐最好需要幾個前提

寫在最後:雖然技術在軟件開發中扮演了重要的角色,但任何技術都不是銀彈,作爲工程師、架構師,我們需要對技術選型、架構設計、系統設計進行深思熟慮,並進行全面的評估和判斷。

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