分類: JavaSE經(jīng)典文章
概念:
VO(View Object):視圖對象翎朱,用于展示層,它的作用是把某個(gè)指定頁面(或組件)的所有數(shù)據(jù)封裝起來尺铣。
DTO(Data Transfer Object):數(shù)據(jù)傳輸對象拴曲,這個(gè)概念來源于J2EE的設(shè)計(jì)模式,原來的目的是為了EJB的分布式應(yīng)用提供粗粒度的數(shù)據(jù)實(shí)體凛忿,以減少分布式調(diào)用的次數(shù)澈灼,從而提高分布式調(diào)用的性能和降低網(wǎng)絡(luò)負(fù)載,但在這里,我泛指用于展示層與服務(wù)層之間的數(shù)據(jù)傳輸對象叁熔。
DO(Domain Object):領(lǐng)域?qū)ο笪冢褪菑默F(xiàn)實(shí)世界中抽象出來的有形或無形的業(yè)務(wù)實(shí)體。
PO(Persistent Object):持久化對象荣回,它跟持久層(通常是關(guān)系型數(shù)據(jù)庫)的數(shù)據(jù)結(jié)構(gòu)形成一一對應(yīng)的映射關(guān)系遭贸,如果持久層是關(guān)系型數(shù)據(jù)庫,那么心软,數(shù)據(jù)表中的每個(gè)字段(或若干個(gè))就對應(yīng)PO的一個(gè)(或若干個(gè))屬性壕吹。
VO與DTO的區(qū)別
? ? ? 大家可能會有個(gè)疑問(在筆者參與的項(xiàng)目中,很多程序員也有相同的疑惑):既然DTO是展示層與服務(wù)層之間傳遞數(shù)據(jù)的對象糯累,為什么還需要一個(gè)VO呢算利?對!對于絕大部分的應(yīng)用場景來說泳姐,DTO和VO的屬性值基本是一致的效拭,而且他們通常都是POJO,因此沒必要多此一舉胖秒,但不要忘記這是實(shí)現(xiàn)層面的思維缎患,對于設(shè)計(jì)層面來說,概念上還是應(yīng)該存在VO和DTO阎肝,因?yàn)閮烧哂兄举|(zhì)的區(qū)別挤渔,DTO代表服務(wù)層需要接收的數(shù)據(jù)和返回的數(shù)據(jù),而VO代表展示層需要顯示的數(shù)據(jù)风题。
? ? ? 用一個(gè)例子來說明可能會比較容易理解:例如服務(wù)層有一個(gè)getUser的方法返回一個(gè)系統(tǒng)用戶判导,其中有一個(gè)屬性是gender(性別),對于服務(wù)層來說沛硅,它只從語義上定義:1-男性眼刃,2-女性,0-未指定摇肌,而對于展示層來說擂红,它可能需要用“帥哥”代表男性,用“美女”代表女性围小,用“秘密”代表未指定昵骤。說到這里,可能你還會反駁肯适,在服務(wù)層直接就返回“帥哥美女”不就行了嗎变秦?對于大部分應(yīng)用來說,這不是問題框舔,但設(shè)想一下伴栓,如果需求允許客戶可以定制風(fēng)格,而不同風(fēng)格對于“性別”的表現(xiàn)方式不一樣,又或者這個(gè)服務(wù)同時(shí)供多個(gè)客戶端使用(不同門戶)钳垮,而不同的客戶端對于表現(xiàn)層的要求有所不同惑淳,那么,問題就來了饺窿。再者歧焦,回到設(shè)計(jì)層面上分析,從職責(zé)單一原則來看肚医,服務(wù)層只負(fù)責(zé)業(yè)務(wù)绢馍,與具體的表現(xiàn)形式無關(guān),因此肠套,它返回的DTO舰涌,不應(yīng)該出現(xiàn)與表現(xiàn)形式的耦合。
? ? ? 理論歸理論你稚,這到底還是分析設(shè)計(jì)層面的思維瓷耙,是否在實(shí)現(xiàn)層面必須這樣做呢?一刀切的做法往往會得不償失刁赖,下面我馬上會分析應(yīng)用中如何做出正確的選擇搁痛。
VO與DTO的應(yīng)用
? ? ? 上面只是用了一個(gè)簡單的例子來說明VO與DTO在概念上的區(qū)別,本節(jié)將會告訴你如何在應(yīng)用中做出正確的選擇宇弛。
? ? ? 在以下才場景中鸡典,我們可以考慮把VO與DTO二合為一(注意:是實(shí)現(xiàn)層面):
當(dāng)需求非常清晰穩(wěn)定,而且客戶端很明確只有一個(gè)的時(shí)候枪芒,沒有必要把VO和DTO區(qū)分開來彻况,這時(shí)候VO可以退隱,用一個(gè)DTO即可舅踪,為什么是VO退隱而不是DTO纽甘?回到設(shè)計(jì)層面,服務(wù)層的職責(zé)依然不應(yīng)該與展示層耦合硫朦,所以贷腕,對于前面的例子背镇,你很容易理解咬展,DTO對于“性別”來說,依然不能用“帥哥美女”瞒斩,這個(gè)轉(zhuǎn)換應(yīng)該依賴于頁面的腳本(如JavaScript)或其他機(jī)制(JSTL破婆、EL、CSS)
即使客戶端可以進(jìn)行定制胸囱,或者存在多個(gè)不同的客戶端祷舀,如果客戶端能夠用某種技術(shù)(腳本或其他機(jī)制)實(shí)現(xiàn)轉(zhuǎn)換,同樣可以讓VO退隱
以下場景需要優(yōu)先考慮VO、DTO并存:
上述場景的反面場景
因?yàn)槟撤N技術(shù)原因裳扯,比如某個(gè)框架(如Flex)提供自動把POJO轉(zhuǎn)換為UI中某些Field時(shí)抛丽,可以考慮在實(shí)現(xiàn)層面定義出VO,這個(gè)權(quán)衡完全取決于使用框架的自動轉(zhuǎn)換能力帶來的開發(fā)和維護(hù)效率提升與設(shè)計(jì)多一個(gè)VO所多做的事情帶來的開發(fā)和維護(hù)效率的下降之間的比對饰豺。
如果頁面出現(xiàn)一個(gè)“大視圖”亿鲜,而組成這個(gè)大視圖的所有數(shù)據(jù)需要調(diào)用多個(gè)服務(wù),返回多個(gè)DTO來組裝(當(dāng)然冤吨,這同樣可以通過服務(wù)層提供一次性返回一個(gè)大視圖的DTO來取代蒿柳,但在服務(wù)層提供一個(gè)這樣的方法是否合適,需要在設(shè)計(jì)層面進(jìn)行權(quán)衡)漩蟆。
DTO與DO的區(qū)別
? ? ? 首先是概念上的區(qū)別垒探,DTO是展示層和服務(wù)層之間的數(shù)據(jù)傳輸對象(可以認(rèn)為是兩者之間的協(xié)議),而DO是對現(xiàn)實(shí)世界各種業(yè)務(wù)角色的抽象怠李,這就引出了兩者在數(shù)據(jù)上的區(qū)別圾叼,例如UserInfo和User(對于DTO和DO的命名規(guī)則,請參見筆者前面的一篇博文)扔仓,對于一個(gè)getUser方法來說褐奥,本質(zhì)上它永遠(yuǎn)不應(yīng)該返回用戶的密碼,因此UserInfo至少比User少一個(gè)password的數(shù)據(jù)翘簇。而在領(lǐng)域驅(qū)動設(shè)計(jì)中撬码,正如第一篇系列文章所說,DO不是簡單的POJO版保,它具有領(lǐng)域業(yè)務(wù)邏輯呜笑。
DTO與DO的應(yīng)用
? ? ? 從上一節(jié)的例子中,細(xì)心的讀者可能會發(fā)現(xiàn)問題:既然getUser方法返回的UserInfo不應(yīng)該包含password彻犁,那么就不應(yīng)該存在password這個(gè)屬性定義叫胁,但如果同時(shí)有一個(gè)createUser的方法,傳入的UserInfo需要包含用戶的password汞幢,怎么辦驼鹅?在設(shè)計(jì)層面,展示層向服務(wù)層傳遞的DTO與服務(wù)層返回給展示層的DTO在概念上是不同的森篷,但在實(shí)現(xiàn)層面输钩,我們通常很少會這樣做(定義兩個(gè)UserInfo,甚至更多)仲智,因?yàn)檫@樣做并不見得很明智买乃,我們完全可以設(shè)計(jì)一個(gè)完全兼容的DTO,在服務(wù)層接收數(shù)據(jù)的時(shí)候钓辆,不該由展示層設(shè)置的屬性(如訂單的總價(jià)應(yīng)該由其單價(jià)剪验、數(shù)量肴焊、折扣等決定),無論展示層是否設(shè)置功戚,服務(wù)層都一概忽略娶眷,而在服務(wù)層返回?cái)?shù)據(jù)時(shí),不該返回的數(shù)據(jù)(如用戶密碼)啸臀,就不設(shè)置對應(yīng)的屬性茂浮。
? ? ? 對于DO來說,還有一點(diǎn)需要說明:為什么不在服務(wù)層中直接返回DO呢壳咕?這樣可以省去DTO的編碼和轉(zhuǎn)換工作席揽,原因如下:
兩者在本質(zhì)上的區(qū)別可能導(dǎo)致彼此并不一一對應(yīng),一個(gè)DTO可能對應(yīng)多個(gè)DO谓厘,反之亦然幌羞,甚至兩者存在多對多的關(guān)系。
DO具有一些不應(yīng)該讓展示層知道的數(shù)據(jù)
DO具有業(yè)務(wù)方法竟稳,如果直接把DO傳遞給展示層属桦,展示層的代碼就可以繞過服務(wù)層直接調(diào)用它不應(yīng)該訪問的操作,對于基于AOP攔截服務(wù)層來進(jìn)行訪問控制的機(jī)制來說他爸,這問題尤為突出聂宾,而在展示層調(diào)用DO的業(yè)務(wù)方法也會因?yàn)槭聞?wù)的問題,讓事務(wù)難以控制诊笤。
對于某些ORM框架(如Hibernate)來說系谐,通常會使用“延遲加載”技術(shù),如果直接把DO暴露給展示層讨跟,對于大部分情況纪他,展示層不在事務(wù)范圍之內(nèi)(Open session in view在大部分情況下不是一種值得推崇的設(shè)計(jì)),如果其嘗試在Session關(guān)閉的情況下獲取一個(gè)未加載的關(guān)聯(lián)對象晾匠,會出現(xiàn)運(yùn)行時(shí)異常(對于Hibernate來說茶袒,就是LazyInitiliaztionException)。
從設(shè)計(jì)層面來說凉馆,展示層依賴于服務(wù)層薪寓,服務(wù)層依賴于領(lǐng)域?qū)樱绻袲O暴露出去澜共,就會導(dǎo)致展示層直接依賴于領(lǐng)域?qū)酉虿妫@雖然依然是單向依賴,但這種跨層依賴會導(dǎo)致不必要的耦合咳胃。
對于DTO來說植康,也有一點(diǎn)必須進(jìn)行說明旷太,就是DTO應(yīng)該是一個(gè)“扁平的二維對象”展懈,舉個(gè)例子來說明:如果User會關(guān)聯(lián)若干個(gè)其他實(shí)體(例如Address销睁、Account、Region等)存崖,那么getUser()返回的UserInfo冻记,是否就需要把其關(guān)聯(lián)的對象的DTO都一并返回呢?如果這樣的話来惧,必然導(dǎo)致數(shù)據(jù)傳輸量的大增冗栗,對于分布式應(yīng)用來說,由于涉及數(shù)據(jù)在網(wǎng)絡(luò)上的傳輸供搀、序列化和反序列化隅居,這種設(shè)計(jì)更不可接受。如果getUser除了要返回User的基本信息外葛虐,還需要返回一個(gè)AccountId胎源、AccountName、RegionId屿脐、RegionName涕蚤,那么,請把這些屬性定義到UserInfo中的诵,把一個(gè)“立體”的對象樹“壓扁”成一個(gè)“扁平的二維對象”万栅,筆者目前參與的項(xiàng)目是一個(gè)分布式系統(tǒng),該系統(tǒng)不管三七二十一西疤,把一個(gè)對象的所有關(guān)聯(lián)對象都轉(zhuǎn)換為相同結(jié)構(gòu)的DTO對象樹并返回烦粒,導(dǎo)致性能非常的慢。
DO與PO的區(qū)別
? ? ? DO和PO在絕大部分情況下是一一對應(yīng)的代赁,PO是只含有g(shù)et/set方法的POJO撒遣,但某些場景還是能反映出兩者在概念上存在本質(zhì)的區(qū)別:
DO在某些場景下不需要進(jìn)行顯式的持久化,例如利用策略模式設(shè)計(jì)的商品折扣策略管跺,會衍生出折扣策略的接口和不同折扣策略實(shí)現(xiàn)類义黎,這些折扣策略實(shí)現(xiàn)類可以算是DO,但它們只駐留在靜態(tài)內(nèi)存豁跑,不需要持久化到持久層廉涕,因此,這類DO是不存在對應(yīng)的PO的艇拍。
同樣的道理狐蜕,某些場景下,PO也沒有對應(yīng)的DO卸夕,例如老師Teacher和學(xué)生Student存在多對多的關(guān)系层释,在關(guān)系數(shù)據(jù)庫中,這種關(guān)系需要表現(xiàn)為一個(gè)中間表快集,也就對應(yīng)有一個(gè)TeacherAndStudentPO的PO贡羔,但這個(gè)PO在業(yè)務(wù)領(lǐng)域沒有任何現(xiàn)實(shí)的意義廉白,它完全不能與任何DO對應(yīng)上。這里要特別聲明乖寒,并不是所有多對多關(guān)系都沒有業(yè)務(wù)含義猴蹂,這跟具體業(yè)務(wù)場景有關(guān),例如:兩個(gè)PO之間的關(guān)系會影響具體業(yè)務(wù)楣嘁,并且這種關(guān)系存在多種類型磅轻,那么這種多對多關(guān)系也應(yīng)該表現(xiàn)為一個(gè)DO,又如:“角色”與“資源”之間存在多對多關(guān)系逐虚,而這種關(guān)系很明顯會表現(xiàn)為一個(gè)DO——“權(quán)限”聋溜。
某些情況下,為了某種持久化策略或者性能的考慮叭爱,一個(gè)PO可能對應(yīng)多個(gè)DO勤婚,反之亦然。例如客戶Customer有其聯(lián)系信息Contacts涤伐,這里是兩個(gè)一對一關(guān)系的DO馒胆,但可能出于性能的考慮(極端情況,權(quán)作舉例)凝果,為了減少數(shù)據(jù)庫的連接查詢操作祝迂,把Customer和Contacts兩個(gè)DO數(shù)據(jù)合并到一張數(shù)據(jù)表中。反過來器净,如果一本圖書Book型雳,有一個(gè)屬性是封面cover,但該屬性是一副圖片的二進(jìn)制數(shù)據(jù)山害,而某些查詢操作不希望把cover一并加載纠俭,從而減輕磁盤IO開銷,同時(shí)假設(shè)ORM框架不支持屬性級別的延遲加載浪慌,那么就需要考慮把cover獨(dú)立到一張數(shù)據(jù)表中去冤荆,這樣就形成一個(gè)DO對應(yīng)對個(gè)PO的情況。
PO的某些屬性值對于DO沒有任何意義权纤,這些屬性值可能是為了解決某些持久化策略而存在的數(shù)據(jù)钓简,例如為了實(shí)現(xiàn)“樂觀鎖”,PO存在一個(gè)version的屬性汹想,這個(gè)version對于DO來說是沒有任何業(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完全可以通過JPA,Hibernate Annotations/hbm隱藏在DO之中豪诲。雖然如此,但有些問題我們還必須注意:
對于DO中不需要持久化的屬性挂绰,需要通過ORM顯式的聲明屎篱,如:在JPA中,可以利用@Transient聲明葵蒂。
對于PO中為了某種持久化策略而存在的屬性交播,例如version,由于DO践付、PO合并了秦士,必須在DO中聲明,但由于這個(gè)屬性對DO是沒有任何業(yè)務(wù)意義的永高,需要讓該屬性對外隱藏起來隧土,最常見的做法是把該屬性的get/set方法私有化,甚至不提供get/set方法命爬,但對于Hibernate來說曹傀,這需要特別注意,由于Hibernate從數(shù)據(jù)庫讀取數(shù)據(jù)轉(zhuǎn)換為DO時(shí)饲宛,是利用反射機(jī)制先調(diào)用DO的空參數(shù)構(gòu)造函數(shù)構(gòu)造DO實(shí)例皆愉,然后再利用JavaBean的規(guī)范反射出set方法來為每個(gè)屬性設(shè)值,如果不顯式聲明set方法艇抠,或把set方法設(shè)置為private幕庐,都會導(dǎo)致Hibernate無法初始化DO,從而出現(xiàn)運(yùn)行時(shí)異常家淤,可行的做法是把屬性的set方法設(shè)置為protected异剥。
對于一個(gè)DO對應(yīng)多個(gè)PO,或者一個(gè)PO對應(yīng)多個(gè)DO的場景絮重,以及屬性級別的延遲加載届吁,Hibernate都提供了很好的支持,請參考Hibnate的相關(guān)資料绿鸣。