淺談領域驅動在前端的應⽤

領域驅動是⼀種思想,不僅可以應⽤於軟件開發,也沒有絕對的開發規範,適合⾃⼰的業務和團隊背景就好,我們不是爲了應⽤⽽應⽤,⽽是爲了解決問題。

領域驅動介紹

DDD 這個詞⼉,來⾃ Evans Eric 在 2003 年的⼀本書《Domain-Driven Design: Tackling Complexity in the Heart of Software》[1]。

在這本書中,Evans 提出了他對軟件的複雜性來源的⼀個關鍵洞察——軟件模型跟領域模型的不匹配,並提出他的解決⽅案(即 DDD)。

軟件模型即我們開發對業務理解之後劃分的代碼組織的模型,領域模型則來源於領域專家(或產品同學),⼀般來說,後端同學更加註重開發模型的劃分,但也會存在與領域專家劃分的模型出現出⼊的情況。

領域

領域具體指⼀種特定的範圍或區域,也就是業務範圍。對於如對⼀個圖研發平臺來說,業務範圍就是圖業務相關的圖數據、圖模型、圖計算等。那領域模型如何劃分呢,這就涉及到領域驅動的核⼼知識體系了。

核⼼概念

具體包括:領域、⼦域、核⼼域、通⽤域、⽀撐域、限界上下⽂、實體、值對象、聚合和聚合根等概念。下⾯這張圖可以很好地體現他們的關係:

其實我們不必過多糾結這些概念到底是做什麼意思,除了⽀撐域、 通⽤域和核⼼域,其餘的概念可以⽤⼀個關鍵詞來概括:邊界,只是不同邊界的業務範圍⼤⼩不同。

⽀撐域、 通⽤域和核⼼域

核⼼域

決定產品和公司核⼼競爭⼒的⼦域是核⼼域,它是業務成功的主要因素和公司的核⼼競爭⼒。

例如在圖研發平臺中,圖項⽬、圖計算、圖數據、圖配置、圖運維等是圖研發過程的核⼼內容,應該劃分到核⼼領域。

⽀撐域

不包含決定產品和公司核⼼競爭⼒的功能。

例如在圖研發平臺中,收藏中⼼的功能是⽤來收藏圖模型、圖查詢語句等的,這種功能對於圖平臺的⽤戶⽽⾔,很明顯不是核⼼的功能,類似這樣的功能就可以劃分到⽀撐域。

通⽤域

沒有太多個性化的訴求,同時被多個⼦域使⽤的通⽤功能⼦域是通⽤域。

例如在圖研發平臺中,權限系統、⽇志系統、⼯單系統等是所有⼦域都可能需要的基礎通⽤能⼒。

⼦域

以上的邊界範圍和⼦域相同,⼦域並不是固定的,可以根據具體的業務情況進⾏劃分。

領域劃分

確定好核⼼域、⽀撐域和通⽤域之後,便可以對⼦域下的內容進⾏進⼀步的劃分,即界限上下⽂、聚合和實體。

界限上下⽂

每個⼦域下可以有多個界限線上下⽂,限界上下⽂定義了⼀定業務範圍的邊界,確保每個上下⽂含義在它特定的邊界內都具有唯⼀的含義,例如圖研發平臺的圖項⽬是核⼼領域下的⼀個界限上下⽂,那就意味着所有跟圖項⽬相關的業務內容,例如圖項⽬信息、圖項⽬列表、圖項⽬的創建等,都可以且僅可以在圖項⽬內找到。

聚合和實體

每個界限上下⽂下可以有多個聚合,聚合由多個實體組成,實體是多個屬性、操作或⾏爲的載體,例如圖研發平臺的圖項⽬下圖項⽬信息確定爲⼀個聚合,項⽬的信息就可以作爲這個聚合的實體,⾥⾯可以包含項⽬信息的屬性定義、獲取項⽬信息的⽅法等。

領域模型

