各種值對象辨析
貧血或失血模型的 Java Web 系統(tǒng), 數(shù)據(jù)與操作是彼此分離的. 操作主要體現(xiàn)在服務(wù)層的接口方法以及實現(xiàn), 而所謂數(shù)據(jù)基本上就是常見的各種 O 了. 如:
- BO Business Object
- PO Persistent Object
- VO View Object
- DTO Data Transfer Object
- POJO Plan Old Java Object
BO 即業(yè)務(wù)對象, 也稱為 Model, 即數(shù)據(jù)模型. PO 為持久化對象, 即 Entity 實體, 是模型在數(shù)據(jù)訪問層的化身, 用于存取關(guān)系或非關(guān)系數(shù)據(jù)庫. VO 是模型在控制層的化身, 通常用于向前端接口或圖形界面?zhèn)鬟f數(shù)據(jù). DTO 是模型在數(shù)據(jù)傳遞時的化身, 主要用于數(shù)據(jù)在不同服務(wù)間的轉(zhuǎn)換和傳遞. POJO 即普通 Java 對象, 原來是針對 J2EE 規(guī)范中各種 Bean, 如 EntityBean, Session Bean 等作區(qū)分, 用來指代非 Bean 的簡單對象. 后來 J2EE 沒落, 在很多文章中也就用來泛指這些值對象了. 除了上述帶 O 的, 還有一些不帶 O 的值對象變種, 例如 xxxQuery, 可能就是加了分頁信息的 VO; 又如 xxxResult, 也許即是封了調(diào)用結(jié)果的 DTO 等等. 凡此種種, 不一而足.
為什么不能只使用 BO, 而非要在不同的層使用不同的化身呢? 一, 是因為雖然大致類似, 但不同的 O 關(guān)注點不同, 其中屬性多少會有些區(qū)別. 數(shù)據(jù)庫中保存的, 控制層可能不需要展示; 控制層展示的, 業(yè)務(wù)邏輯層計算可能又用不到. 二, 是為了架構(gòu)清晰, 便于各層獨立封裝. 如單獨提取控制層, 可以做到不依賴底層 PO, 兩層可獨立變化. 所以開發(fā)時要注意不要一個 Entity 一路從前用到后, 貫穿所有分層. 同理, 盡管屬性相似或雷同, VO 也不應(yīng)繼承 PO, 而是應(yīng)該彼此分開. 實際開發(fā)中, 根據(jù)項目具體需要, 各種值對象一般也不會全部使用, 不過 PO 和 VO 基本上是必不可少的.
值對象比較簡單, 算是基礎(chǔ)中的基礎(chǔ), 會寫 Java 的應(yīng)該沒有人不會寫值對象, 然而要寫得規(guī)范統(tǒng)一則需要自覺和約束. 比如控制層代碼都是 VO, 就別突然冒出個什么 Query 來; 還有 dto 的包名下, 就別突然有弄了個 BO 出來. 這種鶴立雞群, 駝立羊群指明了代碼是未做修改就從別的系統(tǒng)粘了過來, 顯得十分不專業(yè). 關(guān)于值對象的規(guī)范, 阿里巴巴的 Java 開發(fā)手冊寫得不錯. 如果所在團(tuán)隊有約定規(guī)范, 服從團(tuán)隊規(guī)范; 若有些部分團(tuán)隊沒有明確規(guī)約, 則可以參考阿里的 Java 開發(fā)手冊. 具體但不限于: 值對象類命名的大小寫, 屬性名的大小寫, Boolean 類型的字段的命名不要加 "is", 屬性類型推薦使用包裝類型等等, 在此不再贅述. 阿里的 Java 開發(fā)手冊也提供了 IDEA 插件, 可以在開發(fā)中對不符合規(guī)范的代碼進(jìn)行提示. 如有條件, 也可接入 Sonar, 配置規(guī)則對代碼進(jìn)行靜態(tài)掃描, 強制執(zhí)行規(guī)范.
實際工程中, 一般都會使用 ORM 框架, 那么 PO 可通過 mybatis-generator
或是 hibernate-tools
逆向工程, 根據(jù)數(shù)據(jù)庫表直接生成. 如果有維護(hù)良好的代碼生成器, 各層值對象均可通過數(shù)據(jù)庫或填寫字段自動生成.
值對象方法
值對象的方法比較簡單, 無外就是 getter
, setter
, toString
, hashCode
和 equals
. 其中 getter
和 setter
的建議只作存取, 不要加特殊邏輯, 否則很容易自尋煩惱. 同時也要注意方法名與屬性名保持一致, 大部分框架都是基于此存取屬性值的. 有些找不到對應(yīng)屬性直接報錯還好, 如果會忽略, 則這種不一致會導(dǎo)致錯誤發(fā)現(xiàn)時間后延, 從而造成不必要的損失. toString
, hashCode
和 equals
源自 Object
, 可根據(jù)實際需要決定是否需要覆寫. 如果覆寫, 那么就別忘了一個面試中問濫了的問題: equals
和 hashCode
為什么要一起覆寫? 具體而言, 這些方法要怎么來寫, 總結(jié)起來, 大致有五種方式:
代碼生成器
一般使用代碼生成器生成的值對象類, 會同時附帶這些方法.IDE
現(xiàn)代開發(fā)工具都會提供相關(guān)功能, 可以根據(jù)不同選項快速生成上述方法.Apache Commons Lang
Apache Commons Lang 提供的一系列 Builder 工具, 常用的如EqualsBuilder
,HashCodeBuilder
,ToStringBuilder
,CompareToBuilder
用于輔助實現(xiàn)一些常用方法. 其原理是利用反射獲得類的屬性, 從而進(jìn)行比較, 拼接或計算.lombok
使用 lombok 提供的注解:@Getter
,@Setter
,@ToString
,@EqualsAndHashCode
或@Data
來標(biāo)注值對象. lombok 的原理是在編譯時修改字節(jié)碼, 故只能在 class 文件中看到這些生成的方法, java 的源碼中則眼不見心不煩, 顯得比較整潔. 也是因為如此, lombok 對泛型的支持不是很好, 而且 IDE 也需要安裝插件才能不提示編譯錯誤.手寫
手工編寫這些方法的代碼.
上述五種方式中, 個人推薦用第 5 種, 手工編寫的方式, 雖然費時而且還容易出錯, 但這能讓你在 Boss 面前顯得很忙碌, 工作量很飽和.
在實際工程中, 如果代碼生成器及模板如果良好可用, 則最好使用代碼生成器, 省時省力, 還可以與部門其它項目保持一致. 如無代碼生成器或模板疏于維護(hù), 個人傾向于使用 lombok, 一是步驟少打字少, 提高效率; 二是因為字節(jié)碼技術(shù), 所以源碼看上去清爽干凈. 如果直接使用 @Data
需要注意, 值對象若有繼承關(guān)系, @ToString
和@EqualsAndHashCode
注解的 callSuper
屬性需改為 true
, 才會使非 Object
的父類屬性參與到 toString
, hashCode
和 equals
的生成之中, 該屬性默認(rèn)為 false
.
值對象轉(zhuǎn)換
除了內(nèi)部方法, 值對象之間的主要方法就是來回來去互相轉(zhuǎn)換了. 這些轉(zhuǎn)換總結(jié)下來, 大致也有五種方式:
Apache BeanUtils / PropertyUtils
Apache 提供的類轉(zhuǎn)換類, BeanUtils 在對 Bean 賦值時會進(jìn)行類型轉(zhuǎn)化, 而 PropertyUtils 不會.Spring BeanUtils
Spring 提供的類轉(zhuǎn)換類, 與 Apache 提供的工具類同名, 但是實際使用和特性均有不同.BeanCopier
由 cglib 提供的工具類, 由于采用字節(jié)碼技術(shù), 轉(zhuǎn)換效率比前兩種基于反射的 BeanUtils 都要高.dozer
開源工具包, 如用在 spring 項目中, 需要整合. dozer 比較靈活, 可以配置 xml 文件, 為值對象中的屬性轉(zhuǎn)化添加自己的邏輯.手寫
手工編寫代碼轉(zhuǎn)換.
之前說 setter
和 getter
手寫只是笑談, 現(xiàn)實中真沒見過有誰手敲的, 不過值對象轉(zhuǎn)換手寫的就普遍的多了, 而且不是不同名不同類型的屬性手敲, 而是從頭敲到尾, 大概是要這樣才能夠獲得一種安全感吧. 手寫轉(zhuǎn)換的相比反射轉(zhuǎn)換的唯一優(yōu)勢, 可能就在于執(zhí)行效率高了. 然而從系統(tǒng)整體運行考慮, 相對于數(shù)據(jù)庫和網(wǎng)絡(luò)傳輸, 這一點執(zhí)行效率幾乎可以忽略不計, 更何況如果真在乎這點效率, 還有基于字節(jié)碼的 BeanCopier 呢.
基于工具的轉(zhuǎn)換, 盡管方便, 但是也有問題需要注意. 比如同名不同類型屬性的轉(zhuǎn)換, setter
和 getter
是否匹配, Date
和 BigDecimal
等特殊類型以及 null
值的處理等等. Apache 的工具類在這些工具中是轉(zhuǎn)換效率最低的, 對于日期類型的處理很容易出錯, 而且還需要處理異常, 故此實際項目中很少使用. 不過因為與 spring 的工具同名, 代碼中經(jīng)郴呵海看到不做區(qū)分, 亂用一氣的, 估計是引包自動提示先引到哪個就用了哪個. 這兩個工具類參數(shù) source 和 target 位置相反, 混淆不分比較容易出現(xiàn)復(fù)制方向搞反的低級錯誤. 不論使用哪一個, 出現(xiàn)對象屬性時都要警覺, 因為其拷貝均為淺拷貝. BeanCopier 是基于字節(jié)碼技術(shù), 生成動態(tài)代理來進(jìn)行對象裝換, 因此效率比基于反射的 BeanUtils 要高不少. 不過代碼中也逞烟玻看到不做封裝, 每轉(zhuǎn)換一次都新生成一個代理類的, 也不知道是不是為了日后優(yōu)化故意留的一個口子. dozer 只是了解, 實際生產(chǎn)中還沒有使用過. 其配置最為靈活, 可通過 xml, 添加特定的映射轉(zhuǎn)換規(guī)則. 個人認(rèn)為, 對于屬性差異性很大的對象來說轉(zhuǎn)換方式統(tǒng)一便捷, 但是對于值對象這種大部分屬性名稱和類型均相同, 只有小部分差異的對象, 使用 dozer 以及其它可擴展的 converter 有些過重, 反而增加了代碼復(fù)雜度.
因此, 實際項目當(dāng)中最普及的用法, 還是在值對象屬性的命名和類型做到盡量統(tǒng)一的前提下, 先使用 spring 的 BeanUtils 拷貝一致的屬性, 再手工寫上幾行代碼, 轉(zhuǎn)換不一致的屬性.