1. 程式人生 > >重構:運用Java反射加多型 “幹掉” switch

重構:運用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