來源:撿田螺的小男孩
最近工作中,我通過層層優化重復代碼 ,最后抽出個通用模板.因此跟大家分享一下優化以及思考的過程。我會先造一個相似的例子,然后一步步帶大家如何優化哈 ,看完一定會有幫助的。
優化前的例子
第一步優化:抽取公用方法
第二步優化:反射對比字段
第三步優化:泛型+ lambda 函數式
第四步優化:繼承多態
第五步優化:模板方法成型
大功告成: 策略模式+工廠模式+模板方法模式
1. 優化前的例子
在這里,我先給大家模擬一個業務場景哈,并給出些簡化版的代碼
假設你有個對賬需求:你要把文件服務器中,兩個A、B 不同端,上送的余額明細和轉賬明細 ,下載下來,對比每個字段是否一致 .
明細和余額的對比類似 ,代碼整體流程:
讀取A、B端文件到內存的兩個list
兩個list通過某個唯一key轉化為map
兩個map字段逐個對比
我們先看明細對比 哈,可以寫出類似醬紫的代碼:
?
//對比明細 private?void?checkDetail(String?detailPathOfA,String?detailPathOfB?)throws?IOException{ ???//讀取A端的文件 ???List?resultListOfA?=?new?ArrayList<>(); ???try?(BufferedReader?reader1?=?new?BufferedReader(new?FileReader(detailPathOfA)))?{ ????????????String?line; ????????????while?((line?=?reader1.readLine())?!=?null)?{ ????????????????resultListOfA.add(DetailDTO.convert(line)); ????????????} ????????} ???//讀取B端的文件 ???List ?resultListOfB?=?new?ArrayList<>(); ???try?(BufferedReader?reader1?=?new?BufferedReader(new?FileReader(detailPathOfB)))?{ ????????????String?line; ????????????while?((line?=?reader1.readLine())?!=?null)?{ ????????????????resultListOfB.add(DetailDTO.convert(line)); ????????????} ????????} ????//A列表轉化為Map ????Map ?resultMapOfA?=?new?HashMap<>(); ????for(DetailDTO?detail:resultListOfA){ ????????resultMapOfA.put(detail.getBizSeq(),detail); ????} ?????//B列表轉化為Map ????Map ?resultMapOfB?=?new?HashMap<>() ????for(DetailDTO?detail:resultListOfB){ ????????resultMapOfB.put(detail.getBizSeq(),detail); ????} ????//明細逐個對比 ????for?(Map.Entry ?temp?:?resultMapOfA.entrySet())?{ ????????if?(resultMapOfB.containsKey(temp.getKey()))?{ ????????????DetailDTO?detailOfA?=?temp.getValue(); ????????????DetailDTO?detailOfB?=?resultMapOfB.get(temp.getKey()); ????????????if?(!detailOfA.getAmt().equals(detailOfB.getAmt()))?{ ??????????????????log.warn("amt?is?different,key:{}",?temp.getKey()); ????????????} ????????????if?(!detailOfA.getDate().equals(detailOfB.getDate()))?{ ????????????????log.warn("date?is?different,key:{}",?temp.getKey()); ????????????} ????????????if?(!detailOfA.getStatus().equals(detailOfB.getStatus()))?{ ????????????????log.warn("status?is?different,key:{}",?temp.getKey()); ????????????} ????????????...... ????????} ??} }
?
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
2. 抽取公用方法去重
大家仔細看 以上明細對比的例子 ,發現了重復 代碼:
我們可以抽取一個公用方法去優化它 ,比如抽取個讀取文件的公用方法 readFile:
同理,這塊代碼也是重復 了:
我們也可以抽個公用方法: convertListToMap
通過抽取公用方法后,已經優雅很多啦~
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
3. 反射對比字段
我們再來看下字段對比的邏輯,如下:
以上代碼會取兩個對象的每個字段對比 ,如果明細對象的屬性字段特別多的話 ,這塊代碼也會顯得重復冗余 。我們可以通過反射去對比兩個對象的屬性,如下:
有了這個反射對比方法 ,原來的代碼就可以優化成這樣啦,是不是優雅了很多:
4.Lambda 函數式+泛型
實現完明細文件的對比,我們還需要余額文件的對比 :
同樣的,也是先讀取文件 ,如下:
大家可以發現,讀取余額文件和剛剛的讀取明細文件 很像,有一部分代碼是重復的 ,但是不能直接一下子抽個共同函數 出來:
對了,convert方法是醬紫的哈 :
大家可以發現,就是一個返回類型,以及這個對應類型的一個靜態 convert 方法不一致而已 ,如果是類型不一樣,我們可以使用泛型替代 ,如果是一個小的靜態方法不一致,我們則可以使用lambda函數式接口提取,因此可以抽這個這么一個公用方法吧:
平時我們用泛型+ Lambda 表達式結合,去抽取公用方法 ,代碼就顯得高端大氣很多,對吧~
5. 繼承多態.
在余額對比文件中,讀取完文件到內存后,我們需要把通過某個唯一key關聯起來,即把List轉為Map,如下:
一般來說,把兩個list轉化為Map,抽一個公用方法是不是就好了?比如說醬紫:
其實也行,但是其實可以更抽象一點 。因為余額和明細對比 都有list轉map的需求,而且也是有共性的,只不過是轉化map的key和value的類型不一致而已
圖片
我們仔細思考一下,value類型是不同類型(分別是BalanceDTO 和 DetailDTO ),而key則是對應對象的一個或者某幾個屬性連接起來的 。對于不同類型,我們可以考慮泛型。對于余額和明細對象不同的key的話,我們則可以考慮繼承和多態,讓它們實現同一個接口就好啦。
我們可以使用繼承和多態,定義一個抽象類BaseKeyDTO,里面有個getKey的抽象方法,然后BalanceDTO 和DetailDTO都繼承它,實現各自getKey的方法,如下:
最后,我們應用繼承多態+擴展泛型(
最后明細和余額 對比,可以優化成這樣,其實看起來已經比較優雅啦 :
?
???//對比明細 ????private?void?checkDetail(String?detailPathOfA,?String?detailPathOfB)?throws?IOException?{ ????????//讀取A端明細的文件 ????????List?resultListOfA?=?readDataFromFile(detailPathOfA,?DetailDTO::convert); ????????//讀取B端明細的文件 ????????List ?resultListOfB?=?readDataFromFile(detailPathOfB,?DetailDTO::convert); ????????//A列表轉化為Map ????????Map ?resultMapOfA?=?convertListToMap(resultListOfA); ????????//B列表轉化為Map ????????Map ?resultMapOfB?=?convertListToMap(resultListOfB); ????????//明細逐個對比 ????????compareDifferent(resultMapOfA,resultMapOfB); ????} ???//對比余額 ????private?void?checkBalance(String?balancePathOfA,String?detailPathOfB)?throws?IOException?{ ????????//讀取A端余額的文件 ????????List ?resultListOfA?=?readDataFromFile(balancePathOfA,BalanceDTO::convert); ????????//讀取B端余額的文件 ????????List ?resultListOfB?=?readDataFromFile(detailPathOfB,BalanceDTO::convert); ????????//A余額列表轉化為Map ????????Map ?resultMapOfA?=?convertListToMap(resultListOfA); ????????//B余額列表轉化為Map ????????Map ?resultMapOfB?=?convertListToMap(resultListOfB); ????????//余額逐個對比 ????????compareDifferent(resultMapOfA,resultMapOfB); ????} ????//對比也用泛型,抽一個公用的方法哈 ????private?void?compareDifferent(Map ?mapA,?Map ?mapB)?{ ????????for?(Map.Entry ?temp?:?mapA.entrySet())?{ ????????????if?(mapB.containsKey(temp.getKey()))?{ ????????????????T?dtoA?=?temp.getValue(); ????????????????T?dtoB?=?mapB.get(temp.getKey()); ????????????????List ?resultList?=?compareObjects(dtoA,?dtoB); ????????????????for?(String?tempStr?:?resultList)?{ ????????????????????log.warn("{}?is?different,key:{}",?tempStr,?dtoA.getKey()); ????????????????} ????????????} ????????} ????} }
?
6. 模板方法
大家回頭細看,可以發現不管是明細還是余額 對比,兩個方法很像,都是一個骨架流程 來的:
讀取A、B端文件到內存的兩個list
兩個list通過某個唯一key轉化為map
兩個map字段逐個對比
大家先回想一下模板方法模式 :
定義了一個算法的骨架 ,將一些步驟延遲到子類中實現。這有助于避免在不同類中重復編寫相似的代碼。
頓時是不是就覺得這塊代碼還有優化空間~~
6.1 定義對比模板的骨架
我們可以嘗試這兩塊代碼再合并,用模板方法優化它。我們先定義一個模板,然后模板內定義它們骨架的流程 ,如下:
6.2 模板的方法逐步細化
因為readDataFromFile需要輸出兩個list,所以我們可以定義返回類型為Pair,代碼如下:
又因為這個函數式的轉化,是不同子類才能定下來的 ,我們就可以聲明個抽象方法convertLineToDTD,讓子類去實現。因此模板就變成這樣啦:
同理,還有兩個list轉化為兩個map再對比,我們可以聲明為這樣:
因此最終模板就是這樣啦 :
6.3 不同對比子類
如果你是余額對比,那你聲明一個CheckBalanceStrategyServiceImpl去繼承抽象模板
如果你是明細對比 ,那你聲明一個CheckDetailStrategyServiceImpl去繼承抽象模板
這兩個不同的子類,就像不同的策略,我們應該都能嗅到策略模式 的味道啦~
7. 工廠模式+ 模板方法 + 策略模式全家桶
有了明細對比、余額對比的模板,為了更方便調用,我們還可以定義一個校驗策略 接口,然后交給spring工廠 類,這樣更方便調用。其實日常開發中,這三種設計模式一般一起出現,非常實用 :
我們先聲明一個校驗ICheckStrategy接口:
然后模板AbstractCheckTemplate實現ICheckStrategy接口:
接著,不同對比策略類CheckDetailStrategyServiceImpl 和CheckDetailStrategyServiceImpl映射對應的對比校驗類型:
?
/** ?*?明細對比策略 ?*/ @Service public?class?CheckDetailStrategyServiceImpl?extends?AbstractCheckTemplate?{ ?????@Override ????protected?DetailDTO?convertLineToDTD(String?line)?{ ????????return?DetailDTO.convert(line); ????} ????@Override ????public?void?check(String?filePathA,?String?filePathB)?throws?IOException?{ ????????checkTemplate(filePathA,?filePathB); ????} ????//對比校驗類型為:明細 ????@Override ????public?CheckEnum?getCheckEnum()?{ ????????return?CheckEnum.DETAIL_CHECK; ????} } /** ?*?余額對比策略 ?*/ @Service public?class?CheckBalanceStrategyServiceImpl?extends?AbstractCheckTemplate ?{ ????@Override ????public?void?check(String?filePathA,?String?filePathB)?throws?IOException?{ ????????checkTemplate(filePathA,?filePathB); ????} ?????//對比校驗類型為:余額 ????@Override ????public?CheckEnum?getCheckEnum()?{ ????????return?CheckEnum.BALANCE_CHECK; ????} ????@Override ????protected?BalanceDTO?convertLineToDTD(String?line)?{ ????????return?BalanceDTO.convert(line); ????} }
?
最后一步,我們借助spring的生命周期,使用ApplicationContextAware接口,把對用的策略,初始化到map里面。然后對外提供checkCompare方法即可。讓調用者決定用哪一種對比,其實這算工廠模式思想 ,大家可以自己思考一下~
最后
本文介紹了:如何將一些通用的、用于優化重復冗余代碼的技巧應用到開發中。最終,我通過這些技巧將代碼優化成一個通用模板。很有實踐的意義~
審核編輯:湯梓紅
評論
查看更多