概述
軟件開發(fā)不是一蹴而就的事情,我們不可能在不了解產(chǎn)品(行業(yè)領(lǐng)域)的前提下進(jìn)行軟件開發(fā),在開發(fā)前,通常需要進(jìn)行大量的業(yè)務(wù)知識梳理,而后到達(dá)軟件設(shè)計的層面,最后才是開發(fā)。而在業(yè)務(wù)知識梳理的過程中,我們必然會形成某個領(lǐng)域知識,根據(jù)領(lǐng)域知識來一步步驅(qū)動軟件設(shè)計,就是領(lǐng)域驅(qū)動設(shè)計的基本概念
軟件開發(fā)和DDD區(qū)別
一般軟件設(shè)計或者說軟件開發(fā)分兩種:瀑布式,敏捷式。
瀑布式:一般是項目經(jīng)理經(jīng)過大量的業(yè)務(wù)分析后,會基于現(xiàn)有需求整理出一個基本模型,再將結(jié)果傳遞給開發(fā)人員,這就是開發(fā)人員的需求文檔,他們只需要照此開發(fā)便是。這種模式下,是很難頻繁的從用戶那里得到反饋,因此在前期分析時就已經(jīng)默認(rèn)了這個業(yè)務(wù)模型是正確的,那么結(jié)果可想而之,數(shù)月甚至數(shù)年后交付的時候,必然和客戶的預(yù)期差距較大。
敏捷式:在此基礎(chǔ)上進(jìn)行了改進(jìn),它也需要大量的分析,范圍會設(shè)計到更精細(xì)的業(yè)務(wù)模塊,它是小步迭代,周期性交付,那么獲取客戶的反饋也就比較頻繁和及時??擅艚菀膊荒軌?qū)I(yè)務(wù)中的方方面面都考慮到,并且敏捷是擁抱變化的,大量的需求或者業(yè)務(wù)模型變更必將帶來不小的維護成本,同時,對人的要求也必然會更高。
DDD則不同:它像是更小粒度的迭代設(shè)計,它的最小單元是領(lǐng)域模型(Domain Model),所謂領(lǐng)域模型就是能夠精確反映領(lǐng)域中某一知識元素的載體,這種知識的獲取需要通過與領(lǐng)域?qū)<?Domain Expert)進(jìn)行頻繁的溝通才能將專業(yè)知識轉(zhuǎn)化為領(lǐng)域模型。領(lǐng)域模型無關(guān)技術(shù),具有高度的業(yè)務(wù)抽象性,它能夠精確的描述領(lǐng)域中的知識體系;同時它也是獨立的,我們還需要學(xué)會如何讓它具有表達(dá)性,讓模型彼此之間建立關(guān)系,形成完整的領(lǐng)域架構(gòu)。通常我們可以用象形圖或一種通用的語言(Ubiquitous Language)去描述它們之間的關(guān)系。在此之上,我們就可以進(jìn)行領(lǐng)域中的代碼設(shè)計(Domain Code Design)。如果將軟件設(shè)計比做是造一座房子,那么領(lǐng)域代碼設(shè)計就好比是貼壁紙。前者已經(jīng)將房子的藍(lán)圖框架規(guī)劃好,而后者只是一個小部分的設(shè)計:如果墻紙貼錯了,我們可以重來,可如果房子結(jié)構(gòu)設(shè)計錯了,那可就悲劇了。
建立領(lǐng)域知識
說了這么多領(lǐng)域模型的概念,到底什么是領(lǐng)域模型呢?以飛機航行為例子:
現(xiàn)要為航空公司開發(fā)一款能夠為飛機提供導(dǎo)航,保證無路線沖突監(jiān)控軟件。那我們應(yīng)該從哪里開始下手呢?根據(jù)DDD的思路,我們第一步是建立領(lǐng)域知識:作為平時管理和維護機場飛行秩序的工作人員來說,他們自然就是這個領(lǐng)域的專家,我們第一個目標(biāo)就是與他們溝通,也許我們并不能從中獲取所有想要的知識,但至少可以篩選出主要的內(nèi)容和元素。你可能會聽到諸如起飛,著陸,飛行沖突,延誤等領(lǐng)域名詞,讓們從一個簡單的例子開始(就算是錯誤的也沒關(guān)系):
- 起點->飛機->終點
這個模型很直接,但有點過于簡單,因為我們無法看出飛機在空中做了什么,也無法得知飛機怎么從起點到的終點,剛才我們似乎提到無路線沖突,那么如此似乎會好些:
- 飛機->路線->起點/終點
既然點構(gòu)成線,那何不:
飛機->路線->points(含起點,終點)
這個過程,是我們不斷建立領(lǐng)域知識的過程,其中的重點就是尋找領(lǐng)域?qū)<翌l繁溝通,從中提煉必要領(lǐng)域元素。盡管看起來還是很簡單,但我們已經(jīng)開始一步步的在建立領(lǐng)域?qū)ο蠛皖I(lǐng)域模型了。
通用語言
上面的例子的確看起來簡單,但過程并非容易:我們(開發(fā)人員)和領(lǐng)域?qū)<以跍贤ǖ倪^程中是存在天然屏障的:我們滿腦子都是類,方法,設(shè)計模式,算法,繼承,封裝,多態(tài),如何面向?qū)ο蟮鹊?;這些領(lǐng)域?qū)<沂遣欢?,他們只知道飛機故障,經(jīng)緯度,航班路線等專業(yè)術(shù)語。
所以,在建立領(lǐng)域知識的時候,我們(開發(fā)人員和領(lǐng)域?qū)<遥┍仨氁粨Q知識,知識的范圍范圍涉及領(lǐng)域模型的各個元素,如果一方對模型的描述令對方感到困惑,那么應(yīng)該立刻換一種描述方式,直到雙方都能夠接受并且理解為止。在這一過程中,就需要建立一種通用語言,作為開發(fā)人員和領(lǐng)域?qū)<业臏贤蛄骸?br>
可如何形成這種通用語言呢?其實答案并不唯一,確切的說也沒有什么標(biāo)準(zhǔn)答案。
- UML
利用UML可以清晰的表現(xiàn)類,并且展示它們之間的關(guān)系。但是一旦聚合關(guān)系復(fù)雜,UML葉子節(jié)點將會變的十分龐大,可能就沒有那么直觀易懂了。最重要的是,它無法精確的描述類的行為。為了彌補這種缺陷,可以為具體的行為部分補充必要說明(可以是標(biāo)簽或者文檔),但這往往又很耗時,而且更新維護起來十分不便。 - 偽代碼
極限編程是推薦這么做的,這個辦法對程序猿來說固然好,可立刻就要將現(xiàn)有模型映射到代碼層面,這對人的要求也是不低,并不容易實現(xiàn)。
模型驅(qū)動設(shè)計
- 分層架構(gòu):DDD中將系統(tǒng)分為UI層,應(yīng)用層,領(lǐng)域?qū)右约盎A(chǔ)設(shè)施層。
- User Interface
負(fù)責(zé)向用戶展現(xiàn)信息,并且會解析用戶行為,即常說的展現(xiàn)層。 - Application Layer
應(yīng)用層沒有任何的業(yè)務(wù)邏輯代碼,它很簡單,它主要為程序提供任務(wù)處理。 - Domain Layer
這一層包含有關(guān)領(lǐng)域的信息,是業(yè)務(wù)的核心,領(lǐng)域模型的狀態(tài)都直接或間接(持久化至數(shù)據(jù)庫)存儲在這一層。 -
Infrastructure Layer
為其他層提供底層依賴操作。
層結(jié)構(gòu)的劃分是很有必要的,只有清晰的結(jié)構(gòu),那么最終的領(lǐng)域設(shè)計才宜用,比如用戶要預(yù)定航班,向Application Layer的service發(fā)起請求,而后Domain Layler從Infrastructure Layer獲取領(lǐng)域?qū)ο?,校驗通過后會更新用戶狀態(tài),最后再次通過Infratructure Layer持久化到數(shù)據(jù)庫中。
分層架構(gòu)上圖中,應(yīng)用層是很薄的一層,因為它只負(fù)責(zé)接收UI層傳來的參數(shù)和路由到對應(yīng)的領(lǐng)域模型,它不負(fù)責(zé)處理具體的業(yè)務(wù)邏輯。系統(tǒng)的業(yè)務(wù)邏輯放在了領(lǐng)域?qū)又?,所以,領(lǐng)域?qū)釉谙到y(tǒng)架構(gòu)中占據(jù)了很大的面積。
在層級結(jié)構(gòu)中,上層模塊調(diào)用下層模塊提供的服務(wù),這里就會存在一種依賴關(guān)系,Rebort C. Martin提出的依賴倒置原則大致是如下:
上層模塊不應(yīng)該依賴于下層模塊,兩者都應(yīng)該依賴于抽象;
抽象不應(yīng)該依賴于實現(xiàn),實現(xiàn)應(yīng)該依賴于抽象;
這是一個面向接口編程的思想,抽象說的是抽象類或接口,實現(xiàn)就是具體實現(xiàn)了這些抽象的實現(xiàn)類。翻譯成白話文是這樣的,上下層之間應(yīng)該通過接口來通訊,接口定義的位置就決定了上下層的依賴關(guān)系是否倒置。比如Application層和Domain層進(jìn)行通訊,接口與接口的實現(xiàn)類都定義在Domain層中,這是正常的面向接口編程,不存在倒置關(guān)系。而Domain層和基礎(chǔ)設(shè)施層進(jìn)行通訊時,原本是Domain層去依賴基礎(chǔ)設(shè)施層,如果我們將接口定義在Domain層,而實現(xiàn)類定義在基礎(chǔ)設(shè)施層,那么,基礎(chǔ)設(shè)施層就將依賴Domain層,這就是“倒置”這個詞的來由。實際上,我們在做這樣分層架構(gòu)設(shè)計時,都是將接口定義在Domain層的。
實體(Entity) & 值對象(Value Object)
實體與面向?qū)ο笾械母拍铑愃?,在這里再次提出是因為它是領(lǐng)域模型的基本元素。在領(lǐng)域模型中,實體應(yīng)該具有唯一的標(biāo)識符,從設(shè)計的一開始就應(yīng)該考慮實體,決定是否建立一個實體也是十分重要的。值對象和我們說的編程中數(shù)值類型的變量是不同的,它僅僅是沒有唯一標(biāo)識符的實體,比如有兩個收獲地址的信息完全一樣,那它就是值對象,并不是實體。值對象在領(lǐng)域模型中是可以被共享的,他們應(yīng)該是“不可變的”(只讀的),當(dāng)有其他地方需要用到值對象時,可以將它的副本作為參數(shù)傳遞。
服務(wù)
當(dāng)我們在分析某一領(lǐng)域時,一直在嘗試如何將信息轉(zhuǎn)化為領(lǐng)域模型,但并非所有的點我們都能用Model來涵蓋。對象應(yīng)當(dāng)有屬性,狀態(tài)和行為,但有時領(lǐng)域中有一些行為是無法映射到具體的對象中的,我們也不能強行將其放入在某一個模型對象中,而將其單獨作為一個方法又沒有地方,此時就需要服務(wù)。服務(wù)是無狀態(tài)的,對象是有狀態(tài)的。所謂狀態(tài),就是對象的基本屬性:高矮胖瘦,年輕漂亮。服務(wù)本身也是對象,但它卻沒有屬性(只有行為),因此說是無狀態(tài)的。
服務(wù)存在的目的就是為領(lǐng)域提供簡單的方法。為了提供大量便捷的方法,自然要關(guān)聯(lián)許多領(lǐng)域模型,所以說,行為(Action)天生就應(yīng)該存在于服務(wù)中。
服務(wù)具有以下特點:
- 服務(wù)中體現(xiàn)的行為一定是不屬于任何實體和值對象的,但它屬于領(lǐng)域模型的范圍內(nèi)
- 服務(wù)的行為一定設(shè)計其他多個對象
- 服務(wù)的操作是無狀態(tài)的
模塊
對于一個復(fù)雜的應(yīng)用來說,領(lǐng)域模型將會變的越來越大,以至于很難去描述和理解,更別提模型之間的關(guān)系了。模塊的出現(xiàn),就是為了組織統(tǒng)一的模型概念來達(dá)到減少復(fù)雜性的目的的。而另一個原因則是模塊可以提高代碼質(zhì)量和可維護性,比如我們常說的高內(nèi)聚,低耦合就是要提倡將相關(guān)的類內(nèi)聚在一起實現(xiàn)模塊化。模塊應(yīng)當(dāng)有對外的統(tǒng)一接口供其他模塊調(diào)用,比如有三個對象在模塊a中,那么模塊b不應(yīng)該直接操作這三個對象,而是操作暴露的接口。模塊的命名也很有講究,最好能夠深層次反映領(lǐng)域模型。
聚合
聚合被看作是多個模型單元間的組合,它定義了模型的關(guān)系和邊界。每個聚合都有一個根,根是一個實體,并且是唯一可被外訪問的。正是如此,聚合可以保證多個模型單元的不變性,因為其他模型都參考聚合的根。所以要想改變其他對象,只能通過聚合的根去操作。根如果沒有了,那么聚合中的其他對象也將不存在。
一個簡單的例子如下:
customer是該聚合的根,其他的都是內(nèi)部對象,如果外部需要用戶地址,拷貝一份傳遞出去即可。顯而易見,用戶如果不存在,其他信息均無意義。
工廠
在大型系統(tǒng)中,實體和聚合通常是很復(fù)雜的,這就導(dǎo)致了很難去通過構(gòu)造器來創(chuàng)建對象。工廠就決解了這個問題,它把創(chuàng)建對象的細(xì)節(jié)封裝起來,巧妙的實現(xiàn)了依賴反轉(zhuǎn)。當(dāng)然對聚合也適用(當(dāng)建立了聚合根時,其他對象可以自動創(chuàng)建)。工廠最早被大家熟知可能還是在設(shè)計模式中,的確,在這里提到的工廠也是這個概念。但是不要盲目的去應(yīng)用工廠,以下場景不需要工廠:
- 構(gòu)造器很簡單
- 構(gòu)造對象時不依賴于其他對象的創(chuàng)建
- 用策略模式就可以解決
倉庫
倉庫封裝了獲取對象的邏輯,領(lǐng)域?qū)ο鬅o須和底層數(shù)據(jù)庫交互,它只需要從倉庫中獲取對象即可。倉庫可以存儲對象的引用,當(dāng)一個對象被創(chuàng)建后,它可能會被存儲到倉庫中,那么下次就可以從倉庫取。如果用戶請求的數(shù)據(jù)沒在倉庫中,則會從數(shù)據(jù)庫里取,這就減少了底層交互的次數(shù)。
