有位朋友最近在為企業做領域驅動設計(Domain Driven Design)內訓時,遇到一位資深學員向他抱怨該技術 “每次一聽就會,一用就不會”!回想到自己也曾在不同場合下聽到人們對領域驅動設計的各種爭辯:掌握它的人覺得這沒什么復雜的,不過是一種很自然的設計方法選擇;而另外的人卻在抱怨這一技術過于晦澀、在現實中根本無處著手。到底是什么造就了領域驅動設計在人們的心中有如此大的gap?本文嘗試回答一下這個問題!
對某一事物的認知差異,往往來自于人們所處的環境差異和經驗差異。所以,讓我們從領域驅動設計的由來說起,從歷史中追溯人們的環境和經驗差異源自何處。
在瀑布過程大行其道的時候,軟件設計和軟件開發是兩個獨立的階段。軟件設計階段使用某種設計范式對領域(要解決的問題域)進行抽象,給出設計模型。隨后的軟件開發階段則用對應的編程語言和技術框架實現該設計模型。
在上述過程中,人們普遍認為分析設計階段是核心,分析設計得到的模型往往決定了軟件能否正確地解決領域問題,因此這一階段需要業務專家和資深的軟件工程師的協作。分析設計階段的軟件工程師往往被委以“架構師”之類高大上的名稱。
分析設計階段采用什么樣的軟件設計范式建模領域,反映了人們透過軟件看待現實世界的思維模式。
面向過程范式認為一切皆過程
,在這里現實世界的問題被分解為一個個的過程,最終通過串聯它們來解決問題。然而隨著計算機軟件開始大規模地解決復雜的商業問題,這種設計范式被證明缺乏足夠的模塊化和抽象手段。過程和被操作數據的分離導致軟件容易走向違背高內聚、低耦合
的方向,帶來維護成本劇增。雖然如此,對于一些簡單的符合事務腳本模型
的程序用這種范式來描述仍是最自然的,這也就是為什么即使像Ruby這種純面向對象語言仍舊允許在頂層寫零散的過程式代碼。
面向對象范式通過對象
來建模世界。對象有自己的屬性和接口,很容易和現實世界中的事物相映射。對象通過封裝緊密依賴的屬性和行為,只允許通過公開接口進行交互的特性,為軟件設計提供了一種邏輯層面的模塊化手段。對象通過組合可以表示更復雜的概念,對象通過接口的泛化可以表示更抽象的概念。人們用面向對象技術大規模地解決復雜商業問題,積累了大量的建模經驗,并最終發展出了“統一建模語言(UML)”。
由于UML誕生于瀑布開發模式仍占主流的時代,所以在標準的UML過程中,軟件設計被明確分為面向對象分析(OOA),面向對象設計(OOD)和面向對象編碼(OOP)階段。實際操作中OOD的工作往往被OOA和OOP各自承擔了一部分。OOA針對要解決的問題在領域中尋找并抽象合適的概念,定義它們之間的關系。OOP則負責在編碼階段補充被OOA忽略的和領域無關但是和軟件開發效率息息相關的因素(軟硬件平臺、數據存儲約束、并發、編程框架...),進一步按照軟件工程的要求(各種設計原則和模式)重塑了OOA給出的業務模型。
由于在這一過程中OOA和OOP是兩個分裂的階段,所以必然出現兩個階段的相互鉗制。人們曾經有一度追求直接根據架構師給出的UML設計圖自動生成代碼。最后實踐證明,這一做法只有在安全性蓋過成本約束并且需求相對穩定的領域可以工作。而在日益復雜的商業軟件開發中,成本因素更多被消耗在軟件代碼的維護和演進上,這時能夠指導人們實際編碼工作的是軟件設計原則(例如SOLID)和設計模式(例如GOF),這就是面向對象的學院派和工程派之爭。在工程派眼中面向對象方法里類
比對象
更重要,類
是代碼層面提供模塊化
以及接口抽象
的重要手段,其次才是運行態的對象
所表述的領域概念。這也就是為何遵循了良好工程化原則的面向對象代碼中容易存在為了消除重復而產生的大量碎片化的類,以及為了代碼靈活性而創造的和領域概念相去甚遠的抽象接口。
最終上述的軟件開發過程中一般同時存在兩個模型,一個是隱藏在各種設計文檔和UML圖中的面向領域的設計模型,另一個是隱藏于軟件源碼中的面向實現的設計模型。這兩個模型的割裂極大地阻礙了軟件產品的交付效率。為了解決這一問題,敏捷軟件開發方法倡議從改善軟件開發端到端的溝通方式做起。所以在敏捷開發中,團隊更傾向于被定義為一個個獨立的特性團隊。在特性團隊內部,業務專家、架構師、開發人員和測試人員要能夠無障礙的溝通,并集體為特性的端到端交付負責。
在敏捷軟件開發中,被普遍接受的一種觀點是“軟件源代碼是唯一真正的設計產物”。一些偏重技術實踐的敏捷軟件開發方法(如XP)極大的推動了這方面的實踐:人們優先將精力投入到代碼上,持續保持代碼的清晰和靈活性,通過重構代碼來演進設計模型,并通過自動化測試來確保設計演進的正確性和安全性。在這種開發模式下,那些能夠做到業務、設計、編碼、測試技能互相融合的技術人員會更受到青睞。傳統的架構師開始遭受到質疑,很多組織要求內部業務專家和架構師都要具備編碼的能力。
敏捷的這一做法在互聯網應用井噴的時代中取得了成功,但在一些傳統的領域知識復雜的組織內卻一直飽受質疑,敏捷也曾因此被訛傳是一種完全不要設計和文檔的開發方式!這種情況一直延續到了Eric Evans提出《領域驅動設計-軟件核心復雜性應對之道》(后面簡稱DDD)。
在DDD中,Eric Evans認為軟件開發中最核心的資產應該是領域模型
,軟件開發中的所有參與者都應該圍繞著一個統一一致的領域模型而工作。為了得到領域模型,DDD擁抱了敏捷軟件開發方法。首先DDD要求軟件開發中的各種角色要緊密地工作在一起,無障礙地溝通。其次DDD認為領域模型需要借助演進式設計得到,在這過程中需要重構和自動化測試等技術實踐的協助。但同時DDD也演進了一些敏捷中的觀念,領域模型不是設計文檔,但也不是代碼!如果我們承認軟件開發的本質是一個學習過程,那么所有參與者對軟件如何解決領域問題在腦海中所構建的一致畫面才是關鍵!在DDD中,這體現在通用語言(Ubiquitous Language)
中,業務專家和開發人員通過領域模型走查每個用例的時候所采用的術語以及腦海中對應的認識應該是高度一致的。
從上可見DDD首先應該是一種軟件開發過程,它擁抱了敏捷開發方法,采用演進式設計和各種先進的軟件技術實踐,追求一個統一一致的領域模型(而不是曾經分裂的分析模型和實現模型),目標是做到模型既設計、代碼與設計保持一致!
同時,DDD發展了敏捷,它顯示地把領域和設計放到了軟件開發的核心,業務人員和軟件開發人員被得到同樣的重視,他們合作來構建領域模型。這讓敏捷開發方法真正的在領域知識復雜的行業內得以有效應用。
為了做到在代碼中凸顯領域模型,DDD提出了分層架構。首先代碼中需要把用戶界面、調度框架、基礎設施等與領域無關的實現元素分離到不同的層次中去,讓領域層中的代碼可以和領域模型保持高度一致!然后DDD從戰略和戰術兩個層面給出了可以得到領域模型的一些最佳實踐。
由于通用語言
的重要性,所以需要讓每個概念在各自上下文中是清晰無歧義的,于是DDD在戰略上提出了劃分Bounded Context。從實踐的角度看,Kent Beck很早在《實現模式》中說過如果一個類中的屬性被它不同接口訪問的內聚度不同或者訪問頻率不同,就應該將這些屬性和接口拆分出來形成一個新類。這些新類往往和原有的類表示一個概念的不同方面,例如OrderedBook
、DeliveredBook
。當如此需要依賴前綴區分的概念逐漸變多則代表著一種味道,提醒著我們需要考慮將它們拆分到不同的BC中去。最終這些概念在每個BC下的含義又變得唯一和一致,也就不再需要前綴的修飾。不同BC間通過Context Mapping
集成在一起工作,每個BC都會有一個領域模型。拆分BC的同時也分離了關注點,降低了每個BC下領域模型的復雜度。
對于如何獲得每個BC的領域模型,DDD并沒有給出具體的方法。DDD在戰術層面只是對領域模型中應該有的元素進行了分類:Entity
、Value Object
、Aggregate
、Service
、Factory
、Repository
,并給出了每類元素在領域模型中的職責和特征。上述分類基本上是站在面向對象范式的基礎上給出的,這些詞匯對沒有面向對象基礎的人會顯得晦澀!
DDD雖然采用面向對象設計范式,但并不意味所有場景下只有面向對象最適合來構建領域模型。由于領域模型是從領域問題出發人為構建的一種面向領域的指示性語義,選擇某種基本設計范式只是選擇了一種構建基礎而已。理論上選擇使用面向過程
、面向對象
還是函數式
做為構建基礎都是圖靈完備的,但在工程上需要考量應用哪種范式和要構建的領域語義之間的gap最小、成本最低。另外現代編程語言基本都支持多范式編程,提供程序員在局部使用多種范式的自由。雖然如此,主流的編程語言仍舊將面向對象作為主范式,這不僅是因為面向對象的適應場景更廣,人們在面向對象建模上積累了大量的經驗,更是因為面向對象提供了低成本的模塊化手段和抽象能力,可以讓程序員從一個不錯的起點開始工作。
但遺憾的是DDD并沒有教人們如何在某個具體領域找到合適的領域模型。即使對熟悉面向對象的程序員來說,要在領域中找出合理的領域模型也不是容易的,這中間的gap往往需要實踐者在DDD之外去補齊!
很明顯的是領域模型中的概念應該來自領域,模型要盡可能反映領域本質!從這個角度來說領域模型更應該靠近分析模型!所以傳統的領域分析技術(例如各種OOA技術和企業架構模式)對領域建模都是有益的。區別是我們要確保這個模型是被代碼清晰表達的,和業務專家及開發人員腦海中的理解是一致的,并且是需要被一直演進著的!
由于領域模型的最終目的是解決領域問題,所以任何脫離use case的領域建模都是無源之水!傳統的分析技術在這方面已經積累了很多經驗,例如借助四色建模可以讓我們針對要解決的問題識別出完備合理的概念和關系。另一方面模型還需要兼顧軟件復雜度和性能等其他制約因素,例如單純地問模型中Customer
和Order
是何種引用關系是沒有意義的,一方面我們根據要解決的問題來決定誰引用誰(單向引用)或者雙向引用,另一方面我們會根據實際的軟件復雜度或者性能來決定是引用地址還是引用ID,所以即使在相同的領域下解決的問題不同,領域模型都是不同的。而業務專家和開發人員需要做的則是緊密配合,不斷從use case或者test case出發借助各種建模技術建立模型,根據各種約束來調整模型,同時重構代碼保持領域層代碼和領域模型的一致。
正如前面所說學習各種企業架構模式以及分析模式是做好領域建模的必要條件,但遺憾的是不同領域在這方面的學習曲線陡峭程度差異很大。我們能輕易從各種書或者教程中學習到的案例,往往是研究得比較成熟的領域,例如電子商務、人力資源管理等。類似的領域極端情況下去觀察沒有軟件之前人是怎么做的和記錄的,就能把涉及到的領域概念關系挖掘的差不多了。由于這類系統中交互對象之間邊界天然且清晰,玩各種建模技術(例如Event Storming
)都會相對容易很多。而另一類系統,例如“電信系統”、“能源系統”等,復雜度全在系統內部。這類系統大多比較龐大,且有復雜的領域知識,涉及到復雜的事務、協議和算法。出于性能原因,往往分布式部署在各種差異化的軟硬件平臺上。這類系統中的各種設計模型和概念都是經過多年沉淀后得到的結果,且相對封閉,沒有經驗繼承的分析建模很難設計得合理。雖然按照DDD提倡的做法確實可以讓這件事做得更科學和高效,但在本質上并沒有讓這件事變得簡單!
自Eric Evans提出DDD之后,這些年該技術又得到很多新的發展。人們用DCI(Data Context Interactive)架構對DDD進行補充,試圖解決領域對象中行為邊界和數據邊界不一致的問題(即service
帶來的貧血與充血之爭)。人們為DDD補充了Domain Event
的建模元素,發展出了CQRS架構。人們提出了六邊形架構更進一步解耦了傳統的DDD分層架構。Anyway,這些最終都沒抵上微服務架構的出現對DDD帶來的推動作用。微服務架構從一出來就沒有很好的理論支撐如何合理的劃分服務邊界,人們常常為服務要劃分多大而爭吵不休。而DDD被發現恰好可以彌補微服務的營養不良:服務最大不要大過一個BC,否則服務內會存在有歧義的領域概念;服務最小不要小過一個聚合,否則會引入分布式事務的復雜度;服務間最好通過Domain Event
來進行交互,這樣可以讓服務保持松耦合。微服務和DDD的結合,讓微服務架構看起來似乎更加穩健了!但其實微服務需要的不只是DDD,微服務雖然讓某些事變得簡單了,但是構建好微服務對軟件設計的優秀技術實踐和基礎設施的要求都變高了。
對DDD的追溯就到這里,現在我們分析一下人們對DDD的認知gap會發生在哪些方面!
如果你還工作在瀑布軟件開發模式下,很遺憾,即使你在做領域建模,你的團隊也很難工作在一個統一一致的模型下。要記住DDD首先要求我們改變原有的軟件開發過程。
如果你的團隊沒有領域專家,很遺憾,你在挖掘領域本質的過程中會走很多彎路,你需要找到領域專家的協助或者把自己變成領域專家。
如果你不熟悉面向對象軟件設計,很遺憾,市面上大多DDD的教程與你無關,人們在面向對象建模上積累的大量經驗也很難直接為你所用。
如果你不熟悉面向對象分析技術,很遺憾,你的建模過程可能不是高效的,你需要通過學習和實踐來彌補這中間的能力gap。
如果你的團隊編碼能力比較差,或者你的團隊不具備重構的能力和相應的基礎設施,很遺憾,你的模型很難落地!DDD最重要的是要保持代碼和領域模型的一致,并且是同時演進著的!
如果你工作在相對封閉且有復雜領域知識的領域,那么你需要找到或者培養精通DDD的工程師,并愿意長期耕耘在該領域!
如果你還沒有讀過《領域驅動設計》這本書,那么對不起,此文到此為止,你應該先去好好讀一下這本書!