幾天前的一次上線,腦殘手抖不小心寫了bug,雖然組裡的老大沒有說什麼,但心裏面很是難過。同事說我之所以寫蟲子是因為我討厭if/else,這個習慣不好。的確,if/else可以協助我們很方便的寫出流程式控制制代碼,簡潔明了,這個條件做什麼,那個條件做什麼,說得很清楚。說真的,我從來不反對if/else,從經驗上看,越複雜的業務情境下,代碼寫的越簡單單一,通常越不容易出錯。以結果為導向的現代專案管理方式,這是一種很有效實踐經驗。
同事說的沒錯,我的確很討厭if/else。這個習慣很大程度是受Thoughtworks一位諮詢師朋友影響,他經常在我耳邊嘮叨,寫代碼要乾淨,要簡潔,要靈活多變,不要固守城規,不要動不動就if/else,switch/case。初入it領域,我一直把這句話奉為經典。在以後的學習工作中也時刻提醒自己要讓自己的代碼儘可能的看起來簡潔,不失靈活。不喜歡if/else並不意味著拒絕它,該使用的時候必要使用,比如函數介面入參check,處理異常分支邏輯流程等。通常能不用分支語句,我盡量不會使用,因為我覺得if/else很醜,每每看到if/else代碼,總會以挑剔的眼光看待它,想想能不能重構的更好。大多數時候,關於什麼好的代碼,大家的意見往往分歧很大,每個人都有各自的想法,審查你代碼的人可能會選擇另一種實現方式,這並不能說明誰對誰錯。
OO設計遵循SOLID(單一功能、開閉原則、裡氏替換、介面隔離以及依賴反轉)原則,使用這個原則去審視if/else,可能會發現很多問題,比如不符合單一原則,它本身就像一團漿糊,融合了各種作料,黏糊糊的很不乾淨;比如不符合開閉原則,每新增一種情境,就需要修改源檔案增加一條分支語句,商務邏輯複雜些若有1000種情境就得有1000個分支流,這種情況下代碼不僅僅噁心問題了,效率上也存在很大問題。由此可見,if/else雖然簡單方便,但不恰當的使用會給編碼代碼帶來非常痛苦的體驗。針對這種噁心的if/else分支,我們當然首先想到的去重構它--在不改變代碼外部功能特徵的前提下對代碼內部邏輯進行調整和最佳化,但,如何做呢。前段時間在項目中正好遇到一個噁心的if/else例子,想在這篇部落格裡和大家分享一下去除if/else重構的曆程。
if/else的惡瘤
有句話說的好--好文章是改出來,同樣,好的代碼也肯定是重構出來的,因為沒有哪個軟體工程師能夠拍著胸脯保證在項目之初代碼設計這塊,就考慮到了所有需求變化可能性的擴充。隨著項目的不斷成長,商務邏輯變的越來越複雜,代碼也開始變的越來越多,原有的設計可能不再滿足需求,那麼此時必須要重構。就系統整體架構而言,重構可能需要很大的改動,可能在架構流程上需要評審;就功能內代碼層次而言,這種重構在我們編碼過程中隨時可以進行,類似於if/else,swicth/case這種代碼的重構也屬於這種類型。今天我們要重構的if/else源碼如下所示,針對不同的status code,CountRecoder對象會執行不同的set方法,為不同內部屬性賦值。
public CountRecoder getCountRecoder(List countEntries) { CountRecoder countRecoder = new CountRecoder(); for (CountEntry countEntry : countEntries) { if (1 == countEntry.getCode()) { countRecoder.setCountOfFirstStage(countEntry.getCount()); } else if (2 == countEntry.getCode()) { countRecoder.setCountOfSecondStage(countEntry.getCount()); } else if (3 == countEntry.getCode()) { countRecoder.setCountOfThirdtage(countEntry.getCount()); } else if (4 == countEntry.getCode()) { countRecoder.setCountOfForthtage(countEntry.getCount()); } else if (5 == countEntry.getCode()) { countRecoder.setCountOfFirthStage(countEntry.getCount()); } else if (6 == countEntry.getCode()) { countRecoder.setCountOfSixthStage(countEntry.getCount()); } } return countRecoder;}
CountRecoder對象是一個簡單的Java Bean,用於儲存一天之中六種狀態分別對應的資料條目,提供了get和set方法。CountEntry是對應資料庫中每種狀態的資料條目記錄,包含狀態code和以及count兩個欄位, 我們可以使用mybatis實現資料庫記錄和java對象之間的轉換。上面getCountRecoder的方法實現了將list轉換為CountRecoder的功能。
看到這段代碼,想必已經有很多人要呵呵了,像一坨啥啥啥,長得這麼醜,真不知道它"爸媽"怎麼想的,怎麼敢"生"出來。啥都不說了,直接回爐重構吧。重構是門藝術,Martin flow曾寫過一本書《重構改變代碼之道》,裡面詳細的記錄了重構的方法論,感興趣的朋友可以閱讀一下。說到重構,通常我們在重構中會遇到一個問題,那就是如何能夠保證重構的代碼不改變原有的外部功能特徵 。經過TDD訓練的朋友應該知道答案,那就是單元測試,重構之前要寫單元測試,準確的來說應該是補單元測試,畢竟TDD的核心理念是測試驅動開發。對於今天部落格中分享的例子,因為代碼邏輯比較簡單,所以偷了懶,省卻了單元測試的曆程。
重構初體驗--反射
要重構上面的代碼,對設計模式精通的人可以立馬可以看出來這是使用原則模式/狀態模式的絕佳情境,將策略模式稍微變換,原廠模式應該也是ok的,當然也有些人會選擇使用反射。對於這些方法,這裡不一一列出,主要想講一下使用反射和原廠模式如何解決消除if/else問題,那先說反射吧,代碼如下所示:
private static Map methodsMap = new HashMap<>();static { methodsMap.put(1, "setCountOfFirstStage"); methodsMap.put(2, "setCountOfSecondStage"); methodsMap.put(3, "setCountOfThirdtage"); methodsMap.put(4, "setCountOfForthtage"); methodsMap.put(5, "setCountOfFirthStage"); methodsMap.put(6, "setCountOfSixthStage");}public CountRecoder getCountRecoderByReflect(List countEntries) { CountRecoder countRecoder = new CountRecoder(); countEntries.stream().forEach(countEntry -> fillCount(countRecoder, countEntry)); return countRecoder;}private void fillCount(CountRecoder shippingOrderCountDto, CountEntry countEntry) { String name = methodsMap.get(countEntry.getCode()); try { Method declaredMethod = CountRecoder.class.getMethod(name, Integer.class); declaredMethod.invoke(shippingOrderCountDto, countEntry.getCount()); } catch (Exception e) { System.out.println(e); }}
重構初體驗--所謂模式
使用反射去掉if/else的原理很簡單,使用HashMap建立狀態代碼和需要調用的方法的方法名之間的映射關係,對於每個CountEntry,首先取出狀態代碼,然後根據狀態代碼獲得相應的要調用方法的方法名,然後使用java的反射機制就可以實現對應方法的調用了。本例中使用反射的確可以協助我們完美的去掉if/else的身影,但是,眾所周知,反射效率很低,在高並發的條件下,反射絕對不是一個良好的選擇。除去反射這種方法,能想到的就剩下使用原則模式或者與其類似的狀態模式,以及原廠模式了,我們以原廠模式為例,經典的架構UML架構圖通常由三個組成要素: 抽象產品角色:通常是一個抽象類別或者介面,裡面定義了抽象方法 具體產品角色:具體產品的實作類別,繼承或是實現抽象策略類,通常由一個或多個組成類組成。 工廠角色:持有抽象產品類的引用,負責動態運行時產品的選擇和構建
策略模式的架構圖和原廠模式非常類似,不過在策略模式裡執行的對象不叫產品,叫策略。在本例中,這裡的產品是虛擬產品,它是服務類性質的介面或者實現。Ok,按照原廠模式的思路重構我們的代碼,我們首先定義一個抽象產品介面FillCountService,裡面定義產品的行為方法fillCount,代碼如下所示:
public interface FillCountService { void fillCount(CountRecoder countRecoder, int count);}
接著我們需要分別實現這六種服務類型的產品,在每種產品中封裝不同的服務演算法,具體的代碼如下所示:
class FirstStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfFirstStage(count); }}class SecondStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfSecondStage(count); }}class ThirdStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfThirdtage(count); }}class ForthStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfForthtage(count); }}class FirthStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfFirthStage(count); }}class SixthStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfSixthStage(count); }}
緊接著,我們需要是實現工廠角色,在工廠內需要實現產品的動態選擇演算法,使用HashMap維護狀態code和具體產品的對象之間的映射關係,
就可以非常容易的實現這一點,具體代碼如下所示:
public class FillCountServieFactory { private static Map fillCountServiceMap = new HashMap<>(); static { fillCountServiceMap.put(1, new FirstStageService()); fillCountServiceMap.put(2, new SecondStageService()); fillCountServiceMap.put(3, new ThirdStageService()); fillCountServiceMap.put(4, new ForthStageService()); fillCountServiceMap.put(5, new FirthStageService()); fillCountServiceMap.put(6, new SixthStageService()); } public static FillCountService getFillCountStrategy(int statusCode) { return fillCountServiceMap.get(statusCode); }}
用戶端在具體使用的時候就變的很簡單,那getCountRecoder方法就可以用下面的代碼實現:
public CountRecoder getCountRecoder(List countEntries) { CountRecoder countRecoder = new CountRecoder(); countEntries.stream().forEach(countEntry -> FillCountServieFactory.getFillCountStrategy(countEntry.getCode()) .fillCount(countRecoder, countEntry.getCount())); return countRecoder;}
重構初體驗--Java8對模式設計的精簡
和反射一樣使用設計模式也同樣完美的去除了if/else,但是不得不引入大量的具體服務實作類別,同時程式中出現大量的模板代碼,使得我們程式看起來很不乾淨,幸好Java 8之後引入了Functional Interface,我們可以使用lambda運算式來去除這些模板代碼。將一個介面變為Functional interface,可以通過在介面上添加FunctionalInterface註解實現,代碼如下所示:
@FunctionalInterfacepublic interface FillCountService { void fillCount(CountRecoder countRecoder, int count);}
那麼具體的服務實作類別就可以使用一個簡單的lambda運算式代替,原先的FirstStageService類對象就可以使用下面的運算式代替:
(countRecoder, count) -> countRecoder.setCountOfFirstStage(count)
那麼工廠類中的代碼就可以變為:
public class FillCountServieFactory { private static Map<Integer, FillCountService> fillCountServiceMap = new HashMap<>(); static { fillCountServiceMap.put(1, (countRecoder, count) -> countRecoder.setCountOfFirstStage(count)); fillCountServiceMap.put(2, (countRecoder, count) -> countRecoder.setCountOfSecondStage(count)); fillCountServiceMap.put(3, (countRecoder, count) -> countRecoder.setCountOfThirdtage(count)); fillCountServiceMap.put(4, (countRecoder, count) -> countRecoder.setCountOfForthtage(count)); fillCountServiceMap.put(5, (countRecoder, count) -> countRecoder.setCountOfFirthStage(count)); fillCountServiceMap.put(6, (countRecoder, count) -> countRecoder.setCountOfSixthStage(count)); } public static FillCountService getFillCountStrategy(int statusCode) { return fillCountServiceMap.get(statusCode); }}
這樣我們的代碼就重構完畢了,當然了還是有些不完美,程式中的魔法數字不利於閱讀理解,可以使用易讀的常量標識它們,在這裡就不做過多說明了。
總結
Craig Larman曾經說過軟體開發最重要的設計工具不是什麼技術,而是一顆在設計原則方面訓練有素的頭腦。重構的最終結果不一定會讓代碼變少,相反還有可能增加程式的複雜度和抽象性,就本例中的if/else而言,確實如此。我非常贊同我的一位朋友說的話,做技術要有追求,沒錯if/else可以在代碼中工作的挺好,也可以很容易的被接替者所理解,但是我們可以有更好的選擇,因為簡單的代碼也可以變得很精彩。多勤多思,也許有一天真的就可以達到Craig所說的在設計原則方面擁有訓練有素的頭腦,誰說不是這樣呢。加油吧。