當前位置: 妍妍網 > 碼農

領域驅動設計DDD在B端行銷系統的實踐

2024-07-04碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

  • 1 背景

  • 2 基本概念

  • 3 戰略設計實踐

  • 4 戰術設計實踐

  • 5 程式碼架構實踐

  • 6 總結

  • 7 參考資料



  • 1 背景

    透過行銷活動實作客戶/使用者拉新、留存和促活是業界普遍采用的方法。為實作商戶增長和留存,美團核心本地商業/商業增值技術部也構建了相應的行銷系統來支撐商戶的線上行銷營運。在系統建設過程中,面臨著業務體量大、行業跨度大、場景多樣、客戶結構復雜,需求多變等挑戰。本文試圖還原從0到1構建面向商戶的行銷系統過程中,並透過DDD( 領域驅動設計 )來應對系統設計和建設中遇到的業務復雜度高、需求多變、維護成本大等問題。


    2 基本概念

    軟體系統的復雜性主要體現在三個方面。

  • 隱晦 :一是抽象層面的隱晦,抽象系統時,每個人都有自己特定的視角,你需要站在對方的角度才能明白他為什麽這麽做;其次是實作層面的隱晦,程式碼是一種技術實作,通常與現實世界的業務概念脫節,無形中增加了理解成本。

  • 耦合 :程式碼層面的耦合擴大了修改範圍;模組層面的耦合需要跨模組/服務互動;系統層面的耦合則需要跨團隊協作。從程式碼到模組再到系統,耦合的影響逐漸擴大,成本隨之增加。

  • 變化 :業務需求決定了系統功能,不同的使用者需求不一樣,不同的業務發展階段需求在不斷變化,系統功能要隨著業務需求的變化不斷調整,這時就涉及到系統改動的頻次和範圍。

  • DDD( Domain-Driven Design,領域驅動設計 )是應對軟體設計復雜性的方法之一,它能很好的解決上述三個問題,但其概念體系復雜( 如下圖所示 ),學習曲線陡峭,即便深入研讀DDD的兩本經典著作,計畫落地時依然有點「捉襟見肘」。

    在展開介紹DDD之前,這裏先回顧一下歷史:

  • 早期,電腦創新更多聚焦在語言方面,為軟體工程師提供功能更強大的語言來操作電腦,充分使用電腦的算力。

  • 60年代,物件導向語言誕生,透過封裝、繼承、多型等特性進一步增強了語言的表達能力。

  • 80年代,出現物件導向的分析與設計,解決了如何構建類模型的問題,幫助我們更好地使用物件導向語言來實作系統,但沒有解決如何把物理世界對映到電腦世界的問題。

  • 2000年,出現領域驅動設計方法,透過分析業務,抽取概念,建立對應的領域模型,再采用物件導向的分析與設計方法構建對應的類模型,達成了從物理世界到電腦世界的對映。

  • 什麽是領域?領域由三部份組成:領域裏有使用者,即涉眾域;使用者要實作某種業務價值,解決某些痛點或實作某種訴求,即問題域;面對業務價值,痛點和訴求,有對應的解決方案,這是解決方案域。什麽是領域驅動設計?通俗地講,針對特定業務,使用者在面對業務問題時有對應的解決方案,這些問題與方案構成了領域知識,它包含流程、規則以及處理問題的方法,領域驅動設計就是圍繞這些知識來設計系統。

    以行銷為例,行銷系統所服務的使用者有4類:營運、銷售、電銷人員和商戶。解決3個核心問題:如何發券、發給誰、發什麽( 紅包還是折扣券 )。解決方案:透過行銷活動來承載發券,不同的活動型別對應不同的玩法( 如買贈、折扣、充送等 );透過目標人群來確定發給誰;透過權益來定義發什麽( 如:紅包、代金券、折扣券等 )。

    本文將從戰略設計、戰術設計和程式碼架構分3個部份介紹領域驅動設計的落地:

  • 戰略設計 :確定用例,統一語言和劃分邊界。

  • 戰術設計 :概念模型轉化成類( 程式碼 )模型。

  • 程式碼架構 :將系統設計對映為系統實作。


  • 3 戰略設計實踐

    戰略設計之前,先要確定用例,也就是業務是怎麽玩的,有幾種常見的方法:

    1. 用例圖 :最簡單直觀的表達了使用者與系統的互動。

    2. 使用者故事 :敏捷開發模式下用的較多,從Who、What和Why三個維度描述了業務需求。

    3. 互動原型 :使用者操作的頁面及其操作流程,其缺點是過於關註使用者體驗,而忽略了業務底層邏輯。

    4. 事件風暴 :關註業務的底層邏輯,但使用門檻較高,適用於大型而復雜的業務分析。

    下圖是行銷系統的用例圖( 起初並沒有這麽完整,這是多次叠代後的結果 ):

    確定業務玩法後,接下來是統一語言。從用例裏抽取概念,並對概念進行甄別( 去偽存真,抽象合並 )找到真正描述業務的概念。比如,有多種方式來描述活動規則:充值送規則、返還規則和檔位等,技術可能會泛泛地稱其為規則,業務人員則用檔位來描述(比如充值送活動,充1000送100紅包,充2000送300紅包,充3000送500紅包,那1000、2000、3000就是業務所認為的檔位)。抽取概念時,盡量采納業務側的叫法,這樣統一語言比較容易推行。

    接著是明確概念的含義,概念由術語、Term( 術語的英文版 )和含義三部份構成。含義明確的術語就是統一語言,這些術語將用在日常需求溝通、產品文件,技術設計以及程式碼實作中。

    明確概念後,接著理清概念之間的關系( 1對1,多對1,多對多 ),確定概念所代表的的業務實體的核心內容和行為,從而得到概念模型。後續在業務需求討論、產品和技術方案設計時,基於這個概念模型,使用統一語言進行描述,大家能很容易對齊;同時精心抽出的概念和建立的概念模型更接近業務本質,為後續的戰術設計打下了基礎。

    基於統一語言和概念模型,業務 - 產品 - 技術三個角色比較容易就需求達成共識,保障溝通的一致性。

    缺少這些就很容易出問題,如:剛開始做行銷系統時,在如何描述「商戶」上,沒有統一語言,資金域有三個概念來描述商戶( 資金帳戶、帳號ID、資金帳號 ),商家域有四個概念描述商戶( 商家帳號、商家ID、登入號、登入ID ),到了行銷域,不同的人采用不同的概念來描述商戶,造成了溝通的混亂。給商戶發紅包時,「資金帳戶、帳號ID、資金帳號、商家帳號、商家ID、登入號、登入ID」這些概念都可以描述商戶,但業務人員弄不清這些概念之間的區別,導致ID誤用,紅包發錯。事後對這些概念進行了梳理和統一,行銷域只關註資金帳戶和商家帳號,系統功能上明確使用資金帳戶或商家帳號來發送紅包,這樣就不易出錯了。

    概念模型是一張大網,描述了概念間的關系以及關鍵內容,但還不能直接對映為程式碼模型,要對映為程式碼模型,還需拆解,化繁為簡。

    本源論認為世界的本質是簡單的,復雜問題由多個簡單問題構成;康威原理認為系統架構受制於組織溝通架構,系統落地時,首先要確定系統邊界,再依據系統邊界組織分工。這兩個原理表明:我們可以將復雜問題拆解為多個簡單問題,並針對團隊資源組織分工協作。

    這裏給出一種拆解方法( 該方法 來自美團技術內部份享 ):按縱和橫兩個維度來拆,縱是從業務價值和目標維度劃分,橫是從功能的通用性維度劃分。這裏嘗試從業務角度來拆,沒有系統支持時,業務要線上下運轉,通常根據要達成的業務目標,將業務流程或業務組分拆解為多個節點,並定義每個節點的職責以及對應的規範和標準,安排對應的組織或人員執行。簡單地說,就是從業務問題和解決方案出發,拆解到對應的人。因此基於業務的拆分通常能實作系統使用者、業務問題和解決方案之間的一致性。業務系統是把業務的玩法從線下搬到線上,在進行系統拆分時,也可以使用這個思路。從三個層面來進行:

    1. 基於涉眾域拆解 :也就是按使用者相關性進行拆解,不同的使用者使用不同的系統功能,如:CRM由市場人員、銷售人員、客服人員三類角色協同完成客戶觸達,簽約合作,售後服務三大職能,針對這三個角色建設相應的系統能力。這種拆解方式比較簡單,但也存在較大的局限性,可能導致功能的重復建設。

    2. 基於問題域拆解 :不同角色/使用者要解決的問題是相同/相似的,可基於問題域進行拆解,如行銷系統的使用者包括銷售、商戶、銷運等角色,但它核心是要解決如何發券( 活動 ),發給誰( 人群 ),發什麽( 權益 )的問題。基於問題域的拆解相較於基於涉眾域的拆解更加抽象,但也可能復用性不夠。

    3. 基於解決方案域拆解 :不同的問題,可能有相同的解決方案,如HR域有請假審批、財務上有報銷流程、CRM領域存在客戶資質審批,三個領域各自需要解決審批流程的問題,可以構建通用的審批流引擎來統一解決,這是基於解決方案域進行拆解。基於解決方案域的拆解最抽象,也最貼合業務本質,但也容易陷入過度設計的陷阱。

    行銷系統基於問題域拆解為五個子體( 活動域,權益域,人群域,推播域,數據域 ),每個子體解決特定的問題,各子領域相對內聚和簡單:

    業務系統要運轉起來,需要子體之間相互配合,這就要定義上下文對映,實作不同子體間的協作。如活動域關註的兩個目標人群:一是資金帳戶( 表示已簽約的商戶 );另一個是商家帳號( 表示未簽約商戶 )。資金帳戶是財務域定義的,而商家帳號是帳號域定義的,兩個概念都不是行銷域原生概念。此時,行銷域需透過某種方式依賴外部概念,將外部概念對映到行銷域,透過防腐層來對接外部服務來實作這種對映。領域驅動設計裏定義九種上下遊對映關系,這裏不贅述:

    下圖是行銷系統的整體上下文關系:

    從用例分析,統一語言到子體拆分,初步完成戰略設計,但這並非終局,戰略設計是一個持續叠代的過程,叠代的來源主要有3個:

    1. 用例精化 :在探討需求的過程中,用例不斷豐富。

    2. 需求變更 :業務不斷發展帶來需求變化,進而影響用例及相關概念的內涵,概念模型亦隨之調整和叠代。

    3. 方案選型 :當產品,業務或技術發生較大變化時,可能需要采用另一種方式實作它,這時所采用的概念會有所不同。比如早期構建行銷活動域時,透過參與規則來定義誰可以參加活動,將商戶與參與規則進行匹配,符合就能參與。這種方式帶來的問題是無法提供一個完整的活動人群列表,除非將所有商戶匹配一遍。隨著業務方越來越重視活動參與商戶的分層,觸達和轉化,引入目標人群的概念,透過目標人群來保存所有可參加活動的商戶。從參與規則到目標人群,概念發生了變化,底層模型也完全不一樣( 參與規則是一套規則體系,而目標人群由篩選服務提供 ),實作了戰略設計上的叠代。

    有了戰略設計,構建了統一語言和概念模型後,如何驗證概念模型呢?通常用兩個方法:

    1. 場景走查 :把模型代入到所有的場景確認一遍,確定所抽象出來的概念模型和統一語言能正確描述它。

    2. 業務預判 :未來業務的變化會在哪裏,當變化發生時,概念模型的內涵和外延是否方便擴充套件並支持到變化。


    4 戰術設計實踐

    戰略設計得到了概念模型,戰術設計則是將概念模型對映為程式碼模型,有很多編程範式,比如事務指令碼、表模式、物件導向,函式式等,最好的方式是物件導向的實作。

    從概念模型到物件模型:

  • 首先,概念是分層的,如行銷活動是一個泛化概念,其下還有充值送活動、消費返活動,買贈活動等具體活動。構建物件模型時,透過衍生/繼承來實作概念分層。

  • 其次,概念關系對映成物件關系,比如行銷活動包含了檔位和庫存,那在構建行銷活動物件時,可透過組合實作這種包含關系( 檔位物件和庫存物件成為行銷活動物件的內容 )。

  • 最後,概念的內容行為,可以直接變成物件的內容和行為;概念的狀態機以及生命周期也會變成物件的狀態機。

  • 兩類物件:實體和值物件,這兩者的區別是是否有統一標識和自己的狀態。

    有了物件模型,還需透過聚合根完成封裝,如何確定聚合根的粒度?行銷活動包含活動、庫存、檔位、檔位項、目標人群五個物件,如果采用小聚合根模式,一個物件對應一個聚合根,這樣每個聚合根都很簡單。但從業務角度看,庫存或檔位會影響活動的狀態,如:修改了庫存或檔位,活動需要重新審批和上下線,這種業務上的耦合需要在技術上進行處理。此時,就得在小聚合根上構建領域服務來封裝這些邏輯。

    另外一種模式是大聚合根。圍繞活動,把活動相關的概念( 活動、庫存、檔位、檔位項、目標人群 )都封裝起來,但聚合根比較復雜,影響活動載入( 一些活動的目標人群上百萬,懶載入可解決問題,但增加了復雜度 )。

    聚合根的設計要遵循一定的原則:

    1. 滿足業務一致性、數據完整性、狀態一致性。比如庫存檔位和活動狀態要一致,在數據上也要完整,不存在沒有檔位的活動,也不存在沒有庫存的活動。

    2. 技術限制。有些實體會帶來技術挑戰,如數據量太大,可抽出來單獨考慮。

    3. 業務邏輯不滅,在業務封裝與適度的職責邊界之間尋找平衡。不管是大聚合根還是小聚合根,業務邏輯永遠都是存在的,就是看把它放在哪裏。

    如下圖是行銷系統的聚合根:

    聚合根已經非常接近程式碼實作,落地程式碼時,大家還會糾結用貧血模型還是充血模型。Spring MVC通常執行在單例模式下,引入充血模型會增加理解成本和技術復雜度。另外,不適合放在聚合根裏的領域邏輯,可以放在領域服務裏,如:同時存在多個充值送活動時,使用者只能參加優先級最高的一個,在充值送活動聚合根裏會標識活動的優先級,但挑選優先級最高的活動並非聚合根的職責,但確實是領域邏輯的一部份,此時可透過領域服務實作。

    從概念模型,類模型到程式碼實作,整個過程都要使用統一語言。在落地程式碼時,程式碼要體現出業務含義,比如下圖的例子,要避免左邊updateStatus()這樣的方法,它沒有體現業務含義( 必須閱讀程式碼實作,才知道這個方法做了什麽 );圖中右邊的submitCampaign(),approveCampaign(),cancelCampaign()則有明確的業務含義。


    5 程式碼架構實踐

    完成戰術設計後,如何組織程式碼架構?無論是六邊形架構,整潔架構還是洋蔥架構本質上都是圍繞著領域模型展開,套用層、基礎設施層和外部介面都依賴領域模型:

    下圖是我們團隊的工程實踐,與前面三個圖本質上是一樣的。領域層和套用層次放在中間( 兩者都屬於領域邏輯 ),基礎設施和使用者介面依賴中間層:


    6 總結

  • 我們做的大部份系統都不是全新系統,如CRM、HR或SCM等,已經有很多業界實踐,可充分借鑒這些實踐,沒必要自己創造新概念。

  • 要重視統一語言。沒有統一語言就不會有概念模型,沒有概念模型就不可能有靠譜的程式碼模型,拿到需求後就開始設計程式碼模型是不靠譜的。

  • 領域驅動設計是團隊工作。現實中沒有一個是嚴格意義上的領域專家,所有參與到這項工作的人都可以是領域專家,整個工作可以由技術團隊主導,但一定要落地到產品和業務。

  • 擁抱變化,持續叠代。模型是相對穩定的,但並非一成不變,業務理解的深度,抽象的角度與方式,業務的變化都會影響到領域模型,領域模型的建立是持續叠代的過程。

  • 這裏分享幾個常見的誤區:

  • 深陷領域驅動設計的概念體系。在程式碼裏生搬硬套領域驅動設計裏的概念,比如聚合根、值物件、實體等,掰扯概念之間的細微差異,設計復雜的領域事件等。這反而增加理解成本,讓系統變得復雜。領域驅動的精髓在於從業務出發,抽象出業務領域知識,構建概念模型,一步一步將這些概念模型對映成系統。至於如何采用聚合根、領域服務、實體、值物件、領域事件等,可以靈活取舍。

  • 試圖透過精心設計來獲得領域模型。領域模型不是設計出來的,而是透過戰略設計的幾個步驟,從業務中抽象出來的,最重要是理解業務,對業務進行抽象。

  • 使用了DDD就一定會產生好的領域模型的想法也不可取,我們知道飛機怎麽造,但我們不一定能夠造出好飛機,但如果我們知道這個方法,可以少走彎路。

  • 在聊需求的那一刻,設計就開始了,統一語言就是設計的一部份。

    解決方案域在模型維度分為四層:

    1. 功能模型 :產品表達給我們業務的玩法,我們把它變成了用例,從用例裏抽取出功能模型。

    2. 概念模型 :對功能模型進一步抽象,統一語言,形成概念模型。

    3. 程式碼模型 :將概念模型對映為程式碼模型。

    4. 數據模型 :業務數據需要儲存,需要設計對應的表結構。

    這裏有兩個陷阱:

    1. 看到功能模型後,就開始設計數據模型,考慮數據該怎麽建立、怎麽更新、什麽時候該刪除,淪落為CRUD boy。

    2. 看到功能模型後,就開始考慮運算元據的流程是什麽,陷入到事務指令碼陷阱。( 對於一些簡單的功能,不排斥使用事務指令碼,但對於復雜功能,事務指令碼的維護成本非常大

    另外,領域至少可以分為兩大類:一是學科型,比如財務、會計、圖形學、動力學,這類系統的設計須先深入理解學科知識;二是實踐型,如CRM、訂單交易等,是業務經驗的總結,這類系統的設計不妨參考前人的實踐。當然,如果自己的業務具有獨特性,那就只能靠自己摸索了。

    本文整理自美團技術沙龍第73期【基於領域驅動設計(DDD)的架構演進和實踐】,系統復雜性根源於隱晦(難理解),耦合(難改動)和變化(難擴充套件),DDD正是應對系統復雜性的重要方法。 本文針對B端行銷系統設計中的復雜性,從戰略設計,戰術設計到程式碼架構,詳細介紹了DDD在各個階段的實踐,期望為大家提供一些可供參考和借鑒的思路。

    7 參考資料

    [1] 【DDD 實戰課】 歐創新

    [2] 【領域驅動設計】 Eric Evans

    [3] 【企業套用架構模式】 Martin Fowler

    [4]【實作領域驅動設計】Vaughn Vernon

    [5] 【 The Clean Architecture 】Robert C. Martin

    [6] 【 The Onion Architecture 】 Jeffrey Palermo

    [7] https://carlalexander.ca/what-is-software-complexity/

    [8] https://martinfowler.com/bliki/BoundedContext.html

    如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」 第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:HZ

    來源:美團技術團隊

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]