領域設計: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了:

  • Entity在整個生命週期中,有多個「狀態」,也就是說「狀態」是可變的(至於變不變就看實際情況了)
  • 而VO在整個生命週期中,只有一個「狀態」,也就是說「狀態」不變

現在,問題又來了,對於VO來說,因爲「狀態」是不可變的,我們就可以用其「狀態」來表示VO!但是對於Entity來說,因爲有多個「狀態」,且「狀態」是可變的,那我們如何來表示呢?以上面的Order爲例,假設同一個買家在同一個賣家那裏買了兩個同樣的商品,那兩個訂單裏的信息都是一樣的,但是它是兩個不同的訂單,我們如何區分這兩個訂單呢?

現在就輪到下一個主角登場了:「標識」!

標識

說到「標識」,我們最先想到的是編程語言中的「引用」或「指針」!比如下面的代碼:

Order orderA = new Order("productA",...);
Order orderB = new Order("productA",...);
orderA.productName = "productB";
  • 前面兩行,orderA和orderB雖然訂單信息(狀態)都相同,但是這是兩個不同的訂單
  • 第三行,即使改了orderA的產品名稱(狀態),依然還是相同的訂單

這解決了「區分相同狀態的不同Entity」的問題,但是沒有解決Entity有多個狀態的問題。因爲「標識」指向的是目標對象的當前狀態。而且,很多編程語言中有個很大的問題,就是不區分「標識」和「狀態」!什麼意思呢?

假設我們在看一部電影,當我們開始觀看時,就是這部電影生命週期的開始,觀看結束就是這部電影生命週期的結束,在這段時間裏,電影的畫面(狀態)一幀幀的呈現在我們面前,我們可以通過播放、快進、後退、暫停改變電影的狀態,每個狀態都是相互獨立的,類似這樣:

領域設計:Entity與VO

 

隨着時間的改變,我們能獲取到電影的不同狀態,每個狀態是相互獨立的。但是實際上我們的代碼邏輯像下面這樣:

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編程樂趣》
  • 《七週七併發模型》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章