找回密码
 立即注册
首页 业界区 业界 app开发:模拟服务器数据接口 - MockApi

app开发:模拟服务器数据接口 - MockApi

井晶灵 7 天前
为了方便app开发过程中,不受服务器接口的限制,便于客户端功能的快速测试,可以在客户端实现一个模拟服务器数据接口的MockApi模块。本篇文章就尝试为使用gradle的android项目设计实现MockApi。
需求概述

在app开发过程中,在和服务器人员协作时,一般会第一时间确定数据接口的请求参数和返回数据格式,然后服务器人员会尽快提供给客户端可调试的假数据接口。不过有时候就算是假数据接口也来不及提供,或者是接口数据格式来回变动——很可能是客户端展示的原因,这个是产品设计决定的,总之带来的问题就算服务器端的开发进度会影响客户端。
所以,如果可以在客户端的正常项目代码中,自然地(不影响最终apk)添加一种模拟服务器数据返回的功能,这样就可以很方便的在不依赖服务器的情况下展开客户端的开发。而且考虑一种情况,为了测试不同网络速度,网络异常以及服务器错误等各种“可能的真实数据请求的场景”对客户端UI交互的影响,我们往往需要做很多手动测试——千篇一律!如果本地有一种控制这种服务器响应行为的能力那真是太好了。
本文将介绍一种为客户端项目增加模拟数据接口功能的方式,希望能减少一些开发中的烦恼。
设计过程

下面从分层设计、可开关模拟模块、不同网络请求结果的制造这几个方面来阐述下模拟接口模块的设计。
为了表达方便,这里要实现的功能表示为“数据接口模拟模块”,对应英文为MockDataApi,或简写为MockApi,正常的数据接口模块定义为DataApi。
分层思想

说到分层设计,MVC、MVP等模式一定程度上就起到了对代码所属功能的一个划分。分层设计简单的目标就是让项目代码更加清晰,各层相互独立,好处不多说。
移动app的逻辑主要就是交互逻辑,然后需要和服务器沟通数据。所以最简单的情形下可以将一个功能(比如一个列表界面)的实现分UI层和数据访问层。
下面将数据访问层表述为DataApi模块,DataApi层会定义一系列的接口来描述不同类别的数据访问请求。UI层使用这些接口来获取数据,而具体的数据访问实现类就可以在不修改UI层代码的情况下进行替换。
例如,有一个ITaskApi定义了方法List getTasks(),UI层一个界面展示任务列表,那么它使用ITaskApi来获取数据,而具体ITaskApi的实现类可以由DataApi层的一个工厂类DataApiManager来统一提供。
有了上面的分层设计,就可以为UI层动态提供真实数据接口或模拟数据接口。
模拟接口的开关

可能大家都经历过在UI层代码里临时写一些假数据得情况。比如任务列表界面,开发初,可以写一个mockTaskData()方法来返回一个List。但这种代码只能是开发阶段有,最终apk不应该存在。
不能让“模拟数据”的代码到处散乱,在分层设计的方式下,可以将真实的数据接口DataApi和模拟数据接口MockDataApi分别作为两个数据接口的实现模块,这样就可以根据项目的构建类型来动态提供不同的数据接口实现。
实现MockDataApi的动态提供的方法也不止一种。
一般的java项目可以使用“工厂模式+反射”来动态提供不同的接口实现类,再专业点就是依赖注入——DI框架的使用了。
目前gradle是java的最先进的构建工具,它支持根据buildType来分别指定不同的代码资源,或不同的依赖。
可以在一个单独的类库module(就是maven中的项目)中来编写各种MockDataApi的实现类,然后主app module在debug构建时添加对它的依赖,此时数据接口的提供者DataApiManager可以向UI层返回这些mock类型的实例。
为了让“正常逻辑代码”和mock相关代码的关联尽量少,可以提供一个MockApiManager来唯一获取各个MockDataApi的实例。然后在debug构建下的MockApiManager会返回提供了mock实现的数据接口实例,而release构建时MockApiManager会一律返null。
不同请求结果的模拟

MockApi在多次请求时提供不同的网络请求结果,如服务器错误,网络错误,成功等,并模拟出一定的网络延迟,这样就很好的满足了UI层代码的各种测试需求。
为了达到上述目标,定义一个接口IMockApiStrategy来表示对数据请求的响应策略,它定义了方法onResponse(int callCount)。根据当前请求的次数callCount,onResponse()会得到不同的模拟响应结果。很明显,可以根据测试需要提供不同的请求响应策略,比如不断返回成功请求,或者不断返回错误请求,或轮流返回成功和错误等。
关键代码解析

下面就给出各个部分的关键代码,来说明以上所描述的MockDataApi模块的实现。
UI层代码

作为示例,界面MainActivity是一个“任务列表”的展示。任务由Task类表示:
  1. public class Task {
  2.   public String name;
  3. }
复制代码
界面MainActivity使用一个TextView来显示“加载中、任务列表、网络错误”等效果,并提供一个Button来点击刷新数据。代码如下:
  1. public class MainActivity extends Activity {
  2.     private TextView tv_data;
  3.     private boolean requesting = false;
  4.     @Override
  5.     protected void onCreate(Bundle savedInstanceState) {
  6.         super.onCreate(savedInstanceState);
  7.         setContentView(R.layout.activity_main);
  8.         tv_data = (TextView) findViewById(R.id.tv_data);
  9.         getData();
  10.     }
  11.     private void getData() {
  12.         if (requesting) return;
  13.         requesting = true;
  14.         ITaskApi api = DataApiManager.ofTask();
  15.         if (api != null) {
  16.             api.getTasks(new DataApiCallback<List<Task>>() {
  17.                 @Override
  18.                 public void onSuccess(List<Task> data) {
  19.                     // 显示数据
  20.                     StringBuilder sb = new StringBuilder("请求数据成功:\n");
  21.                     for (int i = 0; i < data.size(); i++) {
  22.                         sb.append(data.get(i).name).append("\n");
  23.                     }
  24.                     tv_data.setText(sb.toString());
  25.                     requesting = false;
  26.                 }
  27.                 @Override
  28.                 public void onError(Throwable e) {
  29.                     // 显示错误
  30.                     tv_data.setText("错误:\n" + e.getMessage());
  31.                     requesting = false;
  32.                 }
  33.                 @Override
  34.                 public void onStart() {
  35.                     // 显示loading
  36.                     tv_data.setText("正在加载...");
  37.                 }
  38.             });
  39.         }
  40.     }
  41.     public void onRefreshClick(View view) {
  42.         getData();
  43.     }
  44. }
复制代码
在UI层代码中,使用DataApiManager.ofTask()获得数据访问接口的实例。
考虑到数据请求会是耗时的异步操作,这里每个数据接口方法接收一个DataApiCallback 回调对象,T是将返回的数据类型。
  1. public interface DataApiCallback<T>  {
  2.     void onSuccess(T data);
  3.     void onError(Throwable e);
  4.     void onStart();
  5. }
复制代码
接口DataApiCallback定义了数据接口请求数据开始和结束时的通知。
DataApiManager

根据分层设计,UI层和数据访问层之间的通信就是基于DataApi接口的,每个DataApi接口提供一组相关数据的获取方法。获取Task数据的接口就是ITaskApi:
  1. public interface ITaskApi {
  2.     void getTasks(DataApiCallback<List<Task>> callback);
  3. }
复制代码
UI层通过DataApiManager来获得各个DataApi接口的实例。也就是在这里,会根据当前项目构建是debug还是release来选择性提供MockApi或最终的DataApi。
  1. public class DataApiManager {
  2.     private static final boolean MOCK_ENABLE = BuildConfig.DEBUG;
  3.     public static ITaskApi ofTask() {
  4.         if (MOCK_ENABLE) {
  5.             ITaskApi api = MockApiManager.getMockApi(ITaskApi.class);
  6.             if (api != null) return api;
  7.         }
  8.         return new NetTaskApi();
  9.     }
  10. }
复制代码
当MOCK_ENABLE为true时,会去MockApiManager检索一个所需接口的mock实例,如果没找到,会返回真实的数据接口的实现,上面的NetTaskApi就是。倘若现在服务器还无法进行联合调试,它的实现就简单的返回一个服务器错误:
  1. public class NetTaskApi implements ITaskApi {
  2.     @Override
  3.     public void getTasks(DataApiCallback<List<Task>> callback) {
  4.         // 暂时没用实际的数据接口实现
  5.         callback.onError(new Exception("数据接口未实现"));
  6.     }
  7. }
复制代码
MockApiManager

DataApiManager利用MockApiManager来获取数据接口的mock实例。这样的好处是模拟数据接口的相关类型都被“封闭”起来,仅通过一个唯一类型来获取已知的DataApi的一种(这里就指mock)实例。这样为分离出mock相关代码打下了基础。
在DataApiManager中,获取数据接口实例时会根据开关变量MOCK_ENABLE判断是否可以返回mock实例。仅从功能上看是满足动态提供MockApi的要求了。不过,为了让最终release构建的apk中不包含多余的mock相关的代码,可以利用gradle提供的buildVariant。

  • buildVariant
    使用gradle来构建项目时,可以指定不同的buildType,默认会有debug和release两个“构建类型”。此外,还可以提供productFlavors来提供不同的“产品类型”,如demo版,专业版等。
    每一种productFlavor和一个buildType组成一个buildVariant(构建变种)。
    可以为每一个buildType,buildVariant,或productFlavor指定特定的代码资源。
这里利用buildType来为debug和release构建分别指定不同的MockApiManager类的实现。
默认的项目代码是在src/main/java/目录下,创建目录/src/debug/java/来放置只在debug构建时编译的代码。在/src/release/java/目录下放置只在release构建时编译的代码。

  • debug构建时的MockApiManager
  1. public class MockApiManager {
  2.     private static final MockApiManager INSTANCE = new MockApiManager();
  3.     private HashMap<String, BaseMockApi> mockApis;
  4.     private MockApiManager() {}
  5.     public static <T> T getMockApi(Class<T> dataApiClass) {
  6.         if (dataApiClass == null) return null;
  7.         String key = dataApiClass.getName();
  8.         try {
  9.             T mock = (T) getInstance().mockApis.get(key);
  10.             return mock;
  11.         } catch (Exception e) {
  12.             return null;
  13.         }
  14.     }
  15.     private void initApiTable() {
  16.         mockApis = new HashMap<>();
  17.         mockApis.put(ITaskApi.class.getName(), new MockTaskApi());
  18.     }
  19.     private static MockApiManager getInstance() {
  20.         if (INSTANCE.mockApis == null) {
  21.             synchronized (MockApiManager.class) {
  22.                 if (INSTANCE.mockApis == null) {
  23.                     INSTANCE.initApiTable();
  24.                 }
  25.             }
  26.         }
  27.         return INSTANCE;
  28.     }
  29. }
复制代码
静态方法getMockApi()根据传递的接口类型信息从mockApis中获取可能的mock实例,mockApis中注册了需要mock的那些接口的实现类对象。

  • release构建时的MockApiManager
  1. public class MockApiManager {
  2.     public static <T> T getMockApi(Class<T> dataApiClass) {
  3.         return null;
  4.     }   
  5. }
复制代码
因为最终release构建时是不需要任何mock接口的,所以此时getMockApi()一律返回null。也没有任何和提供mock接口相关的类型。
通过为debug和release构建提供不同的MockApiManager代码,就彻底实现了MockApi代码的动态添加和移除。
MockApi的实现

模拟数据接口的思路非常简单:根据请求的次数callCount,运行一定的策略来不断地返回不同的响应结果。
响应结果包括“网络错误、服务器错误、成功”三种状态,而且还提供一定的网络时间延迟的模拟。
IMockApiStrategy

接口IMockApiStrategy的作用就是抽象对请求返回不同响应结果的策略,响应结果由IMockApiStrategy.Response表示。
  1. public interface IMockApiStrategy {
  2.     void onResponse(int callCount, Response out);
  3.     /**
  4.      * Mock响应返回结果,表示响应的状态
  5.      */
  6.     class Response {
  7.         public static final int STATE_NETWORK_ERROR = 1;
  8.         public static final int STATE_SERVER_ERROR = 2;
  9.         public static final int STATE_SUCCESS = 3;
  10.         public int state = STATE_SUCCESS;
  11.         public int delayMillis = 600;
  12.     }
  13. }
复制代码
Response表示的响应结果包含结果状态和延迟时间。
作为一个默认的实现,WheelApiStrategy类根据请求次数,不断返回上述的三种结果:
  1. public class WheelApiStrategy implements IMockApiStrategy {
  2.     @Override
  3.     public void onResponse(int callCount, Response out) {
  4.         if (out == null) return;
  5.         int step = callCount % 10;
  6.         switch (step) {
  7.             case 0:
  8.             case 1:
  9.             case 2:
  10.             case 3:
  11.                 out.state = Response.STATE_SUCCESS;
  12.                 break;
  13.             case 4:
  14.             case 5:
  15.                 out.state = Response.STATE_SERVER_ERROR;
  16.                 break;
  17.             case 6:
  18.             case 7:
  19.                 out.state = Response.STATE_SUCCESS;
  20.                 break;
  21.             case 8:
  22.             case 9:
  23.                 out.state = Response.STATE_NETWORK_ERROR;
  24.                 break;
  25.         }
  26.         out.delayMillis = 700;
  27.     }
  28. }
复制代码
方法onResponse()的参数out仅仅是为了避免多次创建小对象,对应debug构建,倒也没太大意义。
BaseMockApi

针对每一个数据访问接口,都可以提供一个mock实现。比如为接口ITaskApi提供MockTaskApi实现类。
为了简化代码,抽象基类BaseMockApi完成了大部分公共的逻辑。
[code]public abstract class BaseMockApi {    protected int mCallCount;    private IMockApiStrategy mStrategy;    private Response mResponse = new Response();    public Response onResponse() {        if (mStrategy == null) {            mStrategy = getMockApiStrategy();        }        if (mStrategy != null) {            mStrategy.onResponse(mCallCount, mResponse);            mCallCount++;        }        return mResponse;    }    protected IMockApiStrategy getMockApiStrategy() {        return new WheelApiStrategy();    }    protected void giveErrorResult(final DataApiCallback callback, Response response) {        Action1 onNext = null;        AndroidSchedulers.mainThread().createWorker().schedule(new Action0() {            @Override            public void call() {                callback.onStart();            }        });        switch (response.state) {            case Response.STATE_NETWORK_ERROR:                onNext = new Action1() {                    @Override                    public void call(Object o) {                        callback.onError(new IOException("mock network error."));                    }                };                break;            case Response.STATE_SERVER_ERROR:                onNext = new Action1() {                    @Override                    public void call(Object o) {                        callback.onError(new IOException("mock server error."));                    }                };                break;        }        if (onNext != null) {            Observable.just(10086)                    .delay(response.delayMillis, TimeUnit.MILLISECONDS)                    .subscribeOn(Schedulers.io())                    .observeOn(AndroidSchedulers.mainThread())                    .subscribe(onNext);        }    }     public  void giveSuccessResult(final Func0 dataMethod, final DataApiCallback callback, final Response response) {        AndroidSchedulers.mainThread().createWorker().schedule(new Action0() {            @Override            public void call() {                Observable.create(new Observable.OnSubscribe() {                    @Override                    public void call(Subscriber

相关推荐

您需要登录后才可以回帖 登录 | 立即注册