跨越DDD從理論到工程落地的鴻溝

      網友投稿 640 2025-04-04

      DDD作為一種優秀的設計思想,的確為復雜業務治理帶來了曙光。然而又因為DDD本身難以掌握,很容易造成DDD從理論到工程落地之間出現巨大的鴻溝。就像電影里面的橋段,只談DDD理論姿勢很優美,一旦工程落地就跪了......所以DDD的項目,工程落地很重要,否則很容易變成“懂得了很多道理,卻依然過不好這一生”。

      這篇文章,我會從DDD的核心概念講起,但重點會講如何把理論落地成代碼,期望給那些正在探索DDD的同學一些指引和啟發。

      1. DDD的核心概念

      DDD難以掌握的原因之一是因為其涉及很多概念,比如像Ubiquitous Language、Bounded Context、Context Mapping、Domain、Domain Service、Repository、Aggregation root、Entity、Value Object等等。這里簡要介紹一下DDD的核心概念,了解這些概念可以更好的幫助我們落地DDD。

      1.1 統一語言

      Eric Evans在解釋DDD本質的時候,重點提到“Exploration and reshaping the ubiquitous languages",也就是探索并重塑統一語言。統一語言是DDD中非常重要的概念,因為語言是我們認知的基礎,語言都不統一,就像一個人說阿拉伯語,一個人說漢語,那怎么能交流的起來呢?

      對于統一語言,我建議每個項目都要有一份自己的核心領域詞匯表。這個詞匯表至少要包含中文、英文、縮寫、解釋四列,中文是我們日常交流和文檔中經常要體現的,所以需要統一,這樣我們在交流的時候才能高效,沒有歧義;英文和英文縮寫主要體現在我們的設計和代碼上,也就是說我們的“統一語言”不僅僅是停留在交流和文檔中,還要和代碼保持一致,這樣才能做到知行合一,提升代碼的可讀性和系統的可理解性。

      比如一個CRM系統,我們可以從業務需求中挖掘出一些重要的領域概念,把這些概念整理成詞匯表會如下所示。

      中文

      英文

      縮寫

      解釋

      客戶

      Customer

      營銷對象

      機會

      Opportunity

      可能成交的機會

      線索

      Leads

      潛在的客戶

      聯系人

      Contact Person

      能聯系到客戶的關鍵人

      公海

      Public Sea

      所有客戶經理共享的客戶資源

      私海

      Private Sea

      跨越DDD從理論到工程落地的鴻溝

      客戶經理獨占的客戶資源

      客戶經理

      Customer Manager

      CM

      銷售人員

      有了這個核心領域詞匯表,以后團隊的交流、文檔、設計和代碼都應該以這個詞匯表為準,這里需要注意的是,詞匯表中英文對中文的翻譯不一定非常“準確”,不過沒關系,語言就是一個符號,共識即正確,只要大家容易理解達成一致即可。就像上面詞匯表中私海這個概念的翻譯是Private Sea,這是一個典型的Chinglish,正統的翻譯是Territory,但是大家都認為Private Sea更容易理解,只要達成共識,用這個名稱也挺好。

      1.2 限界上下文

      大型軟件系統的單體結構很難應付日益膨脹的復雜度。和解決所有復雜問題一樣,除了分而治之,各個擊破,別無他法。事實證明,對于微服務的邊界劃分使用DDD的戰略設計是一個有效手段。AWS全球云架構戰略副總裁Adrian Cockcroft就曾說過:

      Microservices is a loosely-coupled, service-oriented architecture with bounded context.(微服務就是在限界上下文下的松耦合的SOA。)

      如上圖所示,通過服務劃分,我們可以聚焦在一個大系統下的一個Bounded Context里面,從而把原來大而復雜的問題空間,劃分成多個小的、可以理解的小問題域。

      如何把一個大的模型分解成小的部分沒有什么具體的公式。如果非要給服務劃分一個評判標準的話,那么這個評判標準應該是高內聚低耦合。

      高內聚體現在要盡量把那些相關聯的以及能形成一個自然概念的因素放在一個模型里。如果我們發現兩個服務之間的交互過于緊密,比如有非常頻繁的API調用或者數據同步,那么這兩個域可能都不夠內聚,放在一起可能會更好。

      低耦合是和內聚性相對應的,如果領域不夠內聚,他們之間的耦合自然就高了。如果兩個領域,界限不清晰,領域高度重合,就會造成了嚴重的耦合問題。

      系統耦合是一方面,人員耦合是另外一個考量因素。總體上來說,我不提倡微服務(Bounded Context)劃分的太細,因為服務太多,會加重運維成本。但服務也不能太粗,試想一下,如果一個服務需要8個人去維護,在上面做開發。那么解決代碼沖突,環境沖突,發布等待都將是一個問題。通常一個服務,只需要一到兩個人維護是相對比較合理的粒度。

      除了服務的粒度之外,關于領域類型我們也有必要去了解一下,領域的類型劃分旨在幫助我們理解領域的主次之分,從而知道什么是我當前Bounded Context的核心。在DDD中,領域被分成三種類型。

      核心域(Core Domain),顧名思義這是我領域的核心。有一點需要注意,Core的概念是隨著你視角的變化而變化的。對于本領域來說是Core,對于另外一個領域而言可能只是Support。

      支撐子域(Supporting Subdomian),雖然不是當前問題的核心領域,但也是必不可少的。比如授信子服務離不開客戶信息,所以客戶服務是授信服務的支撐子域。

      通用子域(Generic Subdomain),如果一個子域被用于整個業務系統,那么這個子域便是通用子域。通常像賬號、角色、權限都是常見的通用子域,每個系統都需要。

      1.3 上下文映射

      通過上面的戰略設計,一個大型業務系統,會被劃分成多個各自獨立的Bounded Context,也就是多個微服務,這些服務需要互相協作,來完成完整的業務功能。

      每一個限界上下文都有一套自己的“語言”,如果在該領域要使用其它領域的信息,我們就需要一個“翻譯器”,把外域信息翻譯成本領域的概念。這個在不同領域之間進行概念轉化、信息傳遞的動作就叫上下文映射(Context Mapping)。上下文映射主要有兩種解決方案:共享內核和防腐層。

      所謂的共享內核(Shared Kernel),是指把兩個子域中共同的實體概念抽取出來,形成一個組件(java中的jar包),然后通過內聯(inline)的方式,分別被不同的子域使用。

      共享內核的最大好處是代碼復用和能力共享,然而壞處也很明顯,即高耦合:任何對于“共享內核”的改動都要小心翼翼的協調兩個領域的技術團隊,且會影響兩個領域。說實話,這個副作用有點傷不起,所以在實踐中,更推薦的上下文映射方法是防腐層。

      所謂的防腐層(Anti-Corruption,AC),是指在一個領域中,如果需要使用其它領域的信息,可以通過AC進行防腐和轉義。實際上,在微服務的環境下,服務調用是一個普遍的訴求,因為沒有一個服務是孤立的,都需要借助其它服務提供的數據,共同完成業務活動。

      就像中國傍邊需要有個朝鮮,俄羅斯旁邊需要一個中立的烏克蘭一樣,我們不能讓外領域的東西隨便“入侵”滲透到本領域,為了保證本領域的完整性和獨立性,我們需要做一層隔離和防腐,否則唇亡齒寒,國將不國。

      AC的做法有一定的代價,因為你要做一次信息轉換,把外域的信息轉成本域的領域概念。其好處是雙方都擁有了更大的自主權和靈活度。系統架構就是這樣,我們永遠要在重復(Duplication)耦合低和復用(Reuse)耦合高之間取一個折中,進行權衡。

      1.4 領域模型

      領域模型將現實世界抽象為了信息世界,把現實世界中的客觀對象,抽象為某一種信息結構,而這種信息結構并不依賴于具體的計算機系統。它不是對軟件設計的描述,它是和技術無關的(Technology-Free)。

      例如,電商的核心領域模型就是商品、會員、訂單、營銷等實體,和你使用什么技術實現是沒有關系的,你用Java可以實現,用PHP,GO也能實現。但不管是哪種技術實現方式,都不應該影響我們對領域模型的抽象和理解。

      正因為領域模型的技術無關性,并且領域模型是我們的核心,這才有了洋蔥圈架構,即領域模型處在架構的最內核,并且不依賴任何外圍的技術細節。

      這里順便回答一下同學經常問的事務(Transaction)在哪里實現的問題,為了保持領域的技術無關性,事務最好被管理在App的Service中。

      關于如何設計領域模型,簡單來說,就是分析語言。這也是為什么我們一直在強調統一語言的重要性,因為只有真正的理解了業務,把重要的領域概念闡述清楚,才有可能設計出比較好的領域模型。

      具體的建立領域模型的步驟,可以分為以下三步:

      理解問題:我們需要用簡短的語句把問題域描述清楚,用戶故事或者用例,是建模的關鍵前序動作。除了用戶故事外,我們當然也可以使用事件風暴(Event Storming),四色建模法等手段,只是我覺得用戶故事比較簡單易行,所以推薦用這種方式。

      挖掘概念(Digging out concepts):領域概念隱含在語句中,重點關注語句中的名詞(nouns),因為nouns常常以為這重要的領域概念。這一步不容易做到,因為自然語言有很大的隨意性,很多同義詞、多義詞混淆其中。而且,有些關鍵概念也不一定就是名詞,也可能通過動詞(verbs)進行偽裝。

      建立關聯:尋找關系,需要關注動詞(verbs)。因為關聯意味著兩個模型之間存在語義聯系,在用例中的表現通常為兩個名詞被動詞連接起來。

      2. 工程落地

      Talk is cheap,show me the code。一切的一切,最終還是要落到代碼上,而這一步也是造成問題最多的地方。

      DDD本身是一個非常優秀的設計思想,關于這一點應該爭議不大。很多同學的困惑不在于DDD的思想,而在于不知道如何把DDD落到代碼上。

      “我的業務只是CRUD,為什么還要Domain呢?”

      “既然Domain是承載業務邏輯的地方,那我把業務邏輯都放進Domain可以嗎?”

      2.1 都是CRUD為什么要Domain?

      任何的應用都是有一系列功能(functionality)和數據(data)組成。如果只有function沒有data,那么它只是一個函數。相反,如果只有CRUD對數據的操作,那么,它只是一個數據庫(database)。

      可以說,一點業務邏輯沒有的應用基本上是不存在的,Domian層的價值就在于,它為我們提供了一種內聚業務邏輯、顯性化表達業務語義的地方。

      以客戶注冊這個場景為例,如果注冊沒有什么業務邏輯,只是往數據庫中插入一條記錄,那么有沒有Domain都無所謂。然而,真實的業務當然不允許我們這樣做,業務專家們會提出很多業務規則,來防止那些不夠資格的人注冊成功。

      而且業務需求還在不斷變化,有一些業務規則還被用在不同的地方,比如業務那邊發起了一個新的規則:從3月份起,注冊資本在1000萬以上的公司是大客戶,會有特殊的優惠政策。顯然,大客戶是一個比較重要的領域知識,而且可以預判這個概念不僅在注冊的時候,在其它地方也可能被用到。

      現在你有兩種選擇,一種是如下所示,直接把這個業務規則追加到原來的業務邏輯上。

      if(registeredCapital >= 1000W){ // do something }

      另一種是,我們把這個重要的業務概念,內聚到領域實體身上,顯性化的表達出來。

      // 領域能力沉淀 Customer{ private long registeredCapital; // 判斷是否大客戶 public boolean isBigClient(){ return registeredCapital >= 1000W; } } // 使用 if(customer.isBigClient()){ // do something }

      很明顯,第二種領域封裝的方式會更好,它至少有兩個好處。其一,業務語義得到顯性化的表達,大客戶(bigClient)的概念就直接呈現在代碼中。其二,能更好的應對變化,比如有一天我們對大客戶的定義發生變化,除了注冊資本之外,還要看員工數,那么只需要修改isBigClient( ),而第一種做法要散彈式修改所有需要關注大客戶概念的地方。

      類似于“大客戶”這樣的領域知識(Domain Knowledge),就非常適合Domain層來承載。因為Domain里面有我們最重要的領域概念、領域實體,再加上領域能力(也就是那些業務規則),從而形成所謂的 Knowledge Rich Design (知識豐富的設計)。從這個意義上來說,領域模型只是我們領域知識的一部分,業務活動和規則如同所涉及的實體一樣,都是領域的核心。

      除此之外,在當前服務化、分布式大行其道的今天,我們的數據也不一定就是存在本地的數據庫,很可能這個數據是來自于另一個服務,這種情況下,Domain層給我們提供了一個在當前限界上下文(Bounded Context)里,對外域進行防腐、隔離的機會。

      2.2 Domain層是必選的嗎?

      “按你這么說,我一定需要這個Domain層咯?可是Domain層的實體模型和數據模型的轉換,成本有點高啊!”

      有此顧慮的同學不在少數,的確,Domain層作為原來三層架構之外新引入的層次,會帶來一些額外的成本。關于這個問題,與其把Domain層當成負擔,不如把它當成是一個機會或者投資,既然是投資,我們就要看ROI(投入產出比)。

      捫心自問,我當前對Domain的投資——抽象、領域建模、領域能力沉淀等,是否提升了我代碼的可讀性、可理解性,或者從長期來看提升了系統的可維護性,如果ROI成正比,就值得去做。

      有沒有ROI不成正比的時候呢?有的,比如簡單的Query,可能就是讀取數據,沒有什么業務邏輯,那么我們也完全可以繞過Domain層,讓數據模型直接轉換成DTO,減少一層數據轉換,這也是CQRS(Command Query Responsibility Segregation)所提倡的。

      作為一個“沒有銀彈”的信徒,我很認同佛瑞德·布魯克斯的觀點。雖然Domain非常有用,但也不是“銀彈”。所以如下圖所示,在設計DDD的應用架構時,比如我開源的COLA架構。我更愿意把Domain層設計成開放的,這種開放性不僅體現在CQRS的時候,App可以繞過Domain層直達Infrastructure;也體現在當你的團隊實在hold不住DDD的時候,可以選擇退化到老的三層架構。

      雖然可以退化,但不應該成為你輕易放棄Domain層的理由。據我觀察,很多同學不喜歡DDD,其根本原因還不在于對象之間的轉換成本(實際上,這個轉換成本也沒那么大),而在于他不清楚Domain的職責,不知道哪些東西應該放到Domain里面。一種典型的錯誤做法是把所有的業務邏輯都放到了Domain層,包括我們上面說的CRUD統統放到了領域層,這樣的DDD當然沒人喜歡。

      2.3 把業務邏輯都寫進Domain?

      每當我看到同學把所有業務邏輯都寫進Domain層,我就會問他,“你這樣把App層的所有業務邏輯都搬到Domain層,能得到什么益處呢?和把這些代碼直接放在App層的區別在哪里呢?況且,放在App層,因為少了一個層次,代碼會更加簡單,為什么要勞心勞力的再加一個Domain層呢?”

      “那要怎么辦呢?”同學一邊點頭一邊疑惑地問。

      我給的方案是“先把App做厚,再把App做薄”。什么意思?就是我們先可以把業務邏輯都寫到App里面,在寫的過程中,我們會發現有一些業務邏輯,不僅僅是過程式的代碼,它也是領域知識(Domain knowledge),應該被更加清晰、更加內聚的表達出來,那么我們就可以把這段代碼沉淀為領域能力。

      舉一個例子,還是以用戶注冊為例,一開始,正如我們一直這樣做的,直接在App層寫出如下的過程代碼:

      public class CustomerServiceImpl { private CustomerGateway customerGateway; private HealthCodeService healthCodeService; public void register(CustomerDTO customerDTO){ Customer customer = Customer.fromDTO(customerDTO); // 1. 校驗年齡 if(customer.getAge() < 18){ BizException.of("對不起,你未滿18歲"); } // 2. 校驗國籍 if(!customer.getCountry().equals("china")){ BizException.of("對不起,你不是中國人"); } // 3. 查看健康碼,需要調用另外一個服務。 HealthCodeRequest request = new HealthCodeRequest(); request.idCardNo = customer.getIdCardNo(); HealthCodeResponse response = healthCodeService.check(request); if(!response.isSuccess()){ BizException.of("無法驗證健康碼,請稍后再試"); } if(!response.getHealthCode().equals("green")){ BizException.of("對不起,你不是綠碼"); } // 4. 注冊用戶 customerGateway.save(customer); } }

      寫好后,我們再回過頭來審視一下,看看哪些東西可以沉淀為領域能力,然后優化我們的代碼。

      我們先看年齡和國籍校驗,年齡和國籍都是customer的屬性,那么誰對它們最熟悉呢?當然是customer自身了,對于這樣的業務知識,無能是從可理解性的角度,還是從功能內聚和復用性的角度,把它們沉淀到customer身上都會更合適,于是,我們可以在customer實體上沉淀這些業務知識:

      public void isRequiredAge(){ if(age < 18){ BizException.of("對不起,你未滿18歲"); } } public void isValidCountry(){ if(!country.equals("china")){ BizException.of("對不起,你不是中國人"); } }

      健康碼有點特殊,雖然它也是Customer的健康碼,但是它并不存在于本應用中,而是存在于另一個服務中,需要通過遠程調用的方式來獲取。這在我們的分布式系統中,是非常常見的現象,即我們要通過分布式的服務交互來共同完成業務功能。

      如果直接調用外部系統,基于外系統的DTO,當然也能完成代碼功能,但這樣做會有三個問題:

      表達晦澀,我只是要檢查一下健康碼,卻有一堆的代碼。(這只是示意,真實的遠程調用肯定要比這個代碼多)

      復用性差,校驗健康碼不僅僅客戶注冊會用到,可能很多客戶相關的操作都會用到,難道都要這么寫一遍?

      沒有防腐和隔離,HealthCodeResponse不是我這個領域的東西,怎么能讓它如此輕易的侵入到我的業務代碼中呢?

      解決上面的問題,我們就可以充分發揮Domain層防腐的作用,使用上下文映射(Context Mapping),把外領域的信息映射到本領域。即我可以認為HealthCode就是屬于Customer的,至于這個HealthCode是怎么來的,那是Gateway和infrastructure要幫我處理的問題,它可能來自于自身的數據庫,也可能來自于RPC的遠程調用,總之那是infrastructure要處理的“技術細節”問題,對于上層的業務代碼不需要關心。

      按照這樣的思路,我們可以新建一個HealthCodeGateway來解開對健康碼系統的耦合。

      /** * 對外系統的依賴通過gateway進行解耦 */ public interface HealthCodeGateway { public String getHealthCode(String idCardNo); }

      于此同時,把如何獲取HealthCode這樣的技術細節問題丟給infrastructure去處理。

      /** * 在infrastructure中,完成如何獲取healthCode的細節問題 */ public class HealthCodeGatewayImpl implements HealthCodeGateway{ private HealthCodeService healthCodeService; @Override public String getHealthCode(String idCardNo) { HealthCodeRequest request = new HealthCodeRequest(); request.idCardNo = idCardNo; HealthCodeResponse response = healthCodeService.check(request); if(!response.isSuccess()){ BizException.of("無法驗證健康碼,請稍后再試"); } return response.getHealthCode(); } }

      最后,我們把從gateway獲取到的healthCode賦值給customer,對于customer來說,這個healthCode是遠程調用拿到的,還是從數據庫拿到的,它并不需要關心。

      public class Customer { ... // 你雖然是游蕩在外面的游子,但我帶你如同己出 private String healthCode; public void isHealthCodeGreen(){ if(healthCode == null){ healthCode = healthCodeGateway.getHealthCode(idCardNo); } if(!healthCode.equals("green")){ BizException.of("對不起,你不是綠碼"); } } ... }

      經過一系列的“能力下沉”之后,我們原來的客戶注冊邏輯,丑小鴨變白天鵝,成了下面這樣的clean code。

      public class CustomerServiceImpl { private CustomerGateway customerGateway; public void register(CustomerDTO customerDTO){ Customer customer = Customer.fromDTO(customerDTO); // 1. 校驗年齡 customer.isRequiredAge(); // 2. 校驗國籍 customer.isValidCountry(); // 3. 查看健康碼,需要調用另外一個服務。 customer.isHealthCodeGreen(); // 4. 注冊用戶 customerGateway.save(customer); } }

      除了代碼變得clean之外,代碼的可理解性也提高了,因為原來那些過程式平鋪的代碼,被合理的內聚到領域實體身上之后,其代碼的表達能力獲得了提升。閱讀這種代碼的體驗應該和閱讀語句通順的短文無異,差不多是這樣的感覺:“if customer is required age, customer is in valid country, customer's health code is green, then save this customer to be registered”

      所以,我們在落地DDD的時候,千萬要小心不要落入概念的教條,而是要始終盯著我們的北極星目標——即系統的可理解性、可維護性,以及代碼的可讀性。如果你的DDD不僅沒有到達這些目標,反而讓系統變得更復雜,更難理解,給開發者帶來額外的負擔。那么就應該停下來,反思一下,我是不是走偏了。

      3. 上下結合跨越鴻溝

      本文通過代碼案例的方式,嘗試解答一下大家在實施DDD過程中,常見的困惑問題。希望給到大家一個相對正確的落地DDD工程的開發范式,總結一下,這個范式大概可以分為以下七個步驟:

      梳理業務:梳理業務流程,挖掘領域概念,形成統一語言。

      戰略設計:劃分領域邊界,建立限界上下文。

      戰術設計:尋找實體,建立關系,形成領域模型。

      API設計:根據用戶故事,輸出服務功能API。

      做厚App:根據API功能要求,在App層編寫業務過程代碼。

      做薄App:以領域模型為基礎,優化過程代碼,沉淀領域能力和領域知識,讓業務語義顯性化,做到Knowledge Rich Design (知識豐富的設計)。

      技術細節:完善技術細節代碼,比如API的暴露方式(RPC 或者 Restful),數據的存儲方式(關系數據庫 或者 NoSQL),ORM框架的選用(MyBatis 或者 JPA)等等。

      最后,我想再次強調,好的Domain層,不僅僅需要設計,更是在開發過程中,循環迭代沉淀出來的。用一句話來形容這個過程就是:自上而下的結構化分解,自下而上的抽象建模,循環迭代沉淀領域能力。

      關于如何提升抽象思維能力和結構化思維能力,請關注即將上市的新書《程序員的底層思維》。

      微服務 機器翻譯

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:NAND的生產工藝提升或將推動智能手機很快進入TB時代
      下一篇:如何在表格中查找人員
      相關文章
      国产亚洲精品一品区99热| 亚洲a∨国产av综合av下载| 无码专区一va亚洲v专区在线| 亚洲欧美日韩自偷自拍| 亚洲综合色丁香婷婷六月图片| 亚洲一区在线免费观看| 亚洲91精品麻豆国产系列在线| 亚洲国产日韩在线成人蜜芽| 亚洲蜜芽在线精品一区| 亚洲色大成网站www永久| 亚洲美女大bbbbbbbbb| 亚洲成a人片在线观看中文app| 亚洲综合小说久久另类区| 精品日韩亚洲AV无码| 亚洲天堂电影在线观看| 亚洲国产成人va在线观看网址| 亚洲免费福利视频| 亚洲高清中文字幕免费| 亚洲精品无码成人| 亚洲第一se情网站| 亚洲国产成人精品女人久久久 | www亚洲一级视频com| 亚洲av无码成人精品区| 亚洲 无码 在线 专区| 亚洲美女在线国产| 亚洲日韩aⅴ在线视频| 亚洲AV无码乱码在线观看裸奔| 亚洲国产精品国自产拍电影| 久久久久久亚洲精品成人| 亚洲一级视频在线观看| 亚洲啪AV永久无码精品放毛片| 亚洲av无一区二区三区| 亚洲精品动漫人成3d在线| 曰韩亚洲av人人夜夜澡人人爽| 亚洲精品无码高潮喷水在线| 亚洲天天做日日做天天欢毛片 | 亚洲第一精品电影网| 亚洲国产日韩精品| 亚洲?v女人的天堂在线观看| 国产亚洲精品免费视频播放| 亚洲AV无码精品色午夜在线观看|