領域設計:Entity與VO
本文探討如下內容:
- 什麼是狀態
- 什麼是標識
- 什麼是Entity
- 什麼是VO(ValueObject)
- 在設計中如何識別Entity和VO
要理解Entity和VO,需要先理解兩個概念:「狀態」和「標識」!我們先來聊聊「狀態」!
狀態
大家肯定都在淘寶買過東西吧!在淘寶購買商品後,會有一個訂單,記錄了你購買的商品資訊、價格、店鋪資訊、還有一個特別重要的資訊,就是訂單狀態。通過這個訂單狀態,我們可以知道我們的購物流程現在進行到哪一步了。如果你猶豫了很久才下定決心購買了一件心儀已久的商品,你是不是很在意訂單狀態?時不時要重新整理一下頁面,看看訂單狀態是否顯示已送達了?
開發過系統的都知道,一般訂單狀態都是使用一個欄位來表示的,比如status,不同的狀態就是給status賦不同的值。但是這個status就是「訂單狀態」嗎?難道狀態就是一個欄位?!
Order{ product location seller buyer status ... }
你有沒有想過,當我們說「狀態」的時候,我們實際上指的是什麼?
我們在很多場景下會用到「狀態」這個詞,比如:
- 你今天「狀態」不錯哦
- 朋友又發朋友圈「狀態」了
- 我在淘寶買的商品已經是發貨「狀態」了
- REST(表述性狀態轉移)中的狀態
以「你今天狀態不錯」這句為例,如果狀態就是一個欄位!那麼,「你今天狀態不錯」就是status=1?!「你今天狀態不行」就是status=0?!很明顯,這不合理!
如果「狀態」不是簡單的一個欄位的話,那麼「狀態」到底是什麼呢?
其實在架構風格:你真的懂REST嗎?已經提過了!文中對REST的解釋,有這麼一句:一個由網頁組成的網路(一個虛擬狀態機),使用者通過選擇連結在應用中前進(狀態遷移),導致下一個頁面(應用的下一個狀態的表述)被轉移給使用者,並且呈現給他們,以便他們來使用。
結合上面的幾個場景,你有沒有發現,「狀態」實際上表示的是「目標物件在當前時刻所呈現出的內容」!在軟體系統中通過一個欄位來表示狀態只是一種簡化手段!
如無特殊說明,下面所提到的「狀態」指的是「目標物件在當前時刻所呈現出的內容」,而不是指狀態欄位
- 你今天「狀態」不錯哦:你今天給人的感覺很好
- 朋友又發朋友圈「狀態」了:朋友圈當前的內容
- 我在淘寶買的商品已經是發貨「狀態」了:你的購物流程目前所在的環節
- REST(表述性狀態轉移)中的狀態:當前呈現在使用者面前的頁面
既然「狀態」表示的是「當前時刻所呈現出的內容」!那麼說明了「狀態」是個快照/瞬態!也就是說,「目標物件」有多個「狀態」,「當前狀態」只是「目標物件」眾多「狀態」中的一個!
大家應該玩過定格動畫吧?就像下面這樣(下圖截自《大偵探福爾摩斯2:詭影遊戲》):
圖中的小冊子就是「目標物件」,冊子的每一頁就是「狀態」,當前展示出來的那一頁就是「當前狀態」!
在理解了什麼是「狀態」以後,我們就可以來初步區分Entity和VO了:
- Entity在整個生命週期中,有多個「狀態」,也就是說「狀態」是可變的(至於變不變就看實際情況了)
- 而VO在整個生命週期中,只有一個「狀態」,也就是說「狀態」不變
現在,問題又來了,對於VO來說,因為「狀態」是不可變的,我們就可以用其「狀態」來表示VO!但是對於Entity來說,因為有多個「狀態」,且「狀態」是可變的,那我們如何來表示呢?以上面的Order為例,假設同一個買家在同一個賣家那裡買了兩個同樣的商品,那兩個訂單裡的資訊都是一樣的,但是它是兩個不同的訂單,我們如何區分這兩個訂單呢?
現在就輪到下一個主角登場了:「標識」!
標識
說到「標識」,我們最先想到的是程式語言中的「引用」或「指標」!比如下面的程式碼:
Order orderA = new Order("productA",...); Order orderB = new Order("productA",...); orderA.productName = "productB";
- 前面兩行,orderA和orderB雖然訂單資訊(狀態)都相同,但是這是兩個不同的訂單
- 第三行,即使改了orderA的產品名稱(狀態),依然還是相同的訂單
這解決了「區分相同狀態的不同Entity」的問題,但是沒有解決Entity有多個狀態的問題。因為「標識」指向的是目標物件的當前狀態。而且,很多程式語言中有個很大的問題,就是不區分「標識」和「狀態」!什麼意思呢?
假設我們在看一部電影,當我們開始觀看時,就是這部電影生命週期的開始,觀看結束就是這部電影生命週期的結束,在這段時間裡,電影的畫面(狀態)一幀幀的呈現在我們面前,我們可以通過播放、快進、後退、暫停改變電影的狀態,每個狀態都是相互獨立的,類似這樣:
隨著時間的改變,我們能獲取到電影的不同狀態,每個狀態是相互獨立的。但是實際上我們的程式碼邏輯像下面這樣:
var movie1 = new Movie(); movie1.setCurrentFrame("第三幀"); var currentMovie = movie1 movie1.setCurrentFrame("第四幀"); currentMovie // 還是第三幀嗎?
電影播放到第三幀,我們用一個變數currentMovie儲存了電影的當前狀態(第三幀),但是後面電影播放第四幀了,currentMovie也就變成了第四幀的狀態了。
語言中的這種「標識」(我稱為「隱式標識」)還有另外一個問題,就是無法跨系統。比如,在分散式系統中,需要保證兩個系統中的物件是同一個物件,這種「隱式標識」是做不到的。
所以「隱式標識」並不能滿足我們的需求。我們需要「顯示標識」,「顯示標識」在現實中很常見:
- 每個人都有身份證,即使有兩個人名字相同、性別一樣、身材相同、甚至整容了樣貌都一樣,但是身份證號碼是不一樣的,身份證號碼就是每個人的「顯示標識」
- 一個產品線上生產的產品可以說一模一樣,但是都會有一個唯一的產品編號,這個產品編號就是產品的「顯示標識」
在上面購物的列子中,就相當於給Order一個唯一標識,比如一個唯一的訂單號:
Order{ orderNo // 顯示標識 product location seller buyer status ... }
給定訂單號以後,無論訂單的狀態如何變化,只要訂單號不變,那麼它就是同一個訂單。
所以,「標識」是另一個區分Entity和VO的關鍵點:
- Entity有標識
- 而VO沒有標識
注意標識並不一定只是一個欄位,可能是多個欄位的組合,這需要根據不同的業務邏輯來確定。比如在一個學校系統裡,可以通過學年+班級+學號來標識一個學生。
Entity和VO
理解了標識和狀態,我們就可以來定義Entity和VO了:
- Entity是具有多個「狀態」的物件,「狀態」在其生命週期中可能會改變,通過「標識」來唯一確定這個物件
- VO只有一個「狀態」,且是在建立時就確定的,也就是說VO是不可變的
現在我們知道了什麼是Entity,什麼是VO,那麼我們如何在系統中識別哪些物件是Entity,哪些物件又是VO呢?
如何識別Entity和VO
一個物件是表示成Entity還是VO,取決於系統的關注點。
我們還以淘寶購物為例,假設你在某家店鋪買了個商品,質量很好。過了一段時間後,你想再買一個,但是你記不得是哪家店了,於是你從已完成的訂單列表中點選商品想進去再次購買。但是你點進去後發現,商品下架了。
這是因為「商品」在「訂單系統」中是個VO,而在「商品管理系統」中是Entity!其實很好理解:
- 在「商品管理系統」中,系統需要關注「商品」的「狀態」,需要維護是否上架、庫存多少、各種屬性等資訊(多種狀態)。就是說在「商品管理系統」中,商品狀態是可變的。所以它也有「標識」,即商品ID
- 而「訂單系統」並不關心「商品」的「狀態」變化,它只關注在建立訂單時,這個「商品」的當前「狀態」是什麼,並且在訂單建立完成後,這個「商品」的「狀態」就不會再改變了
在「商品管理系統」中,商品可以這樣表示:
Product { id // 商品標識 name desc status ... }
而在「訂單系統」中,訂單是個Entity,商品是個VO,可以這麼表示:
Order{ orderNo // 訂單標識 product:Product status ... } Product { id // 這裡不是標識,只是狀態 name desc status ... }
注意這裡的id並不是標識,這裡的id實際上退化成了狀態的一部分,保留這個id是為了和「商品管理系統」進行互動,通過id從商品管理系統中查詢商品。當然還有其它方式,例如儲存「商品管理系統」中該商品的歷史URL。
總結
本文從對「狀態」和「標識」的理解開始,一步步來解釋什麼是Entity和VO,以及如何在系統中識別Entity和VO。後面將進一步討論Entity與VO的關係,以及與其它元件的關係,例如DTO,Service,Resporitory,DAO等
參考資料
- 《領域驅動設計:軟體核心複雜性應對之道》
- 《實現領域驅動設計》
- 《Clojure程式設計樂趣》
- 《七週七併發模型》