
最近幾年,領域驅動設計(Domain-Driven Design,DDD)這個術語越來越多地出現在軟件工程師的視野里。對DDD不熟悉的人可能會覺得它是軟件領域里的一個新的概念,但是實際上,Eric Evans在十幾年前就已經提出了這個概念。這個“古老”的概念在之所以能夠重煥新生,很大程度上是因為遇上了“微服務”這個浪潮。如果把DDD里面的理念拿去和微服務架構做對比,你會發現它們有著高度的相似性——DDD里的限界上下文不正是微服務架構中的微服務嗎?于是,各大公司紛紛運用DDD的方法論來構建自己的產品架構。有些團隊成功地將DDD結合到了產品架構中,產生了許多優秀的實踐;也有些團隊反映DDD太過復雜,很難落地。那么DDD究竟是個什么樣的理念?為什么大家都爭先恐后地使用它,彷佛不加點DDD都不好意思說自己是微服務架構?然而又為什么那么多團隊說DDD難以落地?本文將會結合簡單的代碼實現來談談筆者對DDD的理解。
什么是領域驅動設計?
軟件的核心是其為用戶解決領域相關的問題的能力。
軟件就是為了解決某一領域相關問題而存在的,比如一個普通的計算器軟件,就是為了滿足我們進行簡單的加減乘除運算而存在。對于計算器這種小而簡單的軟件,一個普通的軟件工程師可能花上幾天就能過設計開發出來,而且基本不會出現Bug。但是對于一些大型而且復雜的系統,一個團隊都得花上很長的時間去設計整個架構,然后經過n輪迭代才能開發出可用的版本,而且后面還有各種Bug要去處理。比如證券交易系統,里面就包括了用戶系統、賬戶系統、訂單系統、撮合系統等一系列的子系統,而且其中的調用關系和業務都非常復雜。像這樣一個龐大的系統,怎樣才能把它設計好呢?這正是DDD要回答的問題。
領域驅動設計(DDD)是一種軟件開發的方法論,旨在幫助我們設計出高質量的軟件模型。
在軟件領域,解決復雜問題的方法不外乎是“分治”和“抽象”,DDD也是基于這兩個理念建立起一套方法論。其中將一個系統劃分成多個限界上下文,限界上下文中劃分出多個子域,這是分治;然后在分別對各個子域進行領域建模,這是抽象。當你在設計一個業務復雜的系統卻無從下手時,嘗試一下DDD,說不定困難就會迎刃而解了。DDD中最核心的理念就是領域建模,可以說它提供的各種方法都是為了幫助我們設計出更能準確傳達業務規則的領域模型。一個好的領域模型可以讓一個系統更加健壯,可以讓一個框架易用性更加好,可以讓一段代碼更加好維護。那么,什么樣的模型才是好的領域模型?下面,我們通過一個例子來簡單說明下。
什么是領域模型?
領域模型是關于某個特定業務領域的軟件模型。通常,領域模型通過對象模型來實現,這些對象同事包含了數據和行為,并且表達了準確的業務含義。
日期和時間領域模型
如何設計一個日期和時間API?
首先需要對日期和時間的概念進行建模,從直覺上,我們可以將日期和時間抽象成一個對象Date
。另外,時間和日期經常都需要進行格式化輸出,因此我們還需要一個用于表示時間格式的對象DateFormat
。為了更好地表示年月周日等概念,再抽象出一個表示日歷的Calendar
對象,以及表示時區的TimeZone
對象。

