重構:運用Java反射加多型 “幹掉” switch
前言:本篇文章主要描述我是如何通過Java的反射加多型幹掉 swtich這個程式碼的壞味道
目錄
- 程式碼的壞味道
- 《重構》曰
- 遭遇switch
- 利劍:多型加反射
- 結束戰鬥
程式碼的壞味道
有這麼一句話:普通的程式碼是寫給機器看的,優秀的程式碼是寫給人看的,在軟體開發的過程中,不知道大家有沒有遇到過if-else或switch滿天飛,函式長的看都不想看這種情況,先不說軟體的擴充套件性怎麼樣,這樣的程式碼就算是我們自己寫的,回頭看時也會發現邏輯很亂,這些都屬於“壞味道”,在我們進行開發中,如聞到這股“壞味道”,應該立刻解決,而不是放縱它發展,否則,後面會遇到其他很多的問題,比如需求改了程式碼不能改?增加新功能?又或者是改自己的程式碼發現看不懂了?添加註釋,不是個好辦法,如果我們能夠通過其他手段來改善這種情況,能很清晰看清程式碼的結構和含義並加強它的拓展性,為什麼還要大片的註釋來解釋它呢?
你可能會有問題,我用if-else或者switch,寫一個長長的函式多簡單,呼叫開銷什麼的都沒了,這也是我原來的疑問,在開發過程中,程式碼的高質量才是我們首要追求的,效能不是這時考慮的,二八定理(原諒我又搬出來了- -),你的常常呼叫的程式碼也就全部的20%,效能瓶頸常常是在IO等其他方面,而且現在的編譯器函式呼叫開銷耗費不了多少效能,我們應該在將來測試時確定性能瓶頸在那裡,再進行效能優化,如果瓶頸真的在這裡,很簡單展開就好。
程式碼的壞味道可不止這些,《重構》裡大約說了近百種,感興趣可以去看看這本書~
《重構》曰
《重構》裡描述過:面向物件程式的一個最明顯特徵就是:少用switch(或case)語句,從本質上說,switch語句的問題在於重複。你常會發現同樣的switch語句散佈於不同地地點。如果要為它新增一個新的case子句,就必須找到所有的switch語句並修改它們,面向物件中的多型概念能可為此帶來優雅的解決方案。多型最根本的好處就是:如果你需要根據物件的不同型別而採取不同的行為,多太使你不必編寫明顯的條件表示式
來看看我的經歷!
遭遇 switch
最近和小夥伴寫一個專案,客戶端發請求,服務端根據請求的不同處理後返回給客戶端,我在原先是這麼幹的,給客戶端的每個請求一個整型值當作標記,服務端接受後,用switch根據標記值mark來判斷請求的型別,接著對應的case處理,不要嘲笑我- -,相信很多人一開始都是這麼幹的,這樣做的結果是什麼?
當客戶端再次增添請求時,我有些受不了了,一方面程式碼寫的很長,逼迫我用了很多註釋,每增添一個case我都要新增許多註釋生怕自己下次看不懂,這樣導致程式碼整體看著很亂,另一方面需求和需求之間耦合性太強,就在一個函式裡。
這時程式碼沒有什麼面向物件思維,感覺就像c with class。
原先的程式碼(我刪掉了邏輯,不需要仔細看)
public class ParseJson {
/* 待解析的json物件 */
JSONObject js;
/* 建構函式,獲取json */
ParseJson(JSONObject json) {
this .js = json;
}
/* 解析json並獲取相應的結果 */
String Parse() throws SQLException {
/* 結果 */
String result = "";
int iret = 0;
int mark = 0;
/* 獲得mark,建立對應的Executesql物件 */
try {
mark = js.getInt("mark");
}catch (Exception e){
result = "{\"error\":0, \"status\":\"success\", \"date\":\"2015-08\", " +
"\"result\":{\"requestPhoneNum\":\"\", \"IsSuccess\":\"failure\"," +
"\"mark\":0, \"ResultINFO\":\"json解析失敗,您的輸入有誤\"}}";
return result;
}
switch (mark){
/* mark == 1 檢測此帳號是否正確且被註冊過 */
case 1:
break;
/* mark == 2 帳號註冊*/
case 2:
break;
/* mark == 3 忘記密碼 注意:應該驗證密寶後返回使用者一個key值,mark4根據key值來改新密碼 */
case 3:
break;
/* mark ==4 使用者更新密碼 */
case 4:
break;
/* mark ==5 使用者更新資訊
* 自己的name,頭像等
* 一般使用者點選詳細資訊會有更新操作 */
case 5:
break;
/* mark ==6
* 登記新的聯絡方式
* 需要修改UserFriend 好友and me 的 is update
* 每次好友登入需要檢查好友的isupdate
* 看誰換聯絡方式了 */
case 6:
break;
/* mark == 7
* 說明本地沒有資料,客戶端需要傳送全部資料 */
case 7:
break;
/* mark == 8
* 說明本地有資料,只更新發送好友的資料
* 相當於下拉重新整理 */
/*
* 先通過account 獲得使用者 uid
* uid 獲取 friendId
* friendId and uid 獲取不為0的 isUpdate資料
* 分析isUpdate資料,找出標記,組裝好友更新的資料,返回
*/
case 8:
break;
/* mark == 9
* 新增聯絡人,UserFriend中新增資料,注意要返回好友的資訊 */
case 9:
break;
/* mark == 10
* 生成二維碼加好友 */
case 10:
break;
/* mark == 11
* 生成二維碼
* 客戶端生成一張二維碼傳送給伺服器伺服器儲存 */
case 11:
break;
/* mark ==12
* 登入*/
case 12:
break;
/* mark == 13
* 刪除聯絡人 */
case 13:
break;
/* mark == 14
* 使用者傳送所有的個人資訊,儲存到資料庫
* */
case 14:
break;
}
return result;
}
}
怎麼樣,大大的switch加上大片註釋,看上去很難受,而且原本的類中ParseJson函式更長,大約1000行!
來看看缺點:
客戶端請求型別很多,switch所在的函式很長
客戶端新增加請求時,必須去改switch,增加case語句
程式碼重複,請求中總有一些類似的
變數滿天飛,變數名詞不達意
看看怎麼解決
利劍:多型加反射
相信很多人包括我也一樣,覺得“多型”是很高貴的詞,其實多型的好處就是:如果你需要根據物件的不同型別採取不同的行為,多型使你不必編寫明顯的條件表示式
改變
首先,我提取出一個公共的基類,因為所有的請求都有類似的地方,比如每個請求都要響應函式。
程式碼僅用來舉例
class request{
/* 建構函式 */
public request(int _mark, String _account){
mark = _mark;
account = _account;
}
/* 響應函式 */
public String response(){
...
}
...
/* 共有欄位 */
int mark;
String account;
}
接著我拆分了這個大大的switch,把它每個case語句換成了一個物件,物件繼承基類request
class AccountRegister extends request{
/* 建構函式 */
public AccountRegister(){}
/* 覆蓋response()函式 */
public String reponse(){
...
}
...
/* 屬於自己的欄位 */
String secret;
...
}
class AddFriendByAccount extends request{
/* 建構函式 */
public AddFriendByAccount(){}
/* 屬於自己的response()函式 */
public String reponse(){
...
}
...
/* 屬於自己的欄位 */
String FriendId;
...
}
還有很多
…
這樣,每種請求變成一個物件,物件名字就是請求內容,非常清晰,不需要要大片的註釋,公共的部分在基類,減少了程式碼重複,擴充套件性變得很強,客戶端要增加新請求?沒關係,我新新增一個類繼承基類就好
放一張修改後的類
每個請求是一個類,是不是清爽了很多?,每個類都不長,根據類名就能清楚的看懂類含義。
程式碼是清爽了,我怎麼用呢?原先根據switch值後case就好了,現在全是物件,我怎麼知道客戶端的請求對應是哪個物件呢,也就是說沒辦法選擇了。
別急,反射來了!!
不熟悉反射的小夥伴可以瞭解下 Thinking in Java – 型別資訊RTTI
我這篇文章很簡單的描述了下。
簡單的說就是我可以通過類的名字來例項化類物件,這麼棒!!通過名字就可以建立類物件?是的,就是這麼簡單。
反射例子
/* 基類 */
public class aaa {
public void func(){
return;
}
public static void main(String[] args) throws Exception {
/* 反射,通過類的名字 */
Class c = Class.forName("tryCode.ccc");
/* 例項化物件 */
aaa b = (aaa) c.newInstance();
/* output: bbb */
b.func();
}
}
/* 派生類bbb */
public class bbb extends aaa {
public void func(){
System.out.println("bbb");
}
}
/* 派生類bbb */
public class ccc extends aaa {
public void func(){
System.out.println("ccc");
}
}
用反射的結果呢?不再需要魔幻數字1, 2, 3…外加長長的註釋來標記這是幹什麼的,名字很清楚的表達了我們的請求,客戶端發來的登入請求不是 1 了,而是LogIn,我們僅需要幾行程式碼就動態建立了客戶端請求對應的物件,接著呼叫物件方法,編譯器會根據派生類物件來執行對應的response, 太神奇了!!!。
於是我的程式碼變成了這樣
除去註釋,看看它有幾行吧~,
結束戰鬥
呼呼~,戰鬥結束了,不得不說Java反射和多型真的非常有用,我這裡也僅是一個參考,或許大家有更好的方法來改進。也有可能我們根本不需要做這樣的改進(也許客戶端這輩子不會增加請求了呢?程式碼寫完就交給別人了呢- -這樣不對)
但我想要說的是:從開始我們就這樣做豈不是更好?別讓程式碼的壞味道瀰漫了你整個程式。
完
By XiyouLinuxGroup –wwh