Stories

Detail Return Return

DDD你真的理解清楚了嗎?怎麼準確理解“值對象” - Stories Detail

這些年,隨着軟件業的不斷髮展,軟件系統開始變得越來越複雜而難於維護。這時,越來越多的開發團隊開始選擇實踐DDD領域驅動設計。領域驅動設計是一種非常優秀的軟件設計思想,它可以非常好地幫助我們梳理複雜業務,解決大規模業務系統的設計開發與更新維護。但是,領域驅動的學習成本卻非常高,使得很多同學難於準確地理解DDD,更難於真正落地實際項目的設計編碼。為此,我通過這一系列知識分享,讓大家真正準確地理解DDD中這些晦澀的概念,特別是讓大家理解最終是怎麼落地到軟件項目的設計開發中的。

今天,我們首先探討的是在DDD中,讓大家最頭疼、最晦澀的概念——什麼是“值對象”?要理解“值對象”的設計思想,首先我們先來梳理一下領域驅動的核心思想。領域驅動認為,軟件的本質就是對真實世界的模擬,軟件中所有業務邏輯正確與否,唯一的判定標準就是,是否與真實世界保持一致。如果一致,設計就是OK的,否則用户就會提BUG,或者變更需求。只要理解了這個本質,軟件設計就簡單了:我們首先理解真實世界的業務,然後將我們對業務的理解形成軟件設計。

然而,這裏有一個問題,那就是軟件是怎麼與真實世界對應的呢?這種對應體現在以下三個方面:

1)真實世界有什麼事物,軟件世界就有什麼對象;

2)真實世界中這些事物有什麼行為,軟件世界這些對象就有什麼方法;

3)真實世界中這些事物間有什麼關係,軟件世界中這些對象就有什麼關聯。

因此,我們在軟件設計時,首先以業務場景為單位,一個一個地分析每個業務場景都有哪些領域對象,以及它們相互之間的行為與關係,形成領域模型。然後再以領域模型為核心,完成軟件的設計與開發。

譬如説,我們要設計一套電子商務系統,按照業務需求會劃分為很多功能,那麼每個功能就是一個業務場景。譬如在“下單”這個業務場景中,在真實世界中都有哪些事物呢?首先有“訂單”,可以形成訂單對象;每個訂單對應一個用户,但一個用户可以有多個訂單,所以從訂單到用户是一個“多對一”關係;一個用户有多個地址,然而一個訂單隻能對應一個地址……如下圖,我們按照這樣的步驟逐一分析領域對象和它們之間的關係。最後,我們對訂單有什麼操作呢?有“下單”、“支付”、“查看訂單”……

這就是領域模型,訂單、用户、地址、訂單明細等類,都是領域對象。然而,在DDD中還要把領域對象嚴格區分為“實體”與“值對象”。這時,很多同學就比較暈,什麼是實體?什麼是值對象?為了準確理解這個地方,我們先看看《領域驅動設計》原著是怎麼説的。

實體(Entity):又稱為Reference Object,它具備生命週期,並且在生命週期的過程中,其形式和內容都有可能發生變化,但它的標識永遠不變。也就是説,每個實體都有一個唯一的標識,用於區分真實世界中的“他”與其他人,比如用户ID。實體是有生命週期的,比如用户的註冊與註銷;實體中的一切都可能會變,比如小張將自己的賬户轉讓給了李四,但賬户ID是不會變的。這個唯一的標識就是主鍵,以這種形式的設計,用户有用户ID、賬户有賬户ID、訂單有訂單ID,就是實體的設計。很顯然,這樣的設計大家都能夠理解,關鍵是“值對象”。

值對象(Value Object):在《領域驅動設計》這本書中對值對象的描述比較隱晦,但我們大致可以歸納為以下幾個特徵:對象聲明與對象中的屬性都是不變的,可以為多個對象所引用與共享(而不是複製),這就是“值對象”。該怎樣來理解這幾個特徵呢?

譬如,在真實世界中,一個用户可以有多個訂單,那麼在訂單查詢時,每個訂單就是一個訂單對象,每個訂單對象都有各自不同的訂單ID。因此,訂單的設計是實體。然而,每個訂單都要引用一個用户,也就是指向一個用户對象。那麼,同一個用户的多個訂單指向的是誰呢?顯然,不能將這個用户複製成多個對象,而是所有這幾個訂單都引用的是這一個對象。也就是説,這裏的“用户”對象是值對象。

然而,值對象又必須是不變的,這就是説,如果“用户”是值對象,那麼用户及其相關的屬性都必須是不變的。但現實情況是,用户及其用户信息都是可變的,我們會對用户信息進行增刪改操作。是的,在真實世界中,一切都是在變化,沒有什麼是不變的。那麼,怎麼來理解值對象的不變性呢?

在DDD的領域建模中,除了有領域模型以外,還有一個非常重要的設計叫“限界上下文”。領域模型是描述的真實世界,但真實世界又是無限大的,那麼領域模型該如何描述真實世界呢?DDD將一個複雜系統的業務劃分成很多個限界上下文,然後對每個限界上下文中進行領域建模。這樣,每個領域模型都有各自的邊界,我只是在這個邊界中描述我的業務。這樣的設計,就將紛繁複雜的世界“分而治之”,對每個領域模型的分析就變得簡單了。

譬如在以上案例中,可以首先將對用户及其用户檔案的管理劃分成一個上下文,叫“用户上下文”。在這個上下文中,用户及其屬性是可變的,要進行增刪改的操作,因此“用户”對象的設計是實體。然而,在另一個“訂單上下文”中,訂單是實體,要對其進行增刪改,但訂單引用的“用户”以及“地址”就是值對象。也就是説,在訂單上下文中,“用户”以及“地址”是隻讀的,僅僅用於訂單對象的引用,而不會對其進行增刪改操作。這樣,“用户”以及“地址”作為值對象,就體現了它的只讀與不變性。

此外,站在開發的角度來説,領域模型最終要落實到軟件開發。如果採用微服務的設計,就會將“用户上下文”與“訂單上下文”劃分為用户微服務與訂單微服務。在劃分微服務的同時,也會劃分數據庫,用户微服務有用户數據庫,訂單微服務有訂單數據庫。有了這樣的設計,用户表必然是設計在用户數據庫中,而不是在訂單數據庫中。當訂單微服務要查詢訂單時,通過調用用户微服務的接口獲得訂單相關的用户對象。然而,這些用户對象只存在於訂單微服務的內存中,是隻讀的,而不會存儲在訂單數據庫中去增刪改。這就是“值對象”的概念及其設計實現。

總而言之,DDD的實體在本上下文中是可讀可寫的,而值對象是隻讀的。在一個上下文中的核心業務是對實體的增刪改,而值對象是與實體相關聯的,僅僅用於實體的引用與查詢的對象。值對象的特性是隻讀,但在實際項目中的表現有2種形式:

1)  對其它微服務中對象的引用,需要調用其它微服務的接口獲得,數據不在我本地的數據庫中,僅僅存在於內存中,並且不能修改,只能查詢;

2)  一些類似類型、種類、類別的字典數據,雖然數據存儲在本地數據庫中,但沒有增刪改的功能,只有查詢與引用。例如會員等級、積分規則、支付方式,等等。

(待續)

Add a new Comments

Some HTML is okay.