相信到這里大家都已經知道,這正是JDK 1.1版本的日期時間API,下面我們先回顧一下它的用法:
public class TestOldDate {public static void main(String[] args) {// 獲取表示當前時刻的Date對象Date date1 = new Date();// 通過Calendar等到指定日期時間的Date對象,采用當前的系統時區Calendar calendar = Calendar.getInstance(TimeZone.getDefault());calendar.set(2020, 2, 10, 0, 0, 0);Date date2 = calendar.getTime();// 進行時間比較System.out.println("date1 is after date2: " + date1.after(date2));// 進行時間的加減法,如獲得昨天的這個時刻:calendar.setTime(date1);calendar.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH) - 1);Date date3 = calendar.getTime();// 對日期格式化輸出DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");System.out.println("date3: " + df.format(date3));}
}
/* Output:
date1 is after date2: false
date3: 2020-02-07 23:55:39
*/
如果你用習慣了JDK 1.1版本的日期時間API,可能會覺得上述例子中的用法也沒有多大的問題。但是,仔細思考一下就會發現這其中的邏輯跟我們人類對時間的處理邏輯不太像,比如要對時間做加法,首先要將需要操作的Date
對象設置到Calendar
,然后對Calendar
做加法,最后調用Calendar
的接口得到結果。時間的加法難道不應該直接對Date
對象加上一個時間段就行了嗎?
相信很多小伙伴都會遇到這種情況,自己寫出來的代碼可讀性不夠好。這其中原因可能是對領域的理解不夠深,設計出來的領域模型沒能準確的表達業務邏輯(如JDK 1.1的日期時間模型),或者開發前根本就沒有進行領域建模。這樣容易導致采用了一種“機器思維”去進行開發,而不是按照我們平常思考問題的思維去開發。
JDK 1.8引入了全新的日期和時間API來解決老版本的API所存在的種種問題,下面,我們來看一下比之前更準確地表達日期和時間的領域模型:

JDK 1.8的日期和時間領域模型中的領域知識明顯比老版本的領域模型要豐富很多,而且模型更加符合人類思考日期和時間的思維。下面,我們看下新的日期和時間API的用法:
public class TestNewDate {public static void main(String[] args) {// 獲取表示當前時刻的LocalDateTime對象LocalDateTime date1 = LocalDateTime.now(ZoneId.systemDefault());// 獲取指定時間的LocalDateTime對象LocalDateTime date2 = LocalDateTime.of(LocalDate.of(2020, 2, 10),LocalTime.of(0, 0, 0));// 進行是時間比較System.out.println("date1 is after date2: " + date1.isAfter(date2));// 進行時間的加減法,如獲得昨天的這個時刻:LocalDateTime date3 = date1.minus(Period.ofDays(1));// 對時間進行格式化輸出DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");System.out.println("date3: " + df.format(date3));}
}
/* Output:
date1 is after date2: false
date3: 2020-02-07 23:56:51
*/
實現同樣的功能,JDK 1.8版本的日期和時間API明顯更加簡潔,而且代碼的邏輯更加符合人的思維,可讀性更好(比如使用.now()
函數創建當前時刻的LocalDateTime
對象,代碼閱讀起來就跟人類的自然語言一樣)。由此可見,設計出一個高質量的領域模型對于軟件系統是多么的重要。
在這個例子中,JDK并沒有顯式地使用DDD提供的戰術建模手段對日期和時間API進行設計,但是從JDK 1.1到JDK 1.8版本中的變化,其中就蘊含著DDD最核心的內容:設計出更符合業務規則和人類思維的領域模型。從這個例子中我們也能看出,DDD并沒有傳說中的那么神秘,也未必一定要運用在復雜的系統,即使是一個簡單的日期和時間API,其中也可以看到它的身影。
如果讓你對JDK 1.1的日期和時間API進行優化,相信很多人都設計不出像JDK 1.8版本的這樣優秀的API,不管在經驗,還是在方法上我們都欠點火候。簡單的API如此,更別說設計復雜的大型系統了。這時,我們需要一些具體的建模方法來指導設計。
DDD的建模方法
DDD主要提出了兩種建模方法來幫助我們設計出高質量的領域模型:戰略建模和戰術建模。
戰略建模根據領域知識對系統進行限界上下文和子域的劃分,戰術建模具體為每個限界上下文設計出領域模型。而這兩者中又內涵很多知識點,光看下面的這張DDD的概念圖,你可能會覺得DDD太過復雜了。

確實,DDD的學習曲線比較陡,特別是第一次看Eric Evans所著的《領域驅動設計——軟件核心復雜性應對之道》時,會有種不知所云的感覺。再看Vaughn Vernon所著的《實現領域驅動設計》可能會好點了,但是里面提到的各種概念還是沒能很清晰地理解。所謂“實踐出真知”,只有通過不斷地實踐,才能學習到DDD的精髓,體會到它的魔力。下一篇文章,我們將開始通過實踐一個簡單的業務功能著手介紹DDD的各種理論知識。
http://weixin.qq.com/r/Tx2zqxDE602UrVTI90hd (二維碼自動識別)