對Java枚舉與靜態變量擴展,以及異步回調處理思考

版權聲明:本文章原創於 RamboPan ,未經允許,請勿轉載。



源碼基於 Retrofit 2.6.0

變量擴展

因爲最近負責開發幾個應用需要加上一個功能:通過訪問服務器端,對比服務器端上軟件的版本號與當前應用的版本號,如果版本號大於當前應用就進行升級。

既然是要幾個應用都需要這個功能,那我們想的是肯定首先做成一個通用型的,每個應用都拷貝一份,然後調用不同的配置就好。

那按照我們常規的思路,就可以這樣,虛擬下代碼。

靜態變量


	//聲明一個檢查類
	public class UpdateChecker{

	    //應用1
	    public static final String TYPE_APP1 = "xxx1";
	    //應用2
	    public static final String TYPE_APP3 = "xxx3";
	    //應用3
	    public static final String TYPE_APP2 = "xxx2";
	 
	 	//針對不同的升級類型,進行調用
	    public void startCheck(String type){
	        switch (type){
	            
	            case TYPE_APP1:
	                break;
	
	            case TYPE_APP2:
	                break;
	
	            case TYPE_APP3:
	                break;
	
	            default:
	                
	        }
	    }
		……
    }
    

當然此處的的類型也可以換成 int ,我們在查找對應應用是否需要升級時調用對應的類別就行。

這種如果是自己用的話,肯定也是沒有問題的,如果要把程序交給別人使用,怎麼讓別人一眼就看到有哪些值可以進行調用。

那接手的人可以嘗試輸入 UpdateChecker. 之後,對應編程軟件彈出對應的靜態屬性或者方法提示,從中尋找看着比較像的選項。

更負責任的做法,就是在對應的方法上,加上註釋,並且使用 {@link } 進行標註,這樣一看註釋,就可以有相關鏈接,跳轉到對應的參數,查看對應參數的註釋。


	/**
     * 傳入不同的應用類型進行對應版本升級檢測。
     * 可以使用這些值。
     *
     * {@link #TYPE_APP1} 應用1
     * {@link #TYPE_APP2} 應用2
     * {@link #TYPE_APP3} 應用3
     *
     * @param type 需要檢測的類型
     */
    public void startCheck(String type){
        switch (type){

            case TYPE_APP1:
                break;

            case TYPE_APP2:
                break;

            case TYPE_APP3:
                break;

            default:

        }
    }

使用帶鏈接註釋很容易讓使用者快速找到相關的類,特別是如果此時的 TYPE 是在另一個專用的常量類裏,那不進行鏈接註釋,怕是不好找了。

當然此處也可能存在傳入值不適當的情況,比如傳了一個 abc ,那麼肯定是不合適的。


雖然這種事情發生概率極極極小,但是很多 Java 書中仍然推薦使用枚舉。因爲你只能選那幾個,隨便輸入值是不行的。

枚舉


	public class UpdateChecker{

	    public void startCheck(AppType type){
	        switch (type){
	            
	            case TYPE_APP1:
	                break;
	
	            case TYPE_APP2:
	                break;
	
	            case TYPE_APP3:
	                break;
	
	            default:
	                
	        }
	    }
		……
    }
    
	public enum AppType {
		//應用1
		TYPE_APP1,
		//應用2
		TYPE_APP2,
		//應用3
		TYPE_APP1;
	}

其實從這看,也沒什麼差別,無法從枚舉中看出有什麼優勢,不過如果我們如果需要把某些固定的信息和對應的類型進行綁定時。

如果是用靜態變量進行擴展時,那就需要加更多的靜態變量,在前綴或者後綴上進行區別,多了還是會有眼花的風險;那如果換成枚舉就很簡單了。

比如我們需要綁定每個類別的名字和網絡地址。


	public enum AppType {
		//需要使用的應用類型
	    //TYPE_APP1
	    TYPE_APP1(AppName.TYPE_APP1,AppUrl.AppUrl1),
	    //TYPE_APP2
	    TYPE_APP2(AppName.TYPE_APP2,AppUrl.AppUrl2),
	    //TYPE_APP3
	    TYPE_APP3(AppName.TYPE_APP3,AppUrl.AppUrl3);

		//需要存儲的對應信息
	    private final String name;
	    private final String url;
	    
	    //構造函數中進行配置
	    AppType(String name,Strng url){
	        this.name = name;
	        this.url = url;
	    }

	    String getName(){
	        return name;
	    }
		
		String getUrl(){
			return url;
		}
	
		//名字常量類
	    public static class AppName{
	        public final static String AppType1 = "AppType1";
	        public final static String AppType2 = "AppType2";
	        public final static String AppType3 = "AppType3";
	    }
	    
	    //網址常量類
	    public static class AppUrl{
	        public final static String AppUrl1 = "AppUrl1";
	        public final static String AppUrl2 = "AppUrl2";
	        public final static String AppUrl3 = "AppUrl3";
	    }
	}

