巧用接口解耦分离实现

时间:2021-11-04 13:22:09
架构对于很多初学者来讲往往都是唯恐不及的,总感觉架构对自己而言太过遥远,实际上架构对大家来说并不陌生,甚至你每天都在与之打交道,只是你习以为常没有留意而已。生活中处处是架构设计的影子,只不过在日常生活中我们不称之为架构罢了。记得在一年多以前爱哥在公司楼下的食堂发现一个有趣的现象,楼下食堂打饭是这样的,你得先去食堂售票处那够买饭票,然后拿着这张饭票排队点餐,点餐的过程是这样的,先会有一个大叔问你吃什么,有A、B、C、D餐,你告诉大叔要B餐,然后大叔吼一声“B”,干净利落!站在大叔身后的阿姨就以迅雷不及掩耳之势打了一份B餐放到前面,最后打水果和配汤的小哥就顺势放上一份水果和配汤到餐盘里,你就可以端走你的套餐去啵叽啵叽了,整个过程一气呵成不过十来秒时间。这里一个简单的食堂打饭过程,涉及到了三个人员的配置:猥琐的叫卖大叔、抠鼻屎往菜里扔的阿姨、不住对着水果和配汤打喷嚏的小哥……那么为什么要分三个人来做呢?这估计*都看得出来,社会分工效率更高是吧,把这种分工思想应用到我们代码里,就是一种简单的分层架构体现,如果我们再往细里想想,你就发现上面食堂打饭的过程大有学问,首先我们来看看猥琐大叔,猥琐大叔是负责跟我们直接接触的,我们可以把他看作是我们架构中的界面层,界面层只管与用户交互的逻辑,这里猥琐大叔也确实只是在问我们要吃什么,而龌龊阿姨呢,则不会理会到底是谁要的哪个套餐,她只接收来自猥琐大叔的指令,猥琐大叔告诉她接下来要打哪个餐,龌龊阿姨并不和点餐的人有任何直接的接触,因此这里我们可以将龌龊阿姨看作是业务逻辑处理的一部分,最后我们来看看孤独的小哥,小哥的作用很单纯,他不接受任何人的指令,他的唯一工作就是重复不断地把水果和配汤放到每一个餐盘里,附送一个包含各种病菌的喷嚏……这里小哥虽然孤独,但是他也算是在处理自己的业务逻辑,也是整个点餐过程中不可或缺的一部分,因此我们也可以将小哥看作是业务处理层的一部分。如果大家能看到这一步,那么恭喜你,至少你已经有一丝架构逻辑的思想了,不过这个点餐过程我们还可以继续往深的地方看。算了,还是不看了……有点跑题了,爱哥大费周章地举了这个例子其实就想说明架构其实离我们真的很近,只要平日里大家多学会思考,多想想,就会发现我们身边到处都是架构思想的影子。


前几天爱哥在群里跟群里的各位大大讲过一个很简单但是很实用的架构小例子,就是利用接口简单地将我们的实现分离出去,以应对项目中不同的变化,这个架构的例子非常简单,只要你了解接口、一眼就能看懂,不过在此之前大家要明白一点的是很多情况下我们的架构都是依赖于某个具体的业务模型,如果这个业务模型变更了那么我们架构也有可能就不再适用,这里很多朋友可能会打爱哥脸,BullShit!简直鬼扯,架构不应该就是为了解决多变的业务逻辑才存在的吗!!是的没错,架构是应该为了解决多变的业务逻辑而存在,但是,如果我们的整个业务模型都变了呢?打个不恰当的比方,人可以穿不同的衣服来表现自己不同的个性,这是可以的,衣服就像那多变的业务逻辑,而人是应用于这套业务逻辑的模型,如果突然人变成了狗,你那些漂亮衣服还有个JB毛用啊!!!废话少说,我们先来看看这个例子,这个例子呢是一个网络请求接口的封装例子,再说这个例子前我先跟大家说说服务器返回的JSON结构,JSON的结构是下面这个样子的:

巧用接口解耦分离实现

也就是说我们的基本JSON结构里有四个字段:code、data、msg、time,其中code、msg和time是每个响应JSON的基本字段,而data字段里的数据就是我们根据不同接口请求所返回的数据,这里code表示响应码,当且仅当code==200时data里才会有数据,而msg呢则表示一些额外的附加信息,如果code==200,msg不会有任何信息,否则msg会填充相应的信息说明为什么请求不成功,最后的time则表示请求响应成功的时间戳。OK,有了这么一个JSON结构,我们就可以在此结构上封装我们的网路请求框架,首先,我们根据JSON的结构新建一个响应的实体类:

 * 网络请求数据响应包装类