通過領域的劃分,我們將得到⼀個領域模型,這個模型即業務的知識體系,有了這個模型我們可以獲得什麼好處呢?

如何驅動

有了領域模型已經可以獲得⼀定的收益,但這並不是我們的 Y 終⽬的,我們希望可以通過領域模型來驅動我們進⾏軟件的開發,其實開發模式並不固定,適合就好。接下來會詳細講解在圖研發平臺中我們是如何借鑑領域驅動思想進⾏前端開發的。

⾯臨的問題

爲什麼要⽤借鑑領域驅動的思想,⾸先看下圖研發平臺前端⾯臨的問題:

業務邏輯複雜

有些業務場景確實本身就很複雜,⾯對複雜的業務,會有如下問題:

  1. 如何合理地將業務進⾏拆分,從⽽降低代碼實現的複雜度,且保障後續的易維護性。

  2. 新⼈⾯對複雜地業務如何快速瞭解,如何快速適應開發且能保證開發質量。

業務理解不夠深⼊全⾯

我們常常會按⻚⾯或者模塊分配任務,短⽽快的迭代節奏,使得被分配到任務的同學很難對其他同學所做的模塊有較深⼊的瞭解,後續可能也不會去看其他同學的代碼,每個⼈接觸的業務都是被切分的,這種形式不利於組內同學對業務的理解。

難形成統⼀邏輯代碼書寫規範

這⾥的規範是實現業務邏輯的位置、⽅式的規範,在不加以約束的情況下,我們很容易看到業務數據的處理遍佈視圖層,並且實現⽅式⼜很多樣,如 dva,hooks 等,這會導致視圖層變得厚重,UI 交互等邏輯代碼耦合這⼤量的業務數據處理的代碼,牽⼀發⽽動全身,我們很難看清業務數據處理的整個過程,不僅不易迭代,⽽且這樣的代碼迭代起來很容易出問題。

CR 成本⾼、不易測試

業務邏輯的複雜性,拆分的不合理,代碼不規範等⼀系列問題,導致我們 CR 效率和質量都會打折扣,單測也變得⽆從下⼿,很難起到質量把控的作⽤。

⽬錄結構劃分不合理不利於復⽤

⻚⾯維度即產品⻚⾯或者 UI 設計⻚⾯,對於業務系統的開發,通常我們習慣以⻚⾯維度組織代碼,將⻚⾯⾥的組件進⾏拆分,這樣開發起來很直接,但是會引發如下問題:

1、Component1 ⼀開始被劃分到 Page1,但是後來發現 Page2 也需要,Component1 繼續劃分在 Page1 就不合理了,可能需要重新進⾏⽬錄劃分,或者組件的提取。

2、PageX 是⼀個新的⻚⾯,新的⻚⾯也會⽤到 Component1,但是開發者並未參與過 Page1 和 Page2 的開發,開發者對 Component1 的復⽤變得不可控。

多版本代碼不易維護

如果倉庫是多版本,⽐如存在主站版本和商業化版本,版本之間的核⼼功能基本是⼀致的,但是⼀定會存在差異,⼀⽅⾯要對相同功能代碼的同步,⼀⽅⾯⼜要保持彼此之間的差異,以上的⼀系列問題,使得這種場景變得很困難。

透過現象看本質,以上問題其實很⼤程度都跟 “邊界” 有關,前端代碼的邊界確實讓⼈難以把控,⽽領域驅動⼗分擅⻓解決 “邊界” 的問題。

結合領域解決問題

以領域維度組織代碼

通過領域劃分得到圖研發平臺的領域模型

我們將項⽬的⽬錄結構域領域模型相對應進⾏劃分

components 爲公共組件,可以被所有聚合引⼊,pages 爲⻚⾯組件,domains 即代表⼦域,如 domains-core 代表核⼼域,核⼼域下 graph-project、graoh-config 等爲界限上下⽂,代表圖項⽬、圖配置等,graph-project 下的 project-info、project-list 等則對應聚合,聚合下的內容:

