领域驱动设计理论基础
運用領(lǐng)域模型
消化知識
做好一塊業(yè)務(wù)的第一步是如何消化一個知識,消化一個知識其實就是建立有效模型,將知識透明化,變成可視化、可交流的文檔或者圖形。有效建模的要素包括以下幾點:
- 模型和現(xiàn)實的綁定。最初的原型可能比較簡陋,但它會在模型與現(xiàn)實之間建立早期鏈接,所有后續(xù)的迭代是維護完善該鏈接。
- 建立了一種基于模型的語言。隨著項目的進展,參與者都能夠使用模型中的術(shù)語,并將它們組織成符合模型結(jié)構(gòu)的語句,而且無需翻譯即可理解互相要表達的意思。
- 開發(fā)一個蘊含豐富知識的模型。 對象具有行為和強制性規(guī)則。模型不僅僅是一種數(shù)據(jù)模式,它還是解決復(fù)雜問題不可或缺的部分。我們需要挖掘提取業(yè)務(wù)知識背后隱藏的概念。將復(fù)雜的知識進行分解,分解成單一變化的對象。
- 提煉模型。 在模型日趨完整的過程中,重要的概念不斷被添加到模型中,但同樣重要的是,不再使用的或不重要的概念則從模型中被移除。當一個不需要的概念與一個重要的概念關(guān)聯(lián)時,則把重要的概念提取到一個新模型中,其他那些不要的概念就可以丟棄。
- 頭腦風暴和實驗。語言和草圖,再加上頭腦風暴活動,將我們的討論變成“模型實驗室”,在這些討論中可以演示、嘗試和判斷上百種變化。當團隊走查場景時,口頭表達本身就可以作為所提議的模型的可行性測試。
在傳統(tǒng)的瀑布方法中,業(yè)務(wù)專家與分析員進行討論,分析員消化理解這些知識后,對其進行抽象并將結(jié)果結(jié)果傳遞給程序員,再由程序員編寫軟件代碼。由于這種方法沒有反饋,因此總是失敗。分析員全權(quán)負責創(chuàng)建模型,但他們創(chuàng)建的模型只是基于業(yè)務(wù)專家的意見。他們既沒有向程序員學(xué)習(xí)的機會,也得不到早期軟件版本的經(jīng)驗。知識只是朝一個方向在流動,但是不會積累。
好的程序員會自然而然地抽象并開發(fā)出一個可以完成更多工作的模型。但是這個模型只是局部的,只能滿足特定時刻特定部分的需求。只有團隊所有成員一起消化理解模型,通過頻繁的交互才能形成全局有效一致的認知。領(lǐng)域模型的不斷精化迫使開發(fā)人員學(xué)習(xí)重要的業(yè)務(wù)原理,而不是機械地進行功能開發(fā)。領(lǐng)域?qū)<冶黄忍釤捵约阂阎闹匾R過程往往也是完善其自身理解的過程,而且他們會漸漸理解軟件項目所必須的概念嚴謹性。模型聚焦于需求分析,同時與編程和設(shè)計緊密交互。它通過良性循環(huán)加深團隊對領(lǐng)域的理解,使他們更透徹地理解模型,并對其進一步精化。
統(tǒng)一團隊的領(lǐng)域交流語言
要想創(chuàng)建一種靈活的、蘊含豐富知識的設(shè)計,需要一種通用的、共享的團隊語言,以及對語言的不斷的試驗。
如果語言支離破碎,項目必將遭遇嚴重問題。日常討論所使用的術(shù)語和代碼(軟件項目的最重要產(chǎn)品)中使用的術(shù)語不一致。甚至同一個人在講話和寫東西時使用的語言也不一致,這導(dǎo)致的后果是,對領(lǐng)域的深刻表述常常稍縱即逝,根本無法記錄到代碼或文檔中。
翻譯使得溝通不暢,并削弱了知識消化,帶來誤解的風險。口頭溝通會產(chǎn)生信息不可逆的丟失。
團隊統(tǒng)一的領(lǐng)域語言的詞匯應(yīng)該包括但不限于類及主要操作的名稱。語言中的術(shù)語,有些可以是模型討論中已經(jīng)明確的規(guī)則,有些可以是來自模型上的高級組織規(guī)則,團隊常常用于領(lǐng)域模型的設(shè)計模式也使領(lǐng)域語言更加豐富。
通過大量使用基于模型的語言,并且不達流暢絕不罷休,我們可以逐步得到一個完整的、易于理解的模型,它由簡單元素組成,并通過組合這些簡單元素表達復(fù)雜概念。
將模型作為語言的支柱。確保團隊在內(nèi)部所有交流中以及代碼中堅持使用這種語言。在畫圖、寫東西,特別是講話時也要使用這種語言。
通過嘗試不同的表示方法(它們反映了備選模型)來消除難點。然后重構(gòu)代碼,重新命名類、方法和模塊,以便與新模型保持一致。解決交談中的屬于混淆問題。領(lǐng)域?qū)<覒?yīng)該抵制不合適或無法充分表達領(lǐng)域理解的術(shù)語或結(jié)構(gòu),開發(fā)人員應(yīng)該密切關(guān)注那些將會妨礙設(shè)計的有歧義和不一致的地方。
討論系統(tǒng)時要結(jié)合模型。使用模型元素及其交互來大聲描述場景,并且按照模型允許的方式將各種概念結(jié)合到一起。找到更簡單的表達方式講出你要講的話,然后將這些新的想法應(yīng)用到圖和代碼中。
綁定模型和實現(xiàn)
如果整個程序設(shè)計或者其核心部分沒有與領(lǐng)域模型相對應(yīng),那么這個模型就是沒有價值的,軟件的正確性也值得懷疑。同時,模型和設(shè)計功能之間過于復(fù)雜的對應(yīng)關(guān)系也是難于理解的,在實際項目中,當設(shè)計改變時也無法維護這種關(guān)系。若分析與設(shè)計之間產(chǎn)生嚴重分歧,那么在分析和設(shè)計活動中所獲得的知識就無法彼此共享。
模型驅(qū)動設(shè)計(Model-Driven Design)不再將分析模型和程序設(shè)計分離開,而是尋求一種能夠滿足這兩方面需求的單一模型。不考慮純粹的技術(shù)問題,程序設(shè)計中的每個對象都反映了模型中所描述的響應(yīng)概念。這就要求我們以更高的標準來選擇模型,因為它必須同時滿足兩種完全不同的目標。
軟件系統(tǒng)各個部分的設(shè)計應(yīng)該忠實地反映領(lǐng)域模型,以便體現(xiàn)出這兩者之間的明確對應(yīng)關(guān)系。我們應(yīng)該反復(fù)檢查并修改模型,以便軟件可以更加自然地實現(xiàn)模型,即使想讓模型反映出更深層次的領(lǐng)域概念也該如此。我們需要的模型不但應(yīng)該滿足這兩種需求,還應(yīng)該能夠支持健壯的通用語言。
從模型中獲取用于程序設(shè)計和基本職責分配的術(shù)語。讓程序代碼成為模型的表達。代碼的改變可能會是模型的改變,而其影響勢必要波及接下來響應(yīng)的需求項目。
面向?qū)ο缶幊讨怨δ軓姶?#xff0c;是因為它基于建模范式,并且為模型構(gòu)造提供了實現(xiàn)方式。從程序員的角度來看,對象真實存在于內(nèi)存中,它們與其他對象互相聯(lián)系,它們被組織成類,并且通過消息傳遞來完成相應(yīng)的行為。
模型驅(qū)動設(shè)計的核心點是編程思想的轉(zhuǎn)變,從過程設(shè)計轉(zhuǎn)變成面向?qū)ο笤O(shè)計。如果編寫代碼的人員認為自己沒必要對模型負責,或者不知道如何讓模型為應(yīng)用程序服務(wù),那么這個模型就和程序沒有任何關(guān)聯(lián)。如果開發(fā)人員沒有意識到改變代碼就意味著改變模型,那么他們對程序的重構(gòu)不但不會增強模型的作用,反而還會削弱它的效果。同樣,如果建模人員不參與到程序?qū)崿F(xiàn)的過程中,那么對程序?qū)崿F(xiàn)的約束就沒有切身的感受,即使有,也會很快忘記。如果分工阻斷了設(shè)計人員與開發(fā)人員之間的協(xié)作,使他們無法轉(zhuǎn)達實現(xiàn)模型驅(qū)動設(shè)計(Model-Driven Design)的種種細節(jié),那么經(jīng)驗豐富的設(shè)計人員則不能將自己的知識和技術(shù)傳遞給開發(fā)人員。
任何參與建模的技術(shù)人員,不管在項目中的主要職責是什么,都必須花時間了解代碼。任何負責修改代碼的人員則必須學(xué)會用代碼來表達模型。每一個開發(fā)人員都必須不同程度地參與模型討論。參與不同工作的人都必須有意識地通過團隊統(tǒng)一語言與接觸代碼的人及時交換關(guān)于模型的想法。
分離領(lǐng)域
分層的價值在于每一層都只代表程序中的某一特定方面。這種限制使每個方面的設(shè)計都更具內(nèi)聚性,更容易解釋。當然,要分離出內(nèi)聚設(shè)計中最重要的方面,選擇恰當?shù)姆謱臃绞绞侵陵P(guān)重要的。盡管Layered Architecture的種類繁多,但是大多數(shù)成功的架構(gòu)使用的都是下面4個概念層的某種變體。
| 用戶界面層(或表示層) | 負責向用戶顯示信息和解釋用戶指令。這里指的用戶可以是另一個計算機系統(tǒng),不一定是使用用戶界面的人 |
| 應(yīng)用層 | 定義軟件要完成的任務(wù),并且指揮表達領(lǐng)域概念的對象來解決問題。這一層所負責的工作對業(yè)務(wù)來說意義重大,也是與其他系統(tǒng)的應(yīng)用層進行交互的必要渠道 |
| 領(lǐng)域?qū)?#xff08;或模型層) | 負責表達業(yè)務(wù)概念,業(yè)務(wù)狀態(tài)信息以及業(yè)務(wù)規(guī)則。盡管保存業(yè)務(wù)狀態(tài)的技術(shù)細節(jié)是由基礎(chǔ)設(shè)施層實現(xiàn)的,但是反映業(yè)務(wù)情況的狀態(tài)是由領(lǐng)域?qū)涌刂撇⑶沂褂玫念I(lǐng)域?qū)邮菢I(yè)務(wù)軟件的核心。 |
| 基礎(chǔ)設(shè)施層 | 為上面各層提供通用的技術(shù)能力;為應(yīng)用層傳遞消息,為領(lǐng)域?qū)犹峁┏志没瘷C制,為用戶界面層繪制屏幕組件等等。基礎(chǔ)設(shè)施層還能夠通過架構(gòu)框架來支持4個層次間的交互模式。 |
如果與領(lǐng)域有關(guān)的代碼分散在大量的其他層(用戶界面層、應(yīng)用層、基礎(chǔ)設(shè)施層等)之中,那么查看與分析領(lǐng)域代碼就會變得異常困難。對用戶界面的簡單修改實際上很可能會改變業(yè)務(wù)邏輯。而要想調(diào)整業(yè)務(wù)規(guī)則也可能需要對用戶界面代碼、數(shù)據(jù)庫操作代碼或者其他的程序元素進行仔細的篩查。這樣就不太可能實現(xiàn)一致的、模型驅(qū)動的對象了,同時也會給自動化測試帶來困難。
給復(fù)雜的應(yīng)用程序劃分層次。在每一層內(nèi)分別進行設(shè)計,使其具有內(nèi)聚性并且只依賴于它的下層。采用標準的架構(gòu)模式,只與上層進行松散的耦合。將所有與領(lǐng)域模型相關(guān)的代碼放在一個層中,并把它與用戶界面層、應(yīng)用層以及基礎(chǔ)設(shè)施層的代碼分開。領(lǐng)域?qū)ο髴?yīng)該將重點放在如何表達領(lǐng)域模型上,而不需要考慮自己的顯示和存儲問題,也無需管理應(yīng)用任務(wù)等內(nèi)容。這使得模型的含義足夠豐富,結(jié)構(gòu)足夠清晰,可以捕捉到基本的業(yè)務(wù)知識,并有效的使用這些知識。
各層之間是松散連接的,層與層的依賴關(guān)系只能是單向的。上層可以直接使用或操作下層元素,方法是通過調(diào)用下層元素的公共接口,保持對下層元素的引用,以及采用常規(guī)的交互手段。而如果下層元素需要與上層元素進行通信,則需要采用另一種通信機制,使用架構(gòu)模式來連接上下層,如回調(diào)模式或觀察者模式等。
一個復(fù)雜的項目應(yīng)該采用模型驅(qū)動設(shè)計(Model-Driven Design),不應(yīng)該使用一些類似Smart UI的“反模式”,這類模式雖然效率高,可以順利地使用關(guān)系數(shù)據(jù)庫,能夠提供數(shù)據(jù)級的整合,但是違反了層次架構(gòu),在復(fù)雜業(yè)務(wù)場景中沒有對業(yè)務(wù)問題進行抽象,導(dǎo)致很多代碼無法復(fù)用,同時會形成概念模糊,無中心的現(xiàn)象。
模型表示的元素
用于表示模型的3種模型元素模式:Entity、Value Object和Service。一個對象是用來表示某種具有連續(xù)性和標識事物,還是用于描述某種狀態(tài)的屬性,這是Entity和Value Objecct之間的根本區(qū)別。明確的選擇這兩種模式中的一個來定義對象,有利于減少歧義,并幫助我們做出特定的選擇,這樣才能得到健壯的設(shè)計。
領(lǐng)域中還有一些方面適合用動作或操作來表示,這比用對象表示更加清楚。這些方面最好用Service來表示,而不應(yīng)把操作的責任強加到Entity或Value Object上,盡管這樣做只是稍微違背了面向?qū)ο蟮慕鹘y(tǒng)。Service是接受客戶端請求來完成某事。在軟件技術(shù)層中有很多Service(這些不全是領(lǐng)域?qū)铀f的Service)。領(lǐng)域?qū)拥囊部梢允褂肧ervice,當對軟件要做的某項無狀態(tài)的活動進行建模時,就可以將該活動作為一項Service。
關(guān)聯(lián)
現(xiàn)實生活中有大量“多對多”關(guān)聯(lián),其中有很多關(guān)聯(lián)天生就是雙向的,我們在模型開發(fā)的早期進行頭腦風暴活動并探索領(lǐng)域時,也會得到很多這樣的關(guān)聯(lián)。但這些普遍的關(guān)聯(lián)會使實現(xiàn)和維護變得很復(fù)雜。
盡可能地對關(guān)系進行約束是非常重要的。我們可以規(guī)定一個方向去梳理關(guān)聯(lián);通過添加一個限定符,以便有效的將“多對多”關(guān)聯(lián)限制為“一對多”關(guān)聯(lián);從業(yè)務(wù)角度去消除不必要的關(guān)聯(lián)。
從仔細地簡化和約束模型的關(guān)聯(lián)到真正的模型驅(qū)動設(shè)計還有一段很漫長的探索過程。仔細區(qū)分對象可以使得模型更加清晰,并得到更實用的實現(xiàn)。
Entity
一些對象主要不是由它們的屬性定義的,它們實際上表示了一條“標識線”,這些主要由標識定義的對象被稱作Entity。Entity(實體)有特殊的建模和設(shè)計思路。它們具有生命周期,這期間它們的形式和內(nèi)容可能發(fā)生根本改變,但必須保持一種內(nèi)在的連續(xù)性。
標識有時只有在系統(tǒng)上下文中才重要。比如體育場預(yù)定程序可能將座位和觀眾當作Entity來處理,但如果活動采用入場券方式,那么就不需要對座位加以區(qū)分,這種情況下,只有座位總數(shù)才是重要的。
當一個對象由其標識(而不是屬性)區(qū)分時,那么在模型中應(yīng)該主要通過標識來確定該對象的定義。使類定義變得簡單,并集中關(guān)注生命周期的連續(xù)性和標識。定義一種區(qū)分每個對象的方式,這種方式應(yīng)該與形式和歷史無關(guān)。要格外注意那些需要通過屬性來匹配對象的需求。在定義標識操作時,要確保這種操作作為每個對象生成唯一的結(jié)果,這可以通過附加一個保證唯一性的符號來實現(xiàn)。這種定義標識的方法可能來自外部,也可能是由系統(tǒng)創(chuàng)建的任意標識符,但它在模型中必須是唯一的標識。模型必須定義出“符合什么條件才算是相同的事物”(可以理解為數(shù)據(jù)庫表的業(yè)務(wù)唯一健)。比如掛號場景下,用戶訂單就是一個Entity,而這個訂單所對應(yīng)的科室疾病就不是一個Entity。
每個Entity都必須有一種建立標識的操作方式,以便與其他對象區(qū)分開。不管系統(tǒng)是如何定義的,都必須確保標識屬性在系統(tǒng)中是唯一的,即使是分布式系統(tǒng)中,或者對象已經(jīng)被歸檔,也必須確保標識的唯一性。
Value Object
很多對象沒有概念上的標識,但它們描述了一個事物的某種特征。用于描述領(lǐng)域的某個方面而本身沒有概念標識的對象稱為Value Object。Value Object被實例化之后用來表示一些設(shè)計元素,對于這些設(shè)計元素,我們只關(guān)心它們是什么,而不關(guān)心它們是誰。Value Object是對Entity的補充。
跟蹤Entity的標識是非常重要的,但為其他對象也加上標識會影響系統(tǒng)性能并增加分析工作,而且會使模型變得混亂,因為所有對象看起來都是相同的。
軟件設(shè)計要時刻與復(fù)雜性做斗爭。我們必須區(qū)別對待問題,僅在真正需要的地方進行特殊處理。
當我們只關(guān)心一個模型元素的屬性時,應(yīng)把它歸類為Value Object。我們應(yīng)該使用這個模型元素能夠表示出其屬性的意義,并為它提供相關(guān)功能。Value Object應(yīng)該是不可變的(不是絕對的不可變)。不要為它分配任何標識,而且不要把它設(shè)計成像Entity那么復(fù)雜。
Entity之間的雙向關(guān)聯(lián)很難維護,兩個Value Object之間的雙向關(guān)聯(lián)則完全沒有任何意義。
Service
有時候?qū)ο蟛皇且粋€事物,在設(shè)計中會包含一些特殊的操作,這些操作從概念上講不屬于任何對象。與其把它們強制地歸于哪一個類,不如順其自然地在模型中引入一種新的元素,這就是Service(服務(wù))。一些領(lǐng)域改變不適合被建模為對象。如果勉強把這些重要的領(lǐng)域功能歸為Entity或Value Object的職責,那么不是歪曲基于建模的對象定義,就是人為增加一些無意義的對象。比如賬戶之間的轉(zhuǎn)賬,用轉(zhuǎn)賬服務(wù)來描述比將轉(zhuǎn)賬行為分解到賬戶中更加自然。
使用Service時應(yīng)謹慎,它們不應(yīng)該替代Entity和Value Object的所有行為。但是,一個操作實際上是一個重要的領(lǐng)域概念時,Service很自然就會稱為Model-Driven Design中的一部分。將模型中的獨立操作聲明為一個Service,而不是聲明為一個不代表任何事情的虛擬對象,可以避免對任何人產(chǎn)生的誤導(dǎo)。好的Service應(yīng)該有以下3個特征:
- 與領(lǐng)域概念相關(guān)的不是Entity或Value Object 的一個自然組成部分。
- 接口是根據(jù)領(lǐng)域模型的其他元素定義的。
- 操作是無狀態(tài),這里說得無狀態(tài)是指任何客戶都可以使用某個Service的任何實例,而不必關(guān)心該實例的歷史狀態(tài)。
當領(lǐng)域中的某個重要的過程或轉(zhuǎn)換操作不是Entity或Value Object的自然職責時,應(yīng)該在模型中添加一個作為獨立接口的操作,并將其聲明為Service。同時應(yīng)該使Service成為無狀態(tài)的。這里說的Service是那些在領(lǐng)域中具有重要意義的Service,但Service這個概念并不只是在領(lǐng)域?qū)又惺褂谩N覀冃枰⒁鈪^(qū)分屬于領(lǐng)域?qū)拥腟ervice和那些屬于其他層的Service(比如發(fā)送短信的service等),并劃分責任,以便將它們明確區(qū)分開。
Service可以控制領(lǐng)域?qū)又械慕涌诘牧6?#xff0c;并且避免客戶端與Entity和Value Object的耦合。在大型系統(tǒng)中,中等粒度的、無狀態(tài)的Service更容易被復(fù)用,因為它們在簡單的接口背后封裝了重要的功能。此外,細粒度的對象可能導(dǎo)致分布式系統(tǒng)的消息傳遞的效率低下。
Module(也成為Package)
Module為人們提供了兩種觀察模型的方式,一是可以在Module中查看細節(jié),而不會被整個模型淹沒;二是觀察Module之間的關(guān)系,而不考慮其內(nèi)部細節(jié)。領(lǐng)域?qū)又械腗odule應(yīng)該成為模型中有意義的部分,Module從更大的角度描述了領(lǐng)域。
眾所周知,Module之間應(yīng)該是低耦合的,而在Module的內(nèi)部則是高內(nèi)聚的。Module不僅僅是代碼的劃分,也是概念的劃分。一個人一次考慮的事情是有限的(因此菜肴低耦合)。不連貫的思想和“一鍋粥”似的思想同樣難于理解(因此才要高內(nèi)聚)。
在一個好的模型中,元素之間是要協(xié)同工作的,而仔細選擇的Module可以將那些具有緊密概念關(guān)系的模型元素集中到一起。將這些具有相同職責的對象元素聚合在一起,可以把建模和設(shè)計集中到單一的Module中,這極大地降級建模和設(shè)計的復(fù)雜性,使人們可以從容應(yīng)對這些工作。
選擇能夠描述系統(tǒng)的Module,并使之包含一個內(nèi)聚的概念集合。這通常會實現(xiàn)Module之間的低耦合,但如果效果不理想,則應(yīng)尋找一種更改模型的方式來消除概念之間的耦合,或者找到一個新的Module概念(可能之前被忽略了),基于這個概念組織的Module可以以一種有意義的方式將元素集中到一起。對模型進行精化,直到可以根據(jù)高層領(lǐng)域概念對模型進行劃分,同時對應(yīng)的代碼也不會產(chǎn)生耦合。Module及其名稱應(yīng)該能反映出領(lǐng)域模型深層知識。
領(lǐng)域模型中的每個概念都應(yīng)該在實現(xiàn)元素中反映出來。Entity、Value Object、它們之間的關(guān)聯(lián)、領(lǐng)域Service以及用于組織元素的Module都是實現(xiàn)與模型直接對應(yīng)的地方。實現(xiàn)中的對象、指針和檢索機制必須直接、清楚地映射到模型元素。如果沒有做到這一點,就要重寫代碼,或者回頭修改模型,或者同時修改代碼和模型。
建模范式
目前主流的范式是面向?qū)ο笤O(shè)計,而且現(xiàn)在的大部分復(fù)雜項目都開始使用對象。對象建模在簡單性和復(fù)雜性之間實現(xiàn)了一個很好的平衡。
雖然對象建模的概念很簡單,但它的豐富功能足以捕獲重要的領(lǐng)域知識。而且它從一開始就獲得了開發(fā)工具的支持,使得模型可以在軟件中表達出來。大多數(shù)開發(fā)人員、項目經(jīng)理和從事項目工作的其他專家都已經(jīng)很好的接受了面向?qū)ο笤O(shè)計。
領(lǐng)域建模不一定是對象模型。當領(lǐng)域中只有個別元素適合用其他范式時,開發(fā)人員可以接受一些蹩腳的對象,以使整個模型保持一致。可以將業(yè)務(wù)規(guī)則引擎或者工作流引擎等類似的非對象組件集成到對象系統(tǒng)中,以使開發(fā)人員能夠用最適當?shù)娘L格對待特殊概念進行建模。
雖然Model-Driven Design 不一定是面向?qū)ο蟮?#xff0c;但它確實需要一種富有表達力的模型結(jié)構(gòu)實現(xiàn),無論是對象、規(guī)則還是工作流,都是如此。當將非對象元素混合到以面向?qū)ο鬄橹鞯南到y(tǒng)中時,需要遵循以下4條經(jīng)驗規(guī)則:
- 不要和實現(xiàn)范式對抗。我們總是可以用別的方式來考慮領(lǐng)域,從而找到適合于范式的模型概念。
- 把通用語言作為依靠的基礎(chǔ)。即使工具之間沒有嚴格聯(lián)系時,語言使用上的高度一致性也能防止各個設(shè)計部分分裂。
- 不要一味依賴UML。有時固定使用某種工具(如UML繪圖工具)將導(dǎo)致人們通過歪曲模型來得到UML圖。有時使用其他風格的圖形或簡單的語言描述比牽強附會地視圖更好。
- 保持懷疑態(tài)度。工具是否真正有用武之地?不能因為存在一些規(guī)則,就必須使用規(guī)則引擎。規(guī)則也可以表示為對象,雖然可能不是特別優(yōu)雅。
領(lǐng)域?qū)ο蟮纳芷?/h3> 領(lǐng)域?qū)ο蟮纳芷?
管理這些對象時面臨諸多挑戰(zhàn),稍有不慎就會偏離Model-Driven Design的軌道。主要的挑戰(zhàn)有以下兩類:
- 在整個生命周期中維護完整性和事務(wù)。
- 防止模型陷入管理生命周期復(fù)雜性造成的困境當中。
通過3種模式解決這些問題。首先是Aggregate(聚合),它通過定義清晰的所屬關(guān)系和邊界,并避免錯綜復(fù)雜的對象關(guān)系網(wǎng)來實現(xiàn)模型的內(nèi)聚。聚合模式對于維護生命周期各個階段的完整性具有至關(guān)重要的作用。接著是在生命周期的開始階段,使用Factory(工廠)來創(chuàng)建和重建復(fù)雜對象,從而封裝它們的內(nèi)部結(jié)構(gòu)。最后,在生命周期的中間和末尾使用Repository(存儲庫)來提供查找和檢索持久化對象并封裝龐大基礎(chǔ)設(shè)施的手段。盡管Repository和Factory本身并不是來源于領(lǐng)域,但它們在領(lǐng)域設(shè)計中扮演者重要的角色。這些結(jié)構(gòu)提供了易于掌握的模型對象處理方式,使Model-Driven Desogn更完備。
Aggregate
典型對象模型中的關(guān)系網(wǎng)使我們難以斷定一個修改會產(chǎn)生哪些潛在的影響。僅僅因為存在依賴就更新系統(tǒng)中的每個對象,這樣做是不現(xiàn)實的。在多個客戶對相同對象進行并發(fā)訪問的系統(tǒng)中,這個問題更加突出。當很對用戶對系統(tǒng)中的對象進行查詢和更新時,必須防止他們同時修改互相依賴的對象,范圍錯誤將導(dǎo)致嚴重的后果。
在具有復(fù)雜關(guān)系的模型中,要保證對象更改的一致性是很難的。不僅互不關(guān)聯(lián)的對象需要遵守一定固定規(guī)則,而且緊密關(guān)聯(lián)的各組對象也要遵守一些固定規(guī)則。然而,過于謹慎的鎖定機制又會導(dǎo)致多個用戶之間毫無意義地互相干擾,從而使系統(tǒng)不可用。
實際上,要想找到一種兼顧各種問題的解決方案,要求對領(lǐng)域有深刻的理解,要了解特定類實例之間的更改頻率這樣的深層次等因素。我們需要找到一個使對象間沖突較少而固定規(guī)則聯(lián)系更緊密的模型。
盡管從表面上看這個問題是數(shù)據(jù)庫事務(wù)方面的一個技術(shù)難題,但它的根源在模型,歸根結(jié)底是由于模型中缺乏明確定義的邊界。從模型得到的解決方案將使模型更易于理解,并且使設(shè)計更易于溝通。當模型被修改時,它將引導(dǎo)我們對實現(xiàn)作出修改。
我們需要用一個抽象來封裝模型中的引用。Aggregate就是一組相關(guān)對象的集合,我們把它作為數(shù)據(jù)修改的單元。每個Aggregate都有一個根(root)和一個邊界(boundary)。根是Aggregate所包含的一個特定Entity。對Aggregate而言,外部對象只可以引用根,而邊界內(nèi)部的對象之間則可以互相引用。
為了實現(xiàn)這個概念上的Aggregate,需要對所有事務(wù)應(yīng)用一組規(guī)則。
- 根Entity具有全局標識,它最終負責檢查固定規(guī)則。
- 根Entity具有全局標識。邊界內(nèi)的Entity具有本地標識,這些標識只在Aggregate內(nèi)部才是唯一的。
- Aggregate外部的對象不能引用除Entity之外的任何內(nèi)部對象。根Entity可以把內(nèi)部Entity的引用傳給它們,但這些對象只能臨時使用這些引用,而不能保持引用。根可以把一個Value Object的副本傳給另一個對象,而不必關(guān)系它發(fā)生什么變化,因為它只是一個Value,不再與Aggregate有任何關(guān)聯(lián)。
- 作為上一條規(guī)則的推論,只有Aggregate的根才能直接通過數(shù)據(jù)庫查詢獲取。所有其他對象必須通過關(guān)聯(lián)來發(fā)現(xiàn)。
- Aggregate內(nèi)部的對象可以保持對其他Aggregate根的引用。
- 刪除操作必須一次刪除Aggregate邊界之內(nèi)的所有對象。
- 當提交對Aggregate邊界內(nèi)部的任何對象的修改時,整個Aggregate的所有固定規(guī)則都必須被滿足。
我們應(yīng)該將Entity和Value Object分門別類地聚集到Aggregate中,并定義每一個Aggregate的邊界。在每個Aggregate中,選擇一個Entity作為根,并通過根來控制對邊界內(nèi)其他對象的所有訪問。由于根控制訪問,因此不能繞過它來修改內(nèi)部對象。這種設(shè)計有利于確保Aggregate中的對象滿足所有固定規(guī)則,也可以確保在任何狀態(tài)變化時Aggregate作為一個整體滿足固定規(guī)則。
Factory
當創(chuàng)建一個對象或創(chuàng)建整個Aggregate時,如果創(chuàng)建工作很復(fù)雜,或者暴露了過多的內(nèi)部結(jié)構(gòu),則可以使用Factory進行封裝。
對象的功能主要體現(xiàn)在其復(fù)雜的內(nèi)部配置以及關(guān)聯(lián)方面。我們應(yīng)該一直對對象進行提煉,直到所有與其意義或在交互中的角色無關(guān)的內(nèi)容被完全剔除為止。一個對象在它的生命周期里面要承擔大量的職責。如果再讓復(fù)雜對象負責自身的創(chuàng)建,那么職責過載將會導(dǎo)致很多問題。
復(fù)雜的對象創(chuàng)建是領(lǐng)域?qū)拥穆氊?#xff0c;然而這項任務(wù)并不屬于那些用于表示模型的對象。當客戶負責創(chuàng)建復(fù)雜對象時,它會牽涉不必要的復(fù)雜性,并將其職責搞的模糊不清。這違背了領(lǐng)域?qū)ο蠹八鶆?chuàng)建的Aggregate的封裝要求。更嚴重的是,如果客戶是應(yīng)用的一部分,那么職責就會從領(lǐng)域?qū)有孤┑綉?yīng)用層中。應(yīng)用層與實現(xiàn)細節(jié)之間的這種耦合使得領(lǐng)域?qū)映橄蟠蟛糠謨?yōu)勢蕩然無存,而且導(dǎo)致后續(xù)更改的代價變得更加高昂。
對象的創(chuàng)建本身可以是一個主要操作,但被創(chuàng)建的對象并不適合承擔復(fù)雜的裝配操作。將這些職責混在一起可能產(chǎn)生難以理解的拙劣設(shè)計。讓客戶直接創(chuàng)建對象又會使客戶的設(shè)計陷入混亂,并且破壞被裝配對象或Aggregate的封裝,而且導(dǎo)致客戶與被創(chuàng)建對象的實現(xiàn)之間產(chǎn)生過于緊密的耦合。
每種面向?qū)ο蟮恼Z言都提供了一種創(chuàng)建對象的機制(例如C++和java中的構(gòu)造函數(shù),Smalltalk中創(chuàng)建實例的類方法),但我們?nèi)匀恍枰环N更加抽象且不與其他對象發(fā)生耦合的構(gòu)造機制。Factory就是一種負責創(chuàng)建其他對象的構(gòu)造機制,封裝了創(chuàng)建復(fù)雜對象或Aggregate所需的知識。Factory提供了反映客戶目標的接口,以及被創(chuàng)建對象的抽象視圖,從而使客戶無需知道對象的工作機理就可以使用對象的功能。
Factory有很多設(shè)計方式,包括但不限于工廠方法模式、抽象工廠模式和構(gòu)造器模式。任何好的工廠都需要滿足以下兩個基本需求:
- 每個創(chuàng)建方法都是原子的,而且要保證被創(chuàng)建對象或Aggregate的所有固定規(guī)則。Factory生成的對象要處于一致的狀態(tài)。在生成Entity時,意味著創(chuàng)建滿足所有固定規(guī)則的整個Aggregate,但在創(chuàng)建完成后可以向Aggregate添加可選元素。
- Factory應(yīng)該被抽象為所需的類型,而不是所要創(chuàng)建的具體類。
在向Aggregate添加元素時可以通過在Aggregate的根上創(chuàng)建一個Factory Method,從而把Aggregate的內(nèi)部實現(xiàn)細節(jié)隱藏起來,使任何外部客戶看不到這些細節(jié),同時使根負責確保Aggregate在添加元素的完整性。
當創(chuàng)建一個對象時,這個對象與另一個對象的生成密切相關(guān),但它并不擁有所生成的對象。也就是當一個對象的創(chuàng)建主要使用另一個對象的數(shù)據(jù)(或許還有規(guī)則)時,則可以在后者的對象上創(chuàng)建一個Factory Method,這樣就不必將后者的信息提取到其他地方來創(chuàng)建前者。這樣做還有利于表達前者和后者之間的關(guān)系。
Factory與被構(gòu)造對象之間是緊密耦合的,因此Factory應(yīng)該只被關(guān)聯(lián)到與被構(gòu)造對象有著密切聯(lián)系的對象上。當有些細節(jié)需要隱藏而又找不到合適的地方來隱藏它們時,必須創(chuàng)建一個專用的Factory對象或Service。整個Aggregate通常由一個獨立的Factory來創(chuàng)建,Factory負責把對根的引用傳遞出去,并確保創(chuàng)建出的Aggregate滿足固定規(guī)則。如果Aggregate內(nèi)部的某個對象需要一個Factory,而這個Factory又不適合在Aggregate根上創(chuàng)建,那么應(yīng)該構(gòu)建一個獨立的Factory。
Factory的引入提供了巨大的優(yōu)勢,而這種優(yōu)勢往往并未得到充分利用。但是,在有些情況下直接使用構(gòu)造函數(shù)確實是最佳選擇。Factory實際上會使那些不具有多態(tài)性的簡單對象復(fù)雜化。以下情況下最好使用簡單的、公共的構(gòu)造函數(shù)。
- 類是一種類型。它不是任何相關(guān)層次結(jié)構(gòu)的一部分,而且也沒有通過接口實現(xiàn)多態(tài)性。
- 客戶關(guān)心的是實現(xiàn),可能是將其作為選擇Strategy的一種方式。比如java中的集合類。
- 客戶可以訪問對象的所有屬性,因此向客戶公開的構(gòu)造函數(shù)中沒有嵌套的對象創(chuàng)建。
- 構(gòu)造并不復(fù)雜。
- 公共構(gòu)造函數(shù)必須遵守與Factory相同的規(guī)則:它必須是原子操作,而且要滿足被創(chuàng)建對象的所有固定規(guī)則。
不要在構(gòu)造函數(shù)中調(diào)用其他類的構(gòu)造函數(shù)。構(gòu)造函數(shù)應(yīng)該保持絕對簡單。復(fù)雜的裝配,特別是Aggregate,需要使用Factory。
無論是獨立的Factory還是Factory Method,都要記住以下兩點:
- 每個操作都必須是原子的。我們必須在與Factory的一次交互中把創(chuàng)建對象所需的所有信息傳遞給Factory。同時必須確定當創(chuàng)建失敗時將執(zhí)行什么操作。可以考慮采用編碼標準來處理所有Factory的失敗。
- Factory將與其參數(shù)發(fā)生耦合。如果在選擇輸入?yún)?shù)時不小心,可能會產(chǎn)生錯綜復(fù)雜的依賴關(guān)系。耦合程度取決于對參數(shù)的處理。如果只是簡單地將參數(shù)插入到要構(gòu)建的對象中,則依賴程度適中;如果從參數(shù)選出一部分在構(gòu)造對象時使用,耦合將更緊密。
在某些情況下,把固定規(guī)則的相關(guān)邏輯放到Factory中是有好處的,這樣可以讓被創(chuàng)建對象的職責更明晰。對于Aggregate規(guī)則來說尤其如此(這些規(guī)則會約束很多對象)。固定規(guī)則的相關(guān)邏輯應(yīng)該集中在Entity的構(gòu)造Factory中,特別不適合放到那些與其他領(lǐng)域?qū)ο箨P(guān)聯(lián)的Factory Method中。
檢索操作需要一個復(fù)雜的操作將各個部分重新裝配成一個可用的對象。用于重建對象的Factory與用于創(chuàng)建對象的Factory很類似,主要有以下兩點不同。
- 用于重建對象的Entity Factory不分配新的跟蹤ID。如果分配新ID,將丟失與先前對象的連續(xù)性。因此,在重建對象的Factory中,標識屬性必須是輸入?yún)?shù)的一部分。
- 當固定規(guī)則未被滿足時,重建對象的Factory采用不同的方式進行處理。當創(chuàng)建新對象時,如果未滿足固定規(guī)則,Factory應(yīng)該簡單的拒絕創(chuàng)建對象,但在重建對象時則需要更靈活的響應(yīng)。如果對象已經(jīng)在系統(tǒng)某個地方存在(如數(shù)據(jù)庫中),那么不能忽略這個事實。
Repository
Factory封裝了對象創(chuàng)建和重建時的生命周期轉(zhuǎn)換。還有一種轉(zhuǎn)換大大增加了領(lǐng)域設(shè)計的技術(shù)復(fù)雜性,就是對象與存儲之間的互相轉(zhuǎn)換。這種轉(zhuǎn)換由另一種領(lǐng)域設(shè)計構(gòu)造來處理,它就是Repository。
我們可以通過對象之間的關(guān)聯(lián)找到對象。但當它處于生命周期的中間時,必須要有一個起點,以便從這個起點遍歷到一個Entity或Value。
領(lǐng)域驅(qū)動設(shè)計的目標是通過關(guān)注領(lǐng)域模型(而不是技術(shù))來創(chuàng)建更好的軟件。假設(shè)開發(fā)人員構(gòu)造了一個SQL查詢,并將它傳遞給基礎(chǔ)設(shè)施層中的某個查詢服務(wù),然后再根據(jù)得到的表行數(shù)據(jù)的結(jié)果集提取出所需信息,最后將這些信息傳遞給構(gòu)造函數(shù)或者Factory。開發(fā)人員執(zhí)行這一連串操作的時候,早已不再把模型當作重點了。當客戶代碼直接使用數(shù)據(jù)庫時,開發(fā)人員會試圖繞過模型的功能(如Aggregate,甚至是對象封裝),而直接獲取和操作他們所需的數(shù)據(jù)。這將導(dǎo)致越來越多的領(lǐng)域規(guī)則被嵌入查詢代碼中,或干脆丟失了。如果基礎(chǔ)設(shè)施提供了這方面的便利,那么開發(fā)人員可能會增加很多遍歷的關(guān)聯(lián),這會使模型變得非常混亂。另一方面,開發(fā)人員可能使用查詢從數(shù)據(jù)庫中提取他們所需的數(shù)據(jù),或是直接提取具體的對象,而不是通過Aggregate的根來得到這些對象。這樣就導(dǎo)致領(lǐng)域邏輯進入查詢和客戶代碼中,而Entity和Value Object則變成單純的數(shù)據(jù)容器。采用大多數(shù)處理數(shù)據(jù)庫訪問的技術(shù)復(fù)雜性很快就會使客戶代碼變得混亂,這將導(dǎo)致開發(fā)人員簡化領(lǐng)域?qū)?#xff0c;最終使模型變得無關(guān)緊要。
除了通過根來遍歷查找對象這種方法以外,禁止用其他方法對Aggregate內(nèi)部的任何對象進行訪問。持久化的Value Object一般可以通過遍歷某個Entity來找到,在這里Entity就是把對象封裝在一起的Aggregate的根。
在所有持久化對象中,有一小部分(通常是Entity,有時是具有復(fù)雜內(nèi)部結(jié)構(gòu)的Value Object)必須通過基于對象屬性的搜索來全局訪問。當很難通過遍歷方式來訪問某些Aggregate根的時候,就需要使用這種訪問方式。 其他對象不宜使用這種訪問方式,因為這會混淆它們之間的重要區(qū)別。隨意的數(shù)據(jù)庫查詢會破壞領(lǐng)域?qū)ο蟮姆庋b和Aggregate。技術(shù)基礎(chǔ)設(shè)施和數(shù)據(jù)庫訪問機制的暴露會增加客戶的復(fù)雜度,并妨礙模型驅(qū)動的設(shè)計。
Repository將某種類型的所有對象表示為一個概念集合(通常是模擬的)。它的行為類似于集合,只是具有更復(fù)雜的查詢功能。在添加或刪除相應(yīng)類型的對象時,Repository的后臺機制負責將對象添加到數(shù)據(jù)庫中,或從數(shù)據(jù)庫中刪除對象。這個定義將一組緊密相關(guān)的職責集中在一起,這些職責提供了對Aggregate根的整個生命周期的全程訪問。
客戶使用查詢方法向Repository請求對象,這些查詢方法根據(jù)客戶所指定的條件(通常是特定屬性的值)來挑選對象。Repository檢索被請求的對象,并封裝數(shù)據(jù)庫查詢和元數(shù)據(jù)映射機制。Repository可以根據(jù)客戶所要求的各種條件來挑選對象。它們也可以返回匯總信息,如有多少個實例滿足查詢條件。
Repository解除了客戶的巨大負擔,使客戶只需與一個簡單的、易于理解的接口進行對話,并根據(jù)模型向這個接口提出它的請求。要實現(xiàn)所有這些功能需要大量復(fù)雜的技術(shù)基礎(chǔ)設(shè)施,但接口很簡單,而且在概念層次上與領(lǐng)域模型緊密聯(lián)系在一起。只為那些確實需要直接訪問的Aggregate根提供Repository,讓客戶始終聚焦于模型,而將所有對象的存儲和訪問操作封裝起來,在Repository里來完成。Respository有很多優(yōu)點,包括:
- 它們?yōu)榭蛻籼峁┝艘粋€簡單的模型,可用來獲取持久化對象并管理它們的生命周期。
- 它們使應(yīng)用程序和領(lǐng)域設(shè)計與持久化技術(shù)(多種數(shù)據(jù)庫策略甚至是多個數(shù)據(jù)源)解構(gòu)。
- 它們體現(xiàn)了有關(guān)對象訪問的設(shè)計決策
- 可以很容易將它們替換為“啞實現(xiàn)”,以便在測試中使用(通常使用內(nèi)存中的集合)
在一些需要執(zhí)行大量查詢的項目上,可以構(gòu)建一個支持更靈活查詢的Repository框架。這要求開發(fā)人員熟悉必要的技術(shù),而且一個支持性的基礎(chǔ)設(shè)施會提供巨大的幫助。即使一個Repository的設(shè)計采取了靈活的查詢方式,也應(yīng)該允許添加專門的硬編碼查詢。不支持這些特殊查詢方式的框架有可能會扭曲領(lǐng)域設(shè)計,或干脆被開發(fā)人員棄之不用。客戶代碼可以忽略Repository的實現(xiàn),但開發(fā)人員不能忽略,必須知道在封裝背后都發(fā)生了什么事情。
Repository概念在很多情況下都適用。可能的實現(xiàn)方法有很多,這里只能列出一些需要謹記的注意事項。
- 對類型進行抽象。Repository“含有”特定類型的所有實例,但這并不意味著每個類都需要一個Repository。類型可以是一個層次結(jié)構(gòu)中的抽象超類。類型可以是一個接口,也可以是一個具體的類。由于數(shù)據(jù)庫缺乏這樣的多態(tài)性質(zhì),因此我們將面臨很多約束。
- 充分利用與客戶解耦的優(yōu)點。我們可以很容易地更改Repository的實現(xiàn),但如果客戶直接調(diào)用底層機制,我們就很難修改其實現(xiàn)。也可以利用解耦來優(yōu)化性能,因為這樣就可以使用不同的查詢技術(shù),或在內(nèi)存中緩存對象。
當數(shù)據(jù)庫被視作對象存儲時,數(shù)據(jù)模型與對象模型的差別不應(yīng)太大。可以犧牲一些對象關(guān)系的豐富性,以保證它與關(guān)系模型的緊密關(guān)聯(lián)。簡單的對應(yīng)關(guān)系才是最好的。表中的一行應(yīng)該包含一個對象,也可能包含Aggregate中的一些附屬項。表中的外鍵應(yīng)該轉(zhuǎn)換為對另一個Entity對象的引用。有時我們不得不違背這種簡單的對應(yīng)關(guān)系,但不應(yīng)該由此就全盤放棄簡單映射的原則。
總結(jié)
以上是生活随笔為你收集整理的领域驱动设计理论基础的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Parsing Netflow usin
- 下一篇: CentOS 7设置KVM硬盘模式为SC