*
* @author AigeStudio{@link http://aigestudio.com/?p=100}
* @since 2016-08-15
*/
public final class Response<T> {
public T mData;// 数据实体

public String mResponseMsg;// 响应消息文本 在mResponseCode不等于200的情况下该字段才会有数据

public long mResponseTimeStamp;// 响应请求的时间戳

public int mResponseCode;// 具体的相应码 200表示请求并响应成功 该值由服务器规定
}
这个类很简单,基本与我们的JSON结构对应,在这个例子中,爱哥只演示两个接口的封装:登录与版本更新,其他的大家可以自行尝试练手玩,对于登录和版本更新,我们也应该根据服务器返回的JSON结构建立不同的数据实体类,假设我的登录JSON是酱紫的:

巧用接口解耦分离实现
那么我们对应的数据实体类就应该是酱紫的:
/**
* 登录信息数据实体类
*
* @author AigeStudio{@link http://aigestudio.com/?p=100}
* @since 2016-08-15
*/
public class LoginInfo {
public String mUser;// 用户名
public String mPassword;// 用户密码
public String mName;// 用户昵称
public String mAvatarUrl;// 用户头像地址
public String mSex;// 用户性别
public String mBirthday;// 用户生日

public long mRegisterTimeStamp;// 用户注册时的时间戳
public long mLastLoginTimeStamp;// 用户上一次登陆的时间戳

public int mAge;// 用户年龄
}

假设我的登录JSON是酱紫的:
巧用接口解耦分离实现

那么我们对应的数据实体类就应该是酱紫的:
/**
* 应用更新信息数据实体类
*
* @author AigeStudio{@link http://aigestudio.com/?p=100}
* @since 2016-08-15
*/
public class UpgradeInfo {
public String mVersionName;// 版本名
public String mDescription;// 版本描述
public String mDownloadUrl;// 安装包下载地址

public int mVersionCode;// 版本号

public boolean isForce;// 是否强制更新
}

有童鞋可能会问,诶?爱哥这两个登录的和版本更新的数据实体木有code、msg、time什么的啊,这里要注意的是这个两个数据实体对应的是JSON结构中的data部分,而至于通用的code、msg和time,我们早就在上面的Response类里声明了,大家注意上面的Response类爱哥用了一个泛型去表示data的类型,为的就是根据传入的具体的数据实体来封装对应的响应实体对象,同时这里爱哥还用了一个final修饰符修饰Response类,也就是说这个Response类不能被继承,从它出生那刻起它就是这样了,除非我们的JSON基础结构改变,否则它就不会变。可能又有好奇的童鞋会问爱哥这里应该也可以直接用继承,让登录和版本更新的数据实体类继承Response类就好了啊,没错,是可以这么干,但是这里使用泛型可以使我们的类结构与JSON结构更符合不是麽?


现在我们的数据实体类定义完了,是时候来看看业务逻辑了,本文的标题是“巧用接口解耦分离实现”,这里我们的网络请求接口方法只有两个:登录和版本更新,我们先定义一个接口来封装这两个方法的声明:

/**
* 网络请求方法接口
*
* @author AigeStudio{@link http://aigestudio.com/?p=100}
* @since 2016-08-15
*/
interface IApi {
/**
* 用户登录
*
* @param user 用户账户
* @param password 用户密码
* @param c 数据请求完成后回调接口
*/
void login(String user, String password, CallBack<LoginInfo> c);

/**
* 应用更新
*
* @param code 版本号
* @param c 数据请求完成后回调接口
*/
void upgrade(String code, CallBack<UpgradeInfo> c);
}
一个登陆方法和一个版本更新方法,对登陆方法而言,我们需要传入两个必要参数:用户名和密码,而对于版本更新而言,我们需要传入一个当前版本的版本号,用于和服务器的最新版本做对比,这些呢都是我们业务模型所决定的,跟代码逻辑无关,但是我们的架构得跟着这些业务模型变化。大家一定注意到上面的两个方法中我们都加了一个CallBack类型的参数,我们先来看看这是啥玩意:

/**
* 网络请求完成后的回调接口
*
* @param <T> 请求完成后返回的数据实体类
* @author AigeStudio{@link http://aigestudio.com/?p=100}
* @since 2016-08-15
*/
public interface CallBack<T> {
/**
* 请求完成后回调该方法
*
* @param isSuccess 请求是否成功
* @param response 如果请求成功该参数不为空否则为空
* @param error 如果请求成功该参数为空否则会将失败原因传入
*/
void onFinish(boolean isSuccess, Response<T> response, String error);
}

CallBack其实就是一个回调接口,大家知道我们网络请求是耗时的,我们不能在主线程里处理这些耗时操作,因此我们需要在子线程里去请求,那么我们就需要在请求完成后将结果返回给调用者,而CallBack则处理这个逻辑。


现在我们声明了接口,接下来就该实现具体的逻辑了,现在问题来了,对于网络请求来说,我们又很多成熟的框架,最基本的我们可以使用HttpURLConnection自己原汁原味地实现一个,也可以用封装的OkHttp去实现,也可以使用当下较为流行的RESTFUL框架,不管你使用哪种方式,都该是遵循我们的业务模型的,不管底层逻辑如何实现,都不应该影响我们上层的结构,这里的体现就是我们上面定义的接口和具体的实现类。这里我们有三种实现:HttpURLConnection、OkHttp和RESTFUL,我们对应不同的实现类:

/**
* 网络请求方法常规实现
*
* @author AigeStudio{@link http://aigestudio.com/?p=100}
* @since 2016-08-15
*/
class ApiNor implements IApi {
@Override
public void login(String user, String password, CallBack<LoginInfo> c) {

}

@Override
public void upgrade(String code, CallBack<UpgradeInfo> c) {

}
}
/** * 网络请求方法OkHttp实现 * * @author AigeStudio{@link http://aigestudio.com/?p=100} * @since 2016-08-15 */class ApiOkHttp implements IApi {    @Override    public void login(String user, String password, CallBack<LoginInfo> c) {    }    @Override    public void upgrade(String code, CallBack<UpgradeInfo> c) {    }}

/**
* 网络请求方法RESTFUL实现
*
* @author AigeStudio{@link http://aigestudio.com/?p=100}
* @since 2016-08-15
*/
class ApiRest implements IApi{
@Override
public void login(String user, String password, CallBack<LoginInfo> c) {

}

@Override
public void upgrade(String code, CallBack<UpgradeInfo> c) {

}
}

这里很多童鞋会问爱哥为什么一个网络请求封装要三种不同的实现呢?仅仅是为了演示吗?NO NO NO,这绝不是演习,事实上我们在实际开发中很有可能遇到这样的情况,如上所说网络请求有很多封装好的框架,但是这些框架即便由很牛逼的大神所写也难免会出现一些小BUG,如何项目紧急而你又无法去解决这些BUG的时候,使用这样的接口隔离实现能让你快速在不同的网络请求框架之间切换且不影响上层结构。说了这么多,我们来看看怎么用,这里爱哥使用一个功能类来封装这三种不同的实现:

/**
* 网络请求方法单例封装类
*
* @author AigeStudio{@link http://aigestudio.com/?p=100}
* @since 2016-08-15
*/
public final class Api implements IApi {
private volatile static Api sInstance;

private IApi mApi;

private Api() {
mApi = new ApiNor();
}

public static Api getInstance() {
if (null == sInstance)
synchronized (Api.class) {
sInstance = new Api();
}
return sInstance;
}

@Override
public void login(String user, String password, CallBack<LoginInfo> c) {
mApi.login(user, password, c);
}

@Override
public void upgrade(String code, CallBack<UpgradeInfo> c) {
mApi.upgrade(code, c);
}
}

这个功能类很简单,大家可以看到它也是实现了IApi接口,本质而言也是个IApi的实现类,不同的是其不实现任何具体的逻辑而只是简单地调用上面那三个实现类对应的逻辑而已,这里我们构造的是ApiNor对象:
mApi = new ApiNor();

也就是说Api里所有的实现方法实质上都是调用的ApiNor的逻辑实现。最后我们再看看如何在上层中使用:

/**
* 客户类
*
* @author AigeStudio{@link http://aigestudio.com/?p=100}
* @since 2016-08-15
*/
public class Main {
public static void main(String[] args) {
Api.getInstance().login("AigeStudio", "123456789", new CallBack<LoginInfo>() {
@Override
public void onFinish(boolean isSuccess, Response<LoginInfo> response, String error) {
if (isSuccess) {
int code = response.mResponseCode;
if (code == 200) {
String user = response.mData.mUser;
String password = response.mData.mPassword;
String name = response.mData.mName;
String avatarUrl = response.mData.mAvatarUrl;
String sex = response.mData.mSex;
String birthday = response.mData.mBirthday;

long registerTimeStamp = response.mData.mRegisterTimeStamp;
long lastLoginTimeStamp = response.mData.mLastLoginTimeStamp;

int age = response.mData.mAge;
} else {
System.out.print(response.mResponseMsg);
}
} else {
System.out.print(error);
}
}
});
}
}

我们在main方法中简单地调用login方法模拟登录,这个网络请求框架我们就算完结了,假使有一天,ApiNor里的逻辑出现了BUG而我们又没时间改,那么我们可以简单地切到OkHttp的实现中:
mApi = new ApiOkHttp();

而这个改动,你不需要去更改任何上层的代码,也就是说我们Main类中的代码不需要任何改动,你只需要做相应的底层实现即可,这就是一个很简单的架构小例子,利用接口分离实现。除此之外,我们还可以在Api类中根据不同的接口方法调用不同的实现,比如对于登录我想用RESTFUL实现而对于版本更新我想用OkHttp实现等,这些底层的改动都不会影响上层的结构。

你可以把这种结构应用于很多很多地方,相信我熟悉了你会爱上它的,简单但是非常实用,特别是在引入第三方框架的时候,对第三方框架做一层封装能让你的项目结构更灵活。

原文链接:http://aigestudio.com/?p=100