業務中使用依賴方 DTO 類所帶來的問題
我負責的系統所依賴的部分服務接口需要重構,把對應的接口從 A 服務遷移到了 B 服務,雖然入參出參格式都一樣,但包路徑完全變了。而原有的 A 服務仍然有接口依賴,是以我必須要相容兩種【類名一樣結構幾乎一樣!】但【包路徑完全不同!】的出入參。
但由于我這邊的系統代碼結構中業務邏輯一直使用了 A 服務提供的 SDK 中的 DTO 定義來做業務參數,這時候就僵硬起來了。。該怎麼辦呢?
要不自定義一個 BO 吧!
基于目前代碼結構來看,自己系統的業務代碼中嚴重依賴了第三方服務 SDK 的定義類,使得自身與第三方服務形成了強耦合的關系(萬一人家更新 SDK 中的 BO 定義,自己豈不是得全改?!)。是以第一個想到的就是:不要用人家的 BO 了,我們自定義一個吧!
這樣無論人家的 SDK 怎麼變,我們隻需要在轉換器中做好映射處理,就可以完美解決強耦合問題了,prefect!
正當這時候定睛一看(兔美醬的眼神突然犀利了起來),這樣會涉及到大量的業務邏輯改動(所有使用到對應 BO 的地方都要改成新的)!而雙十一大促快到了,這時候做大改動出問題的話無疑是【撿了芝麻丢了西瓜】呀。
自定義 Self.BO 改動太大,風險太高,那麼隻能先保證原業務邏輯還是使用 A.BO 類,讓 B.BO 自行去相容了。。T.T
如何優雅降級而後續可擴充?
雖然雙十一前需要使用降級方案,但是考慮擴充性,後續還是需要解耦的。是以決定使用以下方案:
轉化器還是要做,但不是轉化成自定義 SELF.BO 類,而是轉成 A.BO,這樣做我們後續就可以把轉化器邏輯,與主業務的 BO 包換回 SELF.BO,即可完成統一轉化。這等于我們現在用最小的改動,就可以完成整體解耦的一半工作量了,無疑是當下最優方案。
轉化器的實作
先看看 BO 的定義:
public class Bo implements Serializable { private String goodsId; private String goodsName; private String brandStoreId =""; private String brandStoreName =""; private String categoryId; private String status; private Map goodsImageTags; ....... // 還有很多其他屬性,就不一一列舉了 ....... // 然後還有一堆 getter() / setter(),也不一一列舉了}
BO 中還有 一個 Map 類型的屬性,其中 GoodsImgTagVo 也是分别對應 A、B 包。
關于轉化器的實作,其實有很多種方法:
1、最先想到的辦法肯定是逐個映射:
public static A.BO parseToABo(B.BO bo){ A.BO aBo =new A.BO(); // goodsImageTags Map goodsImgTagVoMap =null; // 轉化各種類成傳回類 if(null != bo.getGoodsImageTags()) { goodsImgTagVoMap =new HashMap<>(); for (Map.Entry entry : bo.getGoodsImageTags().entrySet()) { GoodsImgTagVo vo =new GoodsImgTagVo(); vo.setGroup(entry.getValue().getGroup()); vo.setImage(entry.getValue().getImage()); vo.setSort(entry.getValue().getSort()); vo.setTag(entry.getValue().getTag()); goodsImgTagVoMap.put(entry.getKey(), vo); } } // 把這些對應的屬性轉化成傳回類的屬性 aBo.setGoodsId(bo.getGoodsId()); aBo.setGoodsName(bo.getGoodsName()); aBo.setBrandStoreId(bo.getBrandStoreId()); aBo.setBrandStoreName(bo.getBrandStoreName()); aBo.setCategoryId(bo.getCategoryId()); aBo.setStatus(bo.getStatus()); aBo.setGoodsImageTags(goodsImgTagVoMap); return aBo;}
· 優點:直接使用 java 原生實作,執行效率上應該是最快的。
· 缺點:非常明顯,代碼量會非常臃腫,而且隻能從 B 轉化成 A;如果需要增加一個 C,即需要又新增一個方法了~
寫完幾個類映射後我發現,簡直是在刷代碼量啊,而且代碼重複率會被各種拉高!!
2、如果都是基礎類型,可以直接使用 BeanUtils.copyProperties() 方法進行屬性複制
在尋找解決辦法的過程中,找到這個工具類可以對屬性均為基礎類型的 DTO 實體類進行快速複制轉換,用法大概為:
public static A.BO parseToABo(B.BO bo){ A.BO aBo = new A.BO(); //第一個參數是源,第二個參數是目标實體類VO BeanUtils.copyProperties(bo, aBo); return aBo;}
該方法可以複制 bo 執行個體中的所有屬性複制到 aBo 的同名屬性中。可它這有一個缺陷:隻能複制基礎類型(String、integer、boolean 這種),如果像我這樣 BO 中又包含自定義類型的話,方法将無法正常複制而抛出一個 InvocationTargetException,是以我是無法使用這個方法來快速實作轉換了。。(再次哭。。 ToT
3、使用擴充卡設計模式,實作多元度多類型适配
擴充卡模式是一種結構型設計模式。擴充卡模式的思想是:把一個類的接口變換成用戶端所期待的另一種接口,進而使原本因接口不比對而無法在一起工作的兩個類能夠在一起工作。
例如我有一台隻有 Type-C 插口的 macbook 和一個 USB 插頭的 U 盤,電腦想使用 U 盤的話就需要一個轉接頭,此時轉接頭就是一個擴充卡。
擴充卡模式涉及3個角色:
· 源(Adaptee):需要被适配的對象或類型,相當于 USB 插頭。
· 擴充卡(Adapter):連接配接目标和源的中間對象,相當于轉接頭。
· 目标(Target):期待得到的目标,相當于 Type-C 插口。
擴充卡模式包括3種形式:類擴充卡模式、對象擴充卡模式、接口擴充卡模式(或又稱作預設擴充卡模式)。
雖然目前我們處理的都是一些相同屬性的不同 DTO 類,但在業務發展的過程中,無法避免要相容不完全一樣的 DTO,這時候我們就需要有一個擴充卡來适配成統一的接口規範了。
基于上述考慮,我們可以使用擴充卡模式來統一實作 DTO 類轉換:
public class ABoAdapter extends A.BOImpl { private B.BO; public ResultAdapter(B.BO bBo){ this.bBo = bBo; } @Override public String getGoodsId() { return this.bBo.getGoodsId(); } @Override public void set GoodsId(String value) { this.bBo.setGoodsId(value); } ....... // 繼續重寫原 A.BO 的所有 getter() setter() 方法}
當我們使用時就可以直接:
A.BO aBo = new ABoAdapter(bBo);
經典如 Java 中 IO 對 Reader、InputStream之間的适配,字元流、位元組流之間的适配,就是使用擴充卡模式實作,詳細實作就不作展開了。
這樣做的優點是可以對所有(無論内部屬性是否一緻)的 DTO 類都能寫出新的擴充卡進行轉化,大大提高了系統擴充性。
但由于業務代碼中聲明的都是實體類,無法完全使用擴充卡模式中使用接口方式進行聲明,無法完全解耦。(這個故事教會了我們:參數聲明,應盡量使用接口類型)
與此同時帶來的還是跟逐一映射差不多的代碼量,當擁有許多擴充卡時,會讓系統變得非常零亂。
4、使用 Orika 項目中的 MapperFactory 工具類進行自定義映射(最優解)
我們先來看看 Orika 和 MapperFactory 是什麼?
Orika(以前托管于谷歌代碼)聲稱它是一個更簡單、更輕量、更快的Java Bean映射工具。
它允許你對象之間的轉換通過一個對象的屬性值指派到另外一個對象中,操作使用Java的内部機制,代替傳統的XML或者諸如此類的配置。它使用代碼生成來建立映射器,并且它的核心類有一個有趣的政策,它允許您優化性能。
它是完全可配置的,例如,預設情況下,java代碼生成器是Javassist。但是如果你使用EclipseJdt甚至是使用其他的代碼生成器提供者實作适當的接口。最後,這是一個非常有文檔的項目,有清晰的解釋和有用的代碼
通過使用Orika提供的API,你可以建構一個以MapperFactory為主要實作的通道,然後配合DefaultMapperFactory和一些其他的靜态類助手使用者建構MapperFactory,然後你可以從MapperFactory中擷取ClassMapBuilder以便為了流式申明映射配置,計字段映射(預設是雙向的,如果你願意,可以設定成單項的),不包括字段,指定構造函數和自定義清單、集合、嵌套字段等的映射。
有一個預設映射,就是映射兩個類之間名稱相同的字段,當然你可以在MapperFactory注冊自定義映射規則(ClassMapBuild 就是MapperFactory中通過屬性來建構的),這兩個操作都有合适分方法來調用它們。
Orika 的官方文檔在這:http://orika-mapper.github.io/orika-docs/index.html
了解它的能力以後,我們廢話不多說,立刻上實作代碼:
public class BoParserHelper { static MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); /** * 轉化 B.BO 為 A.BO * * @param bo * @return A.BO */ public static A.BO parseToABo(B.BO bo){ return mapperFactory.getMapperFacade().map(bo, A.BO.class); }}
你沒看錯,如果隻是單純包路徑不同,轉化就是一行代碼這麼簡單!
同時 MapperFactory 類還提供自定義的屬性名轉換,如
或
如果以上規則均不适用自身場景,還能使用 CustomerMapper 來高度定制化自己的映射規則,詳看官方文檔。
使用 MapperFactory 後,轉換器代碼從原來的 100+ 行(由于 DTO 類屬性非常多)代碼縮減到了僅 1 行,而且使得可讀性、可維護性和擴充性也得到了很大的增強,是以我們最終采用這種方案進行 DTO 類的轉換器實作,可見 Java 工具的博大精深啊~~
不過需要注意的是,MapperFactory 的初始化性能損耗比較大,是以我們在實際程序中最好隻初始化一次(可以讓 spring 托管自行執行個體化),避免不必要的性能損耗。
結語
不得不感歎前人對程式設計思想的極緻總結和所有程式設計人員的無私奉獻。使得我們能夠得出現在強大的 Java 提供了大量強大的工具庫。