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