這樣看起來是不是清爽很多,首先在內部對不同屬性定義一個靜態類,然後添加不同的常量變量在類中。

在枚舉類中聲明需要存儲的對應 final 變量(因爲我們不希望這個值在後期進行變化),構造函數中聲明需要配置的一些變量,此處例子就是一個名字和網址。

那我們在聲明不同的枚舉類型時,加入不同的變量進行配置。

我們 UpdateChecker.startCheck() 時只需要傳入一個類型,在需要使用名字的時候使用 type.getName() ,在需要使用網址時,使用 type.getUrl() ,那麼就很簡單的把各種類型進行綁定。有效減少了填錯值的情況。

當然也可以試着用一個普通類來完成這種枚舉類的形式,在此就不寫了。


請求網絡檢測的時候使用的 Retrofit ,因爲註解看着很華麗,哈哈。

一般處理網絡回覆分爲異步和同步,其實 … 寫法最後也差不多。

我們就分析異步的情況。進行請求時,需要將邏輯處理的回調作爲參數進行傳入。先來看看這個回調。


	public interface Callback<T> {
	 
		void onResponse(Call<T> call, Response<T> response);
	
		void onFailure(Call<T> call, Throwable t);
		
	}
	

onResponse() 方法是拿到回覆了,但是不一定是成功的,而且 response.body() 有可能是空,那麼我們就拆成三部分,分爲請求失敗,回覆失敗,回覆成功,我們新建一個類來實現。

封裝


	public abstract class BaseCallback<T> implements Callback<T> {
		

	    @Override
	    public void onResponse(Call<T> call, Response<T> response) {
	    	//如果返回成功,並且body 也不爲空。那麼就算成功,其他算回覆失敗。
	        if(response.isSuccessful() && response.body() != null){
	            responseSuccess(call, response);
	        }else{
	            responseFail(call, response);
	        }
	    }
	
	    @Override
	    public void onFailure(Call<T> call, Throwable t) {
	        requestFail(call, t);
	    }
	
	    public abstract void responseSuccess(Call<T> call, Response<T> response);
	
	    public abstract void responseFail(Call<T> call, Response<T> response);
	
	    public abstract void requestFail(Call<T> call, Throwable t);
	
	}

看着好像還闊以,雖然只是封裝了一小步 …

接下來在使用這個抽象類,肯定需要返回結果進行一些日誌的打印,或者是發出對應的事件。比如究竟是沒網,還是 json 解析失敗。


	private BaseCallback<UpdateKey> updateCallback =  
			new BaseCallback<UpdateKey>() {

                @Override
                public void responseSuccess(Call<UpdateKey> call,
                                            Response<UpdateKey> response) {
                                            
                    ……
                    mListener.onUpdateCheck(Result.CHECK_RESPONSE_JSON_FAIL);
                }

                @Override
                public void responseFail(Call<UpdateKey> call,
                                         Response<UpdateKey> response) {
                                         
					……
                    mListener.onUpdateCheck(Result.CHECK_RESPONSE_FAIL);
                }

                @Override
                public void requestFail(Call<UpdateKey> call,
                                        Throwable t) {
					……
                    mListener.onUpdateCheck(Result.CHECK_REQUEST_FAIL);
                }
            };

	public interface OnUpdateListener{

	    void onUpdateCheck(Result errorCode);
	
	    void onDownloadProgress(int progress);
	    
	}
	

這我們做了一個監聽,對請求結果進行回調,此處的 Result 類也是定義的一個枚舉類。用來通知不同的請求結果。其實剛開始還行,就定義了幾個 Result 類型,然後在 responseSuccess() 對數據進行一些更細的校驗時,那麼就對應需要返回更多的 Result 類型。

我想到了,如果是給的 Java 源碼還好,你可以直接在 Result 類進行擴展,那如果是拿到的庫文件,那想加都沒辦法了,只能選擇一個 Result 類型使用。

這樣又不能準確的傳達出想要表達的信息,這樣看來使用枚舉在這種情況下還是有一點的缺陷。

那麼想來想去,可以把 Result 類型從 enum 換成 interface 類型。


	public interface Result {
	
	    String getMsg();
	
	    Result NO_INTERNET_PERMISSION = new Result() {
	        @Override
	        public String getMsg() {
	            return MSG_NO_INTERNET_PERMISSION;
	        }
	    };
	
	    Result NO_WRITE_FILE_PERMISSION = new Result() {
	        @Override
	        public String getMsg() {
	            return MSG_NO_WRITE_FILE_PERMISSION;
	        }
	    };
	
	    Result CHECK_RESPONSE_JSON_FAIL = new Result() {
	        @Override
	        public String getMsg() {
	            return MSG_CHECK_RESPONSE_JSON_FAIL;
	        }
	    };
	
	    static final String MSG_NO_INTERNET_PERMISSION = "沒有獲取網絡權限";
	    static final String MSG_NO_WRITE_FILE_PERMISSION = "沒有獲取讀寫權限";
    	static final String MSG_CHECK_RESPONSE_JSON_FAIL = "JSON 字符解析失敗";
	
	}