components

該聚合下的組件,該⽬錄下的組件,除了公共只能引⼊該聚合⽬錄下的內容,也就是說組件在聚合內是⾃閉環的。

entities

entities 下可以定義多個實體,每個實體內都聲明瞭該實體的屬性和⽅法,如 project-info.ts 我們看到 ProjectInfoEntity ⾥定義了該實體的屬性,以及獲取屬性的⽅法和更新屬性的⽅法,爲了保證代碼語義 updateProjectInfoEntity ⽅法是不允許暴露出去的。

entity 所定義的屬性,是在視圖層直接進⾏消費的,不需要做數據轉換的。

constants

該聚合下⽤到的常量,這⾥可以統⼀書寫規範,例如只能⼤寫字⺟加下劃線。

services

該聚合下⽤到的後端接⼝服務,定義爲⼀個類,如:service 類起到如下作⽤:

1、聲明瞭該聚合下⽤到哪些服務,這些接⼝服務的⼊參和出參都是明確的,將來如果涉及到接⼝變更或替換,可以直接在這⾥做變更,儘可能減少視圖層的變更。

2、規範後端接⼝的命名。

translator

上⾯提到,我們要求 entity 的數據是視圖層直接消費的,後端的數據很多情況下是要做轉換的,這就需要 translator,將後端接⼝數據轉換爲視圖層可以直接消費的數據。

transformer

還有⼀類數據是前端提交到後端的,典型的如表單場景,可能也會涉及到數據的轉換,將前端提交的數據轉換成後端接⼝接收的數據。

通過 transformer 和 translater,可以減少在視圖層進⾏的業務數據處理,理想的狀態是視圖層只有展示和交互邏輯的代碼書寫。

index

可以理解爲聚合根,外部只能通過聚合根來訪問該聚合下的內容,也就是說,index 內需要定義 projectinfo 這個聚合根下,哪些內容可以被外部訪問到,這⾥我們就可以指定⼀些規則,來限制聚合可以被訪問的內容,這樣做可以⼀定程度保證代碼的安全性,例如有些組件和⽅法只是聚合爲聚合內部服務的,並不希望被外部訪問到,就可以不對外暴露,避免後續在維護聚合內部業務的時候,引發外部問題。

contextService

理想情況下,每個聚合之間都是完全⾃閉環的,但是對於複雜的前端業務系統⽽⾔,⼀個聚合內的組件很難做到完全獨⽴,**我們不是爲了應⽤領域驅動⽽應⽤,⽽是希望可以通過領域驅動解決我們的問題,要適合⾃⼰的業務和團隊,**對於 components,我們允許引⼊其他聚合的內容,但是必須要在 contextService 內引⼊,也就是說,實體內的其他組件只能通過 contextService 獲取到,這樣可以很清楚地看到當前聚合的依賴,在改動聚合內容的時候,需要充分 check 對其他聚合的影響,我們也可以添加規則,來限制依賴的內容,如只能依賴聚合的 components 等。

當然聚合⽬錄下還可以有其他的⽬錄,如 hooks、utils,可以按需添加。

引⽤領域代碼拼裝⻚⾯

完成領域代碼就可以 “拼裝⻚⾯了”,只需要看當前開發的⻚⾯,⽤到哪些聚合的內容,在⻚⾯引⼊代碼組裝,此時,⻚⾯內只有⻚⾯視圖層的邏輯了,數據的獲取通過聚合的 entities,組件也都來⾃各個聚合。

調整開發思維

前端開發⽐較常規的開發模式,是圍繞⻚⾯展開的,以 UI 的維度將⻚⾯的組件模塊進⾏拆分,⽽現在的開發流程:

團隊劃分領域模型,統⼀領域模型術語

開發前,產品、前端、後端等⻆⾊需要⼀起對當前需求進⾏領域劃分,每個⼈都要參與其中,加深對業務的理解。

領域代碼開發

開發⼈員按照上述開發規範,進⾏領域代碼的開發。

拼裝⻚⾯

開發⼈員根據⻚⾯中⽤到的領域信息,組裝領域代碼,完成⻚⾯開發。

整體開發流程

收益

項⽬⽬錄就是領域模型,就是 PRD

新⼈友好

新⼈只需要瞭解業務的領域模型劃分,以及項⽬⽬錄和領域模型的對應關係,便可以對項⽬代碼有整體瞭解,即使上⼿開發⼀個新的功能,也可以很容易地復⽤以往實現過的核⼼業務數據、組件、⽅法等,且整體⽅案沒有太偏技術的應⽤,新⼈可以很快進⼊開發迭代節奏。

視圖層變得輕薄

這主要得益於 transformer 和 translator。

⻚⾯測試的邊界清晰

⻚⾯內涉及到的實體⼀⽬瞭然,且實體內的⽬錄職責邊界⼗分清晰,對於前端對業務⻚⾯的測試是很友好的。

降低 CR 成本

通過代碼⽬錄的變更,便可以清楚地知道改動點涉及的業務範圍,以及代碼變更是否符合該⽬錄下代碼的規範,業務數據的處理不會分散在各個⻆落了,可以很直觀地看到整體數據的處理過程。

組件的復⽤變得簡單

復⽤業務組件的思路已經不⼀樣了,⼀個全新的⻚⾯開發,第⼀時間考慮的是⻚⾯中涉及到領域模型中的哪些⼦域以及實體,開發的時候會先去對應的實體⽬錄去找是否有已經實現過得組件、⼯具等,這樣即使⼀個對項⽬整體代碼不是很熟悉的開發者,同樣可以很輕鬆地對已有的實現進⾏復⽤,並且隨着開發量的增加,對每個⼦域和實體都會有所瞭解,⽽不是只專注於⾃⼰開發的⼏個⻚⾯

代碼⻛格變得統⼀

實體下每個⽬錄的職責是清晰的,且每個⽬錄下代碼的⻛格是明確的,即使⼀個新⼈維護,照葫蘆畫瓢也不會出現與團隊⻛格相差較⼤的代碼,這樣也帶來了好的維護性,有利於項⽬的⻓期迭代。

開發者對業務的理解更加全⾯深⼊

隨着產研每次對領域的討論實體的劃分,開發者會更有參與感,前端同學也會對後端接⼝設計有更多的瞭解,按照領域驅動思想進⾏開發的過程中,也會更加深⼊地理解各個領域和實體的功能,這種理解的加深會隨着開發時間的推移變得範圍越來越⼴,直⾄對全局業務都有更加深刻的理解,這種收益是⻓期的。

爲多版本的維護打下基礎

多版本產品之間雖有功能上的差異,但是它們的共同點是:領域模型具有⼀致性,即實體可能存在差異,但⼤的領域劃分,尤其是核⼼領域和通⽤領域的劃分,基本是不會發⽣變化的,這也意味着,我們在復⽤代碼的時候,不需要再將組件作爲 Y ⼩單位進⾏拆分共享,⽽是以實體作爲 Y ⼩單位進⾏復⽤,配合 Bit ⼯具,進⾏源碼級復⽤,可以極⼤提⾼開發效率,並且滿⾜不同版本之間的不同需求。

沒有絕對,適合就好

領域驅動是⼀種思想,不僅可以應⽤於軟件開發,也沒有絕對的開發規範,適合⾃⼰的業務和團隊背景就好,我們不是爲了應⽤⽽應⽤,⽽是爲了解決問題。

作爲前端開發者,對於領域驅動的理解和應⽤仍是在實踐和探索中,如有錯誤或表達不當之處,歡迎探討指正。

[1] Eric Evans Domain-Driven Design –Tackling Complexity in the Heart of Software  

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