对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() 时只需要复写一个方法,如果需要更改的话,再进行复写,那么不会看着比较臃肿。

此分析纯属个人见解,如果有不对之处或者欠妥地方,欢迎指出一起讨论。

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