(轉)領域驅動設計系列文章(2)——淺析VO、DTO、DO、PO的概念、區別和用處
上一篇文章作為一個引子,說明了領域驅動設計的優勢,從本篇文章開始,筆者將會結合自己的實際經驗,談及領域驅動設計的應用。本篇文章主要討論一下我們經常會用到的一些對象: VO 、 DTO 、 DO 和 PO 。
由于不同的項目和開發人員有不同的命名習慣,這里我首先對上述的概念進行一個簡單描述,名字只是個標識,我們重點關注其概念:
概念:
VO ( View Object ): 視圖對象,用于展示層,它的作用是把某個指定頁面(或組件)的所有數據封裝起來。
DTO ( Data Transfer Object ): 數據傳輸對象,這個概念來源于 J2EE 的設計模式,原來的目的是為了 EJB 的分布式應用提供粗粒度的數據實體,以減少分布式調用的次數,從而提高分布式調用的性能和降低網絡負載,但在這里,我泛指用于展示層與服務層之間的數據傳輸對象。
DO ( Domain Object ): 領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。
PO ( Persistent Object ): 持久化對象,它跟持久層(通常是關系型數據庫)的數據結構形成一一對應的映射關系,如果持久層是關系型數據庫,那么,數據表中的每個字段(或若干個)就對應 PO 的一個(或若干個)屬性。
模型:
下面以一個時序圖建立簡單模型來描述上述對象在三層架構應用中的位置
l 用戶發出請求(可能是填寫表單),表單的數據在展示層被匹配為 VO 。
l 展示層把 VO 轉換為服務層對應方法所要求的 DTO ,傳送給服務層。
l 服務層首先根據 DTO 的數據構造(或重建)一個 DO ,調用 DO 的業務方法完成具體業務。
l 服務層把 DO 轉換為持久層對應的 PO (可以使用 ORM 工具,也可以不用),調用持久層的持久化方法,把 PO 傳遞給它,完成持久化操作。
l 對于一個逆向操作,如讀取數據,也是用類似的方式轉換和傳遞,略。
VO 與 DTO 的區別
大家可能會有個疑問(在筆者參與的項目中,很多程序員也有相同的疑惑):既然 DTO 是展示層與服務層之間傳遞數據的對象,為什么還需要一個 VO 呢?對!對于絕大部分的應用場景來說, DTO 和 VO 的屬性值基本是一致的,而且他們通常都是 POJO ,因此沒必要多此一舉,但不要忘記這是實現層面的思維,對于設計層面來說,概念上還是應該存在 VO 和 DTO ,因為兩者有著本質的區別, DTO 代表服務層需要接收的數據和返回的數據,而 VO 代表展示層需要顯示的數據。
用一個例子來說明可能會比較容易理解:例如服務層有一個 getUser 的方法返回一個系統用戶,其中有一個屬性是 gender( 性別 ) ,對于服務層來說,它只從語義上定義: 1- 男性, 2- 女性, 0- 未指定,而對于展示層來說,它可能需要用“帥哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。說到這里,可能你還會反駁,在服務層直接就返回“帥哥美女”不就行了嗎?對于大部分應用來說,這不是問題,但設想一下,如果需求允許客戶可以定制風格,而不同風格對于“性別”的表現方式不一樣,又或者這個服務同時供多個客戶端使用(不同門戶),而不同的客戶端對于表現層的要求有所不同,那么,問題就來了。再者,回到設計層面上分析,從職責單一原則來看,服務層只負責業務,與具體的表現形式無關,因此,它返回的 DTO ,不應該出現與表現形式的耦合。
理論歸理論,這到底還是分析設計層面的思維,是否在實現層面必須這樣做呢?一刀切的做法往往會得不償失,下面我馬上會分析應用中如何做出正確的選擇。
VO 與 DTO 的應用
上面只是用了一個簡單的例子來說明 VO 與 DTO 在概念上的區別,本節將會告訴你如何在應用中做出正確的選擇。
在以下才場景中,我們可以考慮把 VO 與 DTO 二合為一(注意:是實現層面):
l 當需求非常清晰穩定,而且客戶端很明確只有一個的時候,沒有必要把 VO 和 DTO 區分開來,這時候 VO 可以退隱,用一個 DTO 即可,為什么是 VO 退隱而不是 DTO ?回到設計層面,服務層的職責依然不應該與展示層耦合,所以,對于前面的例子,你很容易理解, DTO 對于“性別”來說,依然不能用“帥哥美女”,這個轉換應該依賴于頁面的腳本(如 JavaScript )或其他機制( JSTL 、 EL 、 CSS )
l 即使客戶端可以進行定制,或者存在多個不同的客戶端,如果客戶端能夠用某種技術(腳本或其他機制)實現轉換,同樣可以讓 VO 退隱
以下場景需要優先考慮 VO 、 DTO 并存:
l 上述場景的反面場景
l 因為某種技術原因,比如某個框架(如 Flex )提供自動把 POJO 轉換為 UI 中某些 Field 時,可以考慮在實現層面定義出 VO ,這個權衡完全取決于使用框架的自動轉換能力帶來的開發和維護效率提升與設計多一個 VO 所多做的事情帶來的開發和維護效率的下降之間的比對。
l 如果頁面出現一個“大視圖”,而組成這個大視圖的所有數據需要調用多個服務,返回多個 DTO 來組裝(當然,這同樣可以通過服務層提供一次性返回一個大視圖的 DTO 來取代,但在服務層提供一個這樣的方法是否合適,需要在設計層面進行權衡)。
DTO 與 DO 的區別
首先是概念上的區別, DTO 是展示層和服務層之間的數據傳輸對象(可以認為是兩者之間的協議),而 DO 是對現實世界各種業務角色的抽象,這就引出了兩者在數據上的區別,例如 UserInfo 和 User (對于 DTO 和 DO 的命名規則,請參見筆者前面的一篇博文),對于一個 getUser 方法來說,本質上它永遠不應該返回用戶的密碼,因此 UserInfo 至少比 User 少一個 password 的數據。而在領域驅動設計中,正如第一篇系列文章所說, DO 不是簡單的 POJO ,它具有領域業務邏輯。
DTO 與 DO 的應用
從上一節的例子中,細心的讀者可能會發現問題:既然 getUser 方法返回的 UserInfo 不應該包含 password ,那么就不應該存在 password 這個屬性定義,但如果同時有一個 createUser 的方法,傳入的 UserInfo 需要包含用戶的 password ,怎么辦?在設計層面,展示層向服務層傳遞的 DTO 與服務層返回給展示層的 DTO 在概念上是不同的,但在實現層面,我們通常很少會這樣做(定義兩個 UserInfo ,甚至更多),因為這樣做并不見得很明智,我們完全可以設計一個完全兼容的 DTO ,在服務層接收數據的時候,不該由展示層設置的屬性(如訂單的總價應該由其單價、數量、折扣等決定),無論展示層是否設置,服務層都一概忽略,而在服務層返回數據時,不該返回的數據(如用戶密碼),就不設置對應的屬性。
對于 DO 來說,還有一點需要說明:為什么不在服務層中直接返回 DO 呢?這樣可以省去 DTO 的編碼和轉換工作,原因如下:
l 兩者在本質上的區別可能導致彼此并不一一對應,一個 DTO 可能對應多個 DO ,反之亦然,甚至兩者存在多對多的關系。
l DO 具有一些不應該讓展示層知道的數據
l DO 具有業務方法,如果直接把 DO 傳遞給展示層,展示層的代碼就可以繞過服務層直接調用它不應該訪問的操作,對于基于 AOP 攔截服務層來進行訪問控制的機制來說,這問題尤為突出,而在展示層調用 DO 的業務方法也會因為事務的問題,讓事務難以控制。
l 對于某些 ORM 框架(如 Hibernate )來說,通常會使用“延遲加載”技術,如果直接把 DO 暴露給展示層,對于大部分情況,展示層不在事務范圍之內( Open session in view 在大部分情況下不是一種值得推崇的設計),如果其嘗試在 Session 關閉的情況下獲取一個未加載的關聯對象,會出現運行時異常(對于 Hibernate 來說,就是 LazyInitiliaztionException )。
l 從設計層面來說,展示層依賴于服務層,服務層依賴于領域層,如果把 DO 暴露出去,就會導致展示層直接依賴于領域層,這雖然依然是單向依賴,但這種跨層依賴會導致不必要的耦合。
對于 DTO 來說,也有一點必須進行說明,就是 DTO 應該是一個“扁平的二維對象”,舉個例子來說明:如果 User 會關聯若干個其他實體(例如 Address 、 Account 、 Region 等),那么 getUser() 返回的 UserInfo ,是否就需要把其關聯的對象的 DTO 都一并返回呢?如果這樣的話,必然導致數據傳輸量的大增,對于分布式應用來說,由于涉及數據在網絡上的傳輸、序列化和反序列化,這種設計更不可接受。如果 getUser 除了要返回 User 的基本信息外,還需要返回一個 AccountId 、 AccountName 、 RegionId 、 RegionName ,那么,請把這些屬性定義到 UserInfo 中,把一個“立體”的對象樹“壓扁”成一個“扁平的二維對象”,筆者目前參與的項目是一個分布式系統,該系統不管三七二十一,把一個對象的所有關聯對象都轉換為相同結構的 DTO 對象樹并返回,導致性能非常的慢。
DO 與 PO 的區別
DO 和 PO 在絕大部分情況下是一一對應的, PO 是只含有 get/set 方法的 POJO ,但某些場景還是能反映出兩者在概念上存在本質的區別:
l DO 在某些場景下不需要進行顯式的持久化,例如利用策略模式設計的商品折扣策略,會衍生出折扣策略的接口和不同折扣策略實現類,這些折扣策略實現類可以算是 DO ,但它們只駐留在靜態內存,不需要持久化到持久層,因此,這類 DO 是不存在對應的 PO 的。
l 同樣的道理,某些場景下, PO 也沒有對應的 DO ,例如老師 Teacher 和學生 Student 存在多對多的關系,在關系數據庫中,這種關系需要表現為一個中間表,也就對應有一個 TeacherAndStudentPO 的 PO ,但這個 PO 在業務領域沒有任何現實的意義,它完全不能與任何 DO 對應上。這里要特別聲明,并不是所有多對多關系都沒有業務含義,這跟具體業務場景有關,例如:兩個 PO 之間的關系會影響具體業務,并且這種關系存在多種類型,那么這種多對多關系也應該表現為一個 DO ,又如:“角色”與“資源”之間存在多對多關系,而這種關系很明顯會表現為一個 DO ——“權限”。
l 某些情況下,為了某種持久化策略或者性能的考慮,一個 PO 可能對應多個 DO ,反之亦然。例如客戶 Customer 有其聯系信息 Contacts ,這里是兩個一對一關系的 DO ,但可能出于性能的考慮(極端情況,權作舉例),為了減少數據庫的連接查詢操作,把 Customer 和 Contacts 兩個 DO 數據合并到一張數據表中。反過來,如果一本圖書 Book ,有一個屬性是封面 cover ,但該屬性是一副圖片的二進制數據,而某些查詢操作不希望把 cover 一并加載,從而減輕磁盤 IO 開銷,同時假設 ORM 框架不支持屬性級別的延遲加載,那么就需要考慮把 cover 獨立到一張數據表中去,這樣就形成一個 DO 對應對個 PO 的情況。
l PO 的某些屬性值對于 DO 沒有任何意義,這些屬性值可能是為了解決某些持久化策略而存在的數據,例如為了實現“樂觀鎖”, PO 存在一個 version 的屬性,這個 version 對于 DO 來說是沒有任何業務意義的,它不應該在 DO 中存在。同理, DO 中也可能存在不需要持久化的屬性。
DO 與 PO 的應用
由于 ORM 框架的功能非常強大而大行其道,而且 JavaEE 也推出了 JPA 規范,現在的業務應用開發,基本上不需要區分 DO 與 PO , PO 完全可以通過 JPA , Hibernate Annotations/hbm 隱藏在 DO 之中。雖然如此,但有些問題我們還必須注意:
l 對于 DO 中不需要持久化的屬性,需要通過 ORM 顯式的聲明,如:在 JPA 中,可以利用 @Transient 聲明。
l 對于 PO 中為了某種持久化策略而存在的屬性,例如 version ,由于 DO 、 PO 合并了,必須在 DO 中聲明,但由于這個屬性對 DO 是沒有任何業務意義的,需要讓該屬性對外隱藏起來,最常見的做法是把該屬性的 get/set 方法私有化,甚至不提供 get/set 方法,但對于 Hibernate 來說,這需要特別注意,由于 Hibernate 從數據庫讀取數據轉換為 DO 時,是利用反射機制先調用 DO 的空參數構造函數構造 DO 實例,然后再利用 JavaBean 的規范反射出 set 方法來為每個屬性設值,如果不顯式聲明 set 方法,或把 set 方法設置為 private ,都會導致 Hibernate 無法初始化 DO ,從而出現運行時異常,可行的做法是把屬性的 set 方法設置為 protected 。
l 對于一個 DO 對應多個 PO ,或者一個 PO 對應多個 DO 的場景,以及屬性級別的延遲加載, Hibernate 都提供了很好的支持,請參考 Hibnate 的相關資料。
到目前為止,相信大家都已經比較清晰的了解 VO 、 DTO 、 DO 、 PO 的概念、區別和實際應用了。通過上面的詳細分析,我們還可以總結出一個原則:分析設計層面和實現層面完全是兩個獨立的層面,即使實現層面通過某種技術手段可以把兩個完全獨立的概念合二為一,在分析設計層面,我們仍然(至少在頭腦中)需要把概念上獨立的東西清晰的區分開來,這個原則對于做好分析設計非常重要(工具越先進,往往會讓我們越麻木)。第一篇系列博文拋磚引玉,大唱領域驅動設計的優勢,但其實領域驅動設計在現實環境中還是有種種的限制,需要選擇性的使用,正如我在《田七的智慧》博文中提到,我們不能永遠的理想化的去選擇所謂“最好的設計”,在必要的情況下,我們還是要敢于放棄,因為最合適的設計才是最好的設計。本來,系列中的第二篇博文應該是討論領取驅動設計的限制和如何選擇性的使用,但請原諒我的疏忽,下一篇系列博文會把這個主題補上,敬請關注