該文章在抄自:鏈接
VO(View Object):視圖層或粮,用于展示層,它的作用是把某個(gè)指定頁(yè)面(或組件)的所有數(shù)據(jù)封裝起來(lái)朵锣。
DTO(Data Transfer Object):數(shù)據(jù)傳輸對(duì)象谬盐,這個(gè)概念來(lái)源于J2EE的設(shè)計(jì)模式,原來(lái)的目的時(shí)為了EJB的分布式應(yīng)用提供粗粒度的數(shù)據(jù)實(shí)體诚些,以減少分布式條用的次數(shù)飞傀,從而提高分布式調(diào)用的性能和降低網(wǎng)絡(luò)負(fù)載,但在這里诬烹,我泛指用于展示層與服務(wù)層之間的數(shù)據(jù)傳輸對(duì)象砸烦。
DO(Domain Object):領(lǐng)域?qū)ο螅褪菑默F(xiàn)實(shí)世界中抽象出來(lái)的有形或無(wú)形的業(yè)務(wù)實(shí)體绞吁。
PO(Persistent Object):持久化對(duì)象幢痘,他跟持久層(通常是關(guān)系型數(shù)據(jù)庫(kù))的數(shù)據(jù)結(jié)構(gòu)形成一一對(duì)應(yīng)的映射關(guān)系,如果持久層是關(guān)系型數(shù)據(jù)庫(kù)家破,那么颜说,數(shù)據(jù)表中的每個(gè)字段(或若干個(gè))就對(duì)應(yīng)PO的一個(gè)(或若干個(gè))屬性购岗。
模型:
下面以一個(gè)時(shí)序圖建立簡(jiǎn)單模型來(lái)描述上述對(duì)象在三層架構(gòu)應(yīng)用中的位置
- 用戶發(fā)出請(qǐng)求(可能是填寫表單),表單的數(shù)據(jù)在展示層被匹配為VO门粪。
- 展示層把VO轉(zhuǎn)換為服務(wù)層對(duì)應(yīng)方法所要求的DTO藕畔,傳輸給服務(wù)層。
- 服務(wù)層首先根據(jù)DTO的數(shù)據(jù)構(gòu)造(或重建)一個(gè)DO庄拇,調(diào)用DO的業(yè)務(wù)方法完成具體業(yè)務(wù)注服。
- 服務(wù)層把DO轉(zhuǎn)換為持久層對(duì)應(yīng)的PO(可以使用ORM工具,也可以不用)措近,調(diào)用持久層的持久化方法溶弟,把PO傳遞給它,完成持久化操作瞭郑。
- 對(duì)于一個(gè)逆向操作辜御,如讀取數(shù)據(jù),也是用類似的方式轉(zhuǎn)換和傳遞屈张,略擒权。
VO與DTO的區(qū)別
1.大家可能會(huì)有個(gè)疑問(wèn)(在筆者參與的項(xiàng)目中,很多程序員也有相同的疑惑):既然DTO是展示層與服務(wù)層之間傳遞數(shù)據(jù)的對(duì)象阁谆,為什么還需要一個(gè)VO呢碳抄?對(duì)!對(duì)于絕大部分的應(yīng)用場(chǎng)景來(lái)說(shuō)场绿,DTO和VO的屬性值基本是一致的剖效,而且他們通常都是POJO,因此沒(méi)必要多此一舉焰盗,但不要忘記這是實(shí)現(xiàn)層面的思維璧尸,對(duì)于設(shè)計(jì)層面來(lái)說(shuō),概念上還是應(yīng)該存在VO和DTO熬拒,因?yàn)閮烧哂兄举|(zhì)的區(qū)別爷光,DTO代表服務(wù)層需要接收的數(shù)據(jù)和返回的數(shù)據(jù),而VO代表展示層需要顯示的數(shù)據(jù)澎粟。
2.用一個(gè)例子來(lái)說(shuō)明可能會(huì)比較容易理解:例如服務(wù)層有一個(gè)getUser的方法返回一個(gè)系統(tǒng)用戶蛀序,其中有一個(gè)屬性是gender(性別),對(duì)于服務(wù)層來(lái)說(shuō)捌议,它只從語(yǔ)義上定義:1-男性哼拔,2-女性引有,0-未指定瓣颅,而對(duì)于展示層來(lái)說(shuō),它可能需要用“帥哥”代表男性譬正,用“美女”代表女性宫补,用“秘密”代表未指定檬姥。說(shuō)到這里,可能你還會(huì)反駁粉怕,在服務(wù)層直接就返回“帥哥美女”不就行了嗎健民?對(duì)于大部分應(yīng)用來(lái)說(shuō),這不是問(wèn)題贫贝,但設(shè)想一下秉犹,如果需求允許客戶可以定制風(fēng)格,而不同風(fēng)格對(duì)于“性別”的表現(xiàn)方式不一樣稚晚,又或者這個(gè)服務(wù)同時(shí)供多個(gè)客戶端使用(不同門戶)崇堵,而不同的客戶端對(duì)于表現(xiàn)層的要求有所不同,那么客燕,問(wèn)題就來(lái)了鸳劳。再者,回到設(shè)計(jì)層面上分析也搓,從職責(zé)單一原則來(lái)看赏廓,服務(wù)層只負(fù)責(zé)業(yè)務(wù),與具體的表現(xiàn)形式無(wú)關(guān)傍妒,因此幔摸,它返回的DTO,不應(yīng)該出現(xiàn)與表現(xiàn)形式的耦合颤练。
3.理論歸理論抚太,這到底還是分析設(shè)計(jì)層面的思維,是否在實(shí)現(xiàn)層面必須這樣做呢昔案?一刀切的做法往往會(huì)得不償失尿贫,下面我馬上會(huì)分析應(yīng)用中如何做出正確的選擇。
VO與DTO的應(yīng)用
上面只是用了一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明VO與DTO在概念上的區(qū)別踏揣,本節(jié)將會(huì)告訴你如何在應(yīng)用中做出正確的選擇庆亡。
在以下才場(chǎng)景中,我們可以考慮把VO與DTO二合為一(注意:是實(shí)現(xiàn)層面):
- 當(dāng)需求非常清晰穩(wěn)定捞稿,而且客戶端很明確只有一個(gè)的時(shí)候又谋,沒(méi)有必要把VO和DTO區(qū)分開來(lái),這時(shí)候VO可以退隱娱局,用一個(gè)DTO即可彰亥,為什么是VO退隱而不是DTO?回到設(shè)計(jì)層面衰齐,服務(wù)層的職責(zé)依然不因該與展示層耦合任斋,所以,對(duì)于前面的例子耻涛,你很容易理解废酷,DTO對(duì)于“性別”來(lái)說(shuō)瘟檩,依然不能用“帥哥美女”,這個(gè)轉(zhuǎn)換應(yīng)該依賴與頁(yè)面的腳本(如javaScript)或其他機(jī)制(JSTL,EL,CSS)澈蟆。
- 即使客戶端可以進(jìn)行定制墨辛,或者存在多個(gè)不同的客戶端,如果客戶端能夠用某種技術(shù)(腳本或其他機(jī)制)實(shí)現(xiàn)轉(zhuǎn)換趴俘,同樣可以讓VO隱退睹簇。
以下場(chǎng)景需要優(yōu)先考慮VO,DTO并存:
- 上述場(chǎng)景的反面場(chǎng)景
- 因?yàn)槟撤N技術(shù)原因,比如某個(gè)框架(如Flex)提供自動(dòng)把POJO轉(zhuǎn)換為UI中某些FieId時(shí)寥闪,可以考慮在實(shí)現(xiàn)層面定義出VO带膀,這個(gè)這個(gè)權(quán)衡完全取決于使用框架的自動(dòng)轉(zhuǎn)換能力帶來(lái)的開發(fā)和維護(hù)效率提升與設(shè)計(jì)多一個(gè)VO所多做的事情帶來(lái)的開發(fā)和維護(hù)效率的下降之間的對(duì)比。
- 如果頁(yè)面出現(xiàn)一個(gè)“大視圖”橙垢,而組成這個(gè)大視圖的所有數(shù)據(jù)需要調(diào)用多個(gè)服務(wù)垛叨,返回多個(gè)DTO來(lái)組裝(當(dāng)然,這同樣可以通過(guò)服務(wù)層提供一次性返回一個(gè)大視圖的DTO來(lái)取代柜某,但在服務(wù)層提供一個(gè)這樣的方法是否合適嗽元,需要在設(shè)計(jì)層面進(jìn)行權(quán)衡)。
DTO與DO的區(qū)別
首先是概念上的區(qū)別喂击,DTO是展示層和服務(wù)層之間的數(shù)據(jù)傳輸對(duì)象(可以認(rèn)為是兩者之間的協(xié)議)剂癌,而DO是對(duì)現(xiàn)實(shí)世界各種業(yè)務(wù)角色的抽象,這就引出了兩者在數(shù)據(jù)上的區(qū)別翰绊,例如UserInfo和User(對(duì)于DTO和DO的命名規(guī)則佩谷,請(qǐng)參見筆者前面的一篇博文),對(duì)于一個(gè)getUser方法來(lái)說(shuō)监嗜,本質(zhì)上它永遠(yuǎn)不應(yīng)該返回用戶的密碼谐檀,因此UserInfo至少比User少一個(gè)password的數(shù)據(jù)。而在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)中裁奇,正如第一篇系列文章所說(shuō)桐猬,DO不是簡(jiǎn)單的POJO,它具有領(lǐng)域業(yè)務(wù)邏輯刽肠。
DTO與DO的應(yīng)用
1.從上一節(jié)的例子中溃肪,細(xì)心的讀者可能會(huì)發(fā)現(xiàn)問(wèn)題:既然getUser方法返回的UserInfo不應(yīng)該包含password,那么就不應(yīng)該存在password這個(gè)屬性定義音五,但如果同時(shí)有一個(gè)createUser的方法惫撰,傳入的UserInfo需要包含用戶的password,怎么辦躺涝?在設(shè)計(jì)層面厨钻,展示層向服務(wù)層傳遞的DTO與服務(wù)層返回給展示層的DTO在概念上是不同的,但在實(shí)現(xiàn)層面,我們通常很少會(huì)這樣做(定義兩個(gè)UserInfo莉撇,甚至更多)呢蛤,因?yàn)檫@樣做并不見得很明智惶傻,我們完全可以設(shè)計(jì)一個(gè)完全兼容的DTO棍郎,在服務(wù)層接收數(shù)據(jù)的時(shí)候,不該由展示層設(shè)置的屬性(如訂單的總價(jià)應(yīng)該由其單價(jià)银室、數(shù)量涂佃、折扣等決定),無(wú)論展示層是否設(shè)置蜈敢,服務(wù)層都一概忽略辜荠,而在服務(wù)層返回?cái)?shù)據(jù)時(shí),不該返回的數(shù)據(jù)(如用戶密碼)抓狭,就不設(shè)置對(duì)應(yīng)的屬性伯病。
2.對(duì)于DO來(lái)說(shuō),還有一點(diǎn)需要說(shuō)明:為什么不在服務(wù)層中直接返回DO呢否过?這樣可以省去DTO的編碼和轉(zhuǎn)換工作午笛,原因如下:
- 兩者在本質(zhì)上的區(qū)別可能導(dǎo)致彼此并不一一對(duì)應(yīng),一個(gè)DTO可能對(duì)應(yīng)多個(gè)DO苗桂,反之亦然药磺,甚至兩者存在多對(duì)多的關(guān)系。
- DO具有一些不應(yīng)該讓展示層知道的數(shù)據(jù)煤伟。
- DO具有業(yè)務(wù)方法癌佩,如果直接把DO傳遞給展示層,展示層的代碼就可以繞過(guò)服務(wù)層直接調(diào)用它不應(yīng)該訪問(wèn)的操作便锨,對(duì)于基于AOP攔截服務(wù)層來(lái)進(jìn)行訪問(wèn)控制的機(jī)制來(lái)說(shuō)围辙,這問(wèn)題尤為突出,而在展示層調(diào)用DO的業(yè)務(wù)方法也會(huì)因?yàn)槭聞?wù)的問(wèn)題放案,讓事務(wù)難以控制酌畜。
- 對(duì)于某些ORM框架(如Hibernate)來(lái)說(shuō),通常會(huì)使用“延遲加載”技術(shù)卿叽,如果直接把DO暴露給展示層桥胞,對(duì)于大部分情況,展示層不在事務(wù)范圍之內(nèi)(Open session in view在大部分情況下不是一種值得推崇的設(shè)計(jì))考婴,如果其嘗試在Session關(guān)閉的情況下獲取一個(gè)未加載的關(guān)聯(lián)對(duì)象贩虾,會(huì)出現(xiàn)運(yùn)行時(shí)異常(對(duì)于Hibernate來(lái)說(shuō),就是LazyInitiliaztionException)沥阱。
- 從設(shè)計(jì)層面來(lái)說(shuō)缎罢,展示層依賴于服務(wù)層,服務(wù)層依賴于領(lǐng)域?qū)樱绻袲O暴露出去策精,就會(huì)導(dǎo)致展示層直接依賴于領(lǐng)域?qū)咏⑹迹@雖然依然是單向依賴,但這種跨層依賴會(huì)導(dǎo)致不必要的耦合咽袜。
對(duì)于DTO來(lái)說(shuō)丸卷,也有一點(diǎn)必須進(jìn)行說(shuō)明,就是DTO應(yīng)該是一個(gè)“扁平的二維對(duì)象”询刹,舉個(gè)例子來(lái)說(shuō)明:如果User會(huì)關(guān)聯(lián)若干個(gè)其他實(shí)體(例如Address谜嫉、Account、Region等)凹联,那么getUser()返回的UserInfo沐兰,是否就需要把其關(guān)聯(lián)的對(duì)象的DTO都一并返回呢?如果這樣的話蔽挠,必然導(dǎo)致數(shù)據(jù)傳輸量的大增住闯,對(duì)于分布式應(yīng)用來(lái)說(shuō),由于涉及數(shù)據(jù)在網(wǎng)絡(luò)上的傳輸澳淑、序列化和反序列化比原,這種設(shè)計(jì)更不可接受。如果getUser除了要返回User的基本信息外偶惠,還需要返回一個(gè)AccountId春寿、AccountName、RegionId忽孽、RegionName绑改,那么,請(qǐng)把這些屬性定義到UserInfo中兄一,把一個(gè)“立體”的對(duì)象樹“壓扁”成一個(gè)“扁平的二維對(duì)象”厘线,筆者目前參與的項(xiàng)目是一個(gè)分布式系統(tǒng),該系統(tǒng)不管三七二十一出革,把一個(gè)對(duì)象的所有關(guān)聯(lián)對(duì)象都轉(zhuǎn)換為相同結(jié)構(gòu)的DTO對(duì)象樹并返回造壮,導(dǎo)致性能非常的慢。
DO與PO的區(qū)別
DO和PO在絕大部分情況下是一一對(duì)應(yīng)的骂束,PO是只含有g(shù)et/set方法的POJO耳璧,但某些場(chǎng)景還是能反映出兩者在概念上存在本質(zhì)的區(qū)別:
- DO在某些場(chǎng)景下不需要進(jìn)行顯式的持久化,例如利用策略模式設(shè)計(jì)的商品折扣策略展箱,會(huì)衍生出折扣策略的接口和不同折扣策略實(shí)現(xiàn)類旨枯,這些折扣策略實(shí)現(xiàn)類可以算是DO,但它們只駐留在靜態(tài)內(nèi)存混驰,不需要持久化到持久層攀隔,因此皂贩,這類DO是不存在對(duì)應(yīng)的PO的。
- 同樣的道理昆汹,某些場(chǎng)景下明刷,PO也沒(méi)有對(duì)應(yīng)的DO,例如老師Teacher和學(xué)生Student存在多對(duì)多的關(guān)系满粗,在關(guān)系數(shù)據(jù)庫(kù)中辈末,這種關(guān)系需要表現(xiàn)為一個(gè)中間表,也就對(duì)應(yīng)有一個(gè)TeacherAndStudentPO的PO败潦,但這個(gè)PO在業(yè)務(wù)領(lǐng)域沒(méi)有任何現(xiàn)實(shí)的意義本冲,它完全不能與任何DO對(duì)應(yīng)上准脂。這里要特別聲明劫扒,并不是所有多對(duì)多關(guān)系都沒(méi)有業(yè)務(wù)含義,這跟具體業(yè)務(wù)場(chǎng)景有關(guān)狸膏,例如:兩個(gè)PO之間的關(guān)系會(huì)影響具體業(yè)務(wù)沟饥,并且這種關(guān)系存在多種類型,那么這種多對(duì)多關(guān)系也應(yīng)該表現(xiàn)為一個(gè)DO湾戳,又如:“角色”與“資源”之間存在多對(duì)多關(guān)系贤旷,而這種關(guān)系很明顯會(huì)表現(xiàn)為一個(gè)DO——“權(quán)限”。
- 某些情況下砾脑,為了某種持久化策略或者性能的考慮幼驶,一個(gè)PO可能對(duì)應(yīng)多個(gè)DO,反之亦然韧衣。例如客戶Customer有其聯(lián)系信息Contacts盅藻,這里是兩個(gè)一對(duì)一關(guān)系的DO,但可能出于性能的考慮(極端情況畅铭,權(quán)作舉例)氏淑,為了減少數(shù)據(jù)庫(kù)的連接查詢操作,把Customer和Contacts兩個(gè)DO數(shù)據(jù)合并到一張數(shù)據(jù)表中硕噩。反過(guò)來(lái)假残,如果一本圖書Book,有一個(gè)屬性是封面cover炉擅,但該屬性是一副圖片的二進(jìn)制數(shù)據(jù)辉懒,而某些查詢操作不希望把cover一并加載,從而減輕磁盤IO開銷谍失,同時(shí)假設(shè)ORM框架不支持屬性級(jí)別的延遲加載眶俩,那么就需要考慮把cover獨(dú)立到一張數(shù)據(jù)表中去,這樣就形成一個(gè)DO對(duì)應(yīng)對(duì)個(gè)PO的情況袱贮。
- PO的某些屬性值對(duì)于DO沒(méi)有任何意義仿便,這些屬性值可能是為了解決某些持久化策略而存在的數(shù)據(jù)体啰,例如為了實(shí)現(xiàn)“樂(lè)觀鎖”,PO存在一個(gè)version的屬性嗽仪,這個(gè)version對(duì)于DO來(lái)說(shuō)是沒(méi)有任何業(yè)務(wù)意義的荒勇,它不應(yīng)該在DO中存在。同理闻坚,DO中也可能存在不需要持久化的屬性沽翔。
DO與PO的應(yīng)用
由于ORM框架的功能非常強(qiáng)大而大行其道,而且JavaEE也推出了JPA規(guī)范窿凤,現(xiàn)在的業(yè)務(wù)應(yīng)用開發(fā)仅偎,基本上不需要區(qū)分DO與PO,PO完全可以通過(guò)JPA雳殊,Hibernate Annotations/hbm隱藏在DO之中橘沥。雖然如此,但有些問(wèn)題我們還必須注意:
- 對(duì)于DO中不需要持久化的屬性夯秃,需要通過(guò)ORM顯式的聲明座咆,如:在JPA中,可以利用@Transient聲明仓洼。
- 對(duì)于PO中為了某種持久化策略而存在的屬性介陶,例如version,由于DO色建、PO合并了哺呜,必須在DO中聲明,但由于這個(gè)屬性對(duì)DO是沒(méi)有任何業(yè)務(wù)意義的箕戳,需要讓該屬性對(duì)外隱藏起來(lái)某残,最常見的做法是把該屬性的get/set方法私有化,甚至不提供get/set方法漂羊,但對(duì)于Hibernate來(lái)說(shuō)驾锰,這需要特別注意,由于Hibernate從數(shù)據(jù)庫(kù)讀取數(shù)據(jù)轉(zhuǎn)換為DO時(shí)走越,是利用反射機(jī)制先調(diào)用DO的空參數(shù)構(gòu)造函數(shù)構(gòu)造DO實(shí)例椭豫,然后再利用JavaBean的規(guī)范反射出set方法來(lái)為每個(gè)屬性設(shè)值,如果不顯式聲明set方法旨指,或把set方法設(shè)置為private赏酥,都會(huì)導(dǎo)致Hibernate無(wú)法初始化DO,從而出現(xiàn)運(yùn)行時(shí)異常谆构,可行的做法是把屬性的set方法設(shè)置為protected裸扶。
- 對(duì)于一個(gè)DO對(duì)應(yīng)多個(gè)PO,或者一個(gè)PO對(duì)應(yīng)多個(gè)DO的場(chǎng)景搬素,以及屬性級(jí)別的延遲加載呵晨,Hibernate都提供了很好的支持魏保,請(qǐng)參考Hibnate的相關(guān)資料。