对Java枚举与静态变量扩展,以及异步回调处理思考

132 阅读9分钟

以前发在 CSDN 上, blog.csdn.net/u014443348/…

掘金氛围感觉更好一点,打算慢慢转过来。



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

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