這樣,除了自己定義的一些類型,如果後期打包給別人用,別人使用時,直接 new 一個接口類,並且複寫 getMsg() 就可以定義出自己想要的 Result 類。

枚舉與接口對比

那什麼情況下使用枚舉合適,什麼時候使用接口合適 ?

我的想法是:如果你是需要封裝一個東西,但是又不希望人家進行改動或者擴展時,使用枚舉。如果你希望人家可以在此基礎上進行擴展,那麼就可以使用接口,並且寫一些相關的類,給他人蔘考用。


回調收尾

完成了請求過程中關於信息的傳輸,我們肯定需要把監聽,或者引用的一些其他資源置空,防止內存泄漏。我們可以這麼操作 …


	private BaseCallback<UpdateKey> updateCallback =  
			new BaseCallback<UpdateKey>() {

                @Override
                public void responseSuccess(Call<UpdateKey> call,
                                            Response<UpdateKey> response) {
                                            
                    ……
                    mListener.onUpdateCheck(Result.CHECK_RESPONSE_JSON_FAIL);
                    clear();
                }

                @Override
                public void responseFail(Call<UpdateKey> call,
                                         Response<UpdateKey> response) {
                                         
					……
                    mListener.onUpdateCheck(Result.CHECK_RESPONSE_FAIL);
                    clear();
                }

                @Override
                public void requestFail(Call<UpdateKey> call,
                                        Throwable t) {
					……
                    mListener.onUpdateCheck(Result.CHECK_REQUEST_FAIL);
                    clear();
                }
            };
	
	private void clear(){ 
		……
		mListener = null;
	}
	

這樣雖然沒什麼問題,但是一點也不優雅,還要加三個地方,而且以後新的 BaseCallback 都要加,那我們就把這個 clear() 放在基類裏。


	public abstract class BaseCallback<T> implements Callback<T> {
		
	    @Override
	    public void onResponse(Call<T> call, Response<T> response) {
	    	//如果返回成功,並且body 也不爲空。那麼就算成功,其他算回覆失敗。
	        if(response.isSuccessful() && response.body() != null){
	            responseSuccess(call, response);
	            clear();
	        }else{
	            responseFail(call, response);
	            clear();
	        }
	    }
	
	    @Override
	    public void onFailure(Call<T> call, Throwable t) {
	        requestFail(call, t);
	        clear();
	    }
	    
		……
		
		public abstract void clear();
		
	}
	

那麼在 new 之後,我們需要多複寫一個 clear() 方法。

現在問題來了,如果按照現在的邏輯,在執行中會不會有什麼風險 ?

當然 ! 因爲我們 listener 在使用時並沒有進行 != null 判斷,如果在執行 responseSuccess() , responseFail() , requestFail() 回調時,進行了異步的操作,那麼很可能會出現空指針異常

除了在進行非空判斷以後,還有一個小的技巧,就是調整三個方法的返回值,讓使用者來決定是否需要執行 clear()

此處用 responseSuccess() 進行演示,因爲其他情況失敗了,一般默認是輸出信息,不會進行異步操作,所以暫時就不用那兩個方法演示。


	public abstract class BaseCallback<T> implements Callback<T> {

	    @Override
	    public void onResponse(Call<T> call, Response<T> response) {

	        if(response.isSuccessful() && response.body() != null){
	        	//如果返回 true ,證明沒有異步操作,直接進行清除。
	            if(responseSuccess(call, response ,this)){
	            	clear();
	            }else{
					//如果返回 false 的話,需要使用者自己找時機清除。
					//可以把此 baseCallback 對象傳到異步方法等執行完調用clear()
					//不過稍微有點麻煩.
				}
	        }else{
	            responseFail(call, response);
	            clear();
	        }
	    }
	
	    @Override
	    public void onFailure(Call<T> call, Throwable t) {
	        requestFail(call, t);
	        clear();
	    }
	
		//如果返回 true,表明可以直接調用 clear ,如果返回 false ,則表明剩下還有異步操作。
		//傳入 BaseCallback 是方便異步方法中仍然拿到 callback 引用。
	    public abstract boolean responseSuccess(Call<T> call,
	    										Response<T> response,
	    										BaseCallback callback);
		……
	}
	

responseSuccess() 通過返回 true 或者 false ,來決定是否需要 clear() ,如果是同步操作,那麼順序執行就可以執行 clear() ,反之就不執行。

如果其他地方有持有該 BaseCallback 對象時,也可以在異步操作完成後調用 clear() ,而不需要在 responseSuccess() 把此對象進行傳遞。

至於 responseFail()requestFail() ,我們可以考慮做一個通用的日誌輸出方法,並且方法爲 protected ,那麼 new BaseCallback() 時只需要複寫一個方法,如果需要更改的話,再進行復寫,那麼不會看着比較臃腫。

此分析純屬個人見解,如果有不對之處或者欠妥地方,歡迎指出一起討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章