概念:程序員
VO(View Object):視圖對象,用於展現層,它的做用是把某個指定頁面(或組件)的全部數據封裝起來。數據庫
DTO(Data Transfer Object):數據傳輸對象,這個概念來源於J2EE的設計模式,原來的目的是爲了EJB的分佈式應用提供粗粒度的數據實體,以減小分佈式調用的次數,從而提升分佈式調用的性能和下降網絡負載,但在這裏,我泛指用於展現層與服務層之間的數據傳輸對象。設計模式
DO(Domain Object):領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。網絡
PO(Persistent Object):持久化對象,它跟持久層(一般是關係型數據庫)的數據結構造成一一對應的映射關係,若是持久層是關係型數據庫,那麼,數據表中的每一個字段(或若干個)就對應PO的一個(或若干個)屬性。session
模型:數據結構
下面以一個時序圖創建簡單模型來描述上述對象在三層架構應用中的位置
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的概念、區別和實際應用了。經過上面的詳細分析,咱們還能夠總結出一個原則:分析設計層面和實現層面徹底是兩個獨立的層面,即便實現層面經過某種技術手段能夠把兩個徹底獨立的概念合二爲一,在分析設計層面,咱們仍然(至少在頭腦中)須要把概念上獨立的東西清晰的區分開來,這個原則對於作好分析設計很是重要(工具越先進,每每會讓咱們越麻木)。第一篇系列博文拋磚引玉,大唱領域驅動設計的優點,但其實領域驅動設計在現實環境中仍是有種種的限制,須要選擇性的使用,正如我在《田七的智慧》博文中提到,咱們不能永遠的理想化的去選擇所謂「最好的設計」,在必要的狀況下,咱們仍是要勇於放棄,由於最合適的設計纔是最好的設計。