zoukankan      html  css  js  c++  java
  • App开发:模拟服务器数据接口

    转载:http://blog.csdn.net/hj7jay/article/details/54860866

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

    需求概述

    在app开发过程中,在和服务器人员协作时,一般会第一时间确定数据接口的请求参数和返回数据格式,然后服务器人员会尽快提供给客户端可调试的假数据接口。不过有时候就算是假数据接口也来不及提供,或者是接口数据格式来回变动——很可能是客户端展示的原因,这个是产品设计决定的,总之带来的问题就算服务器端的开发进度会影响客户端。

    所以,如果可以在客户端的正常项目代码中,自然地(不影响最终apk)添加一种模拟服务器数据返回的功能,这样就可以很方便的在不依赖服务器的情况下展开客户端的开发。而且考虑一种情况,为了测试不同网络速度,网络异常以及服务器错误等各种“可能的真实数据请求的场景”对客户端UI交互的影响,我们往往需要做很多手动测试——千篇一律!如果本地有一种控制这种服务器响应行为的能力那真是太好了。

    本文将介绍一种为客户端项目增加模拟数据接口功能的方式,希望能减少一些开发中的烦恼。

    设计过程

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

    分层思想

    说到分层设计,MVC、MVP等模式一定程度上就起到了对代码所属功能的一个划分。分层设计简单的目标就是让项目代码更加清晰,各层相互独立,好处不多说。

    移动app的逻辑主要就是交互逻辑,然后需要和服务器沟通数据。所以最简单的情形下可以将一个功能(比如一个列表界面)的实现分UI层和数据访问层。

    下面将数据访问层表述为DataApi模块,DataApi层会定义一系列的接口来描述不同类别的数据访问请求。UI层使用这些接口来获取数据,而具体的数据访问实现类就可以在不修改UI层代码的情况下进行替换。

    例如,有一个ITaskApi定义了方法List<Task> getTasks(),UI层一个界面展示任务列表,那么它使用ITaskApi来获取数据,而具体ITaskApi的实现类可以由DataApi层的一个工厂类DataApiManager来统一提供。

    有了上面的分层设计,就可以为UI层动态提供真实数据接口或模拟数据接口。

    模拟接口的开关

    可能大家都经历过在UI层代码里临时写一些假数据得情况。比如任务列表界面,开发初,可以写一个mockTaskData()方法来返回一个List<Task>。但这种代码只能是开发阶段有,最终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类表示:

    [java] view plain copy
     
    1. public class Task {  
    2.   public String name;  
    3. }  

    界面MainActivity使用一个TextView来显示“加载中、任务列表、网络错误”等效果,并提供一个Button来点击刷新数据。代码如下:

    [java] view plain copy
     
    1. public class MainActivity extends Activity {  
    2.     private TextView tv_data;  
    3.     private boolean requesting = false;  
    4.   
    5.     @Override  
    6.     protected void onCreate(Bundle savedInstanceState) {  
    7.         super.onCreate(savedInstanceState);  
    8.         setContentView(R.layout.activity_main);  
    9.   
    10.         tv_data = (TextView) findViewById(R.id.tv_data);  
    11.   
    12.         getData();  
    13.     }  
    14.   
    15.     private void getData() {  
    16.         if (requesting) return;  
    17.         requesting = true;  
    18.   
    19.         ITaskApi api = DataApiManager.ofTask();  
    20.         if (api != null) {  
    21.             api.getTasks(new DataApiCallback<List<Task>>() {  
    22.                 @Override  
    23.                 public void onSuccess(List<Task> data) {  
    24.                     // 显示数据  
    25.                     StringBuilder sb = new StringBuilder("请求数据成功: ");  
    26.                     for (int i = 0; i < data.size(); i++) {  
    27.                         sb.append(data.get(i).name).append(" ");  
    28.                     }  
    29.   
    30.                     tv_data.setText(sb.toString());  
    31.                     requesting = false;  
    32.                 }  
    33.   
    34.                 @Override  
    35.                 public void onError(Throwable e) {  
    36.                     // 显示错误  
    37.                     tv_data.setText("错误: " + e.getMessage());  
    38.                     requesting = false;  
    39.                 }  
    40.   
    41.                 @Override  
    42.                 public void onStart() {  
    43.                     // 显示loading  
    44.                     tv_data.setText("正在加载...");  
    45.                 }  
    46.             });  
    47.         }  
    48.     }  
    49.   
    50.     public void onRefreshClick(View view) {  
    51.         getData();  
    52.     }  
    53. }  

    在UI层代码中,使用DataApiManager.ofTask()获得数据访问接口的实例。
    考虑到数据请求会是耗时的异步操作,这里每个数据接口方法接收一个DataApiCallback<T> 回调对象,T是将返回的数据类型。

    [java] view plain copy
     
    1. public interface DataApiCallback<T>  {  
    2.   
    3.     void onSuccess(T data);  
    4.   
    5.     void onError(Throwable e);  
    6.   
    7.     void onStart();  
    8. }  

    接口DataApiCallback定义了数据接口请求数据开始和结束时的通知。

    DataApiManager

    根据分层设计,UI层和数据访问层之间的通信就是基于DataApi接口的,每个DataApi接口提供一组相关数据的获取方法。获取Task数据的接口就是ITaskApi:

    [java] view plain copy
     
    1. public interface ITaskApi {  
    2.     void getTasks(DataApiCallback<List<Task>> callback);  
    3. }  

    UI层通过DataApiManager来获得各个DataApi接口的实例。也就是在这里,会根据当前项目构建是debug还是release来选择性提供MockApi或最终的DataApi。

    [java] view plain copy
     
    1. public class DataApiManager {  
    2.     private static final boolean MOCK_ENABLE = BuildConfig.DEBUG;  
    3.   
    4.     public static ITaskApi ofTask() {  
    5.         if (MOCK_ENABLE) {  
    6.             ITaskApi api = MockApiManager.getMockApi(ITaskApi.class);  
    7.             if (api != null) return api;  
    8.         }  
    9.   
    10.         return new NetTaskApi();  
    11.     }  
    12. }  

    当MOCK_ENABLE为true时,会去MockApiManager检索一个所需接口的mock实例,如果没找到,会返回真实的数据接口的实现,上面的NetTaskApi就是。倘若现在服务器还无法进行联合调试,它的实现就简单的返回一个服务器错误:

    [java] view plain copy
     
    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
      [java] view plain copy
       
      1. public class MockApiManager {  
      2.     private static final MockApiManager INSTANCE = new MockApiManager();  
      3.     private HashMap<String, BaseMockApi> mockApis;  
      4.   
      5.     private MockApiManager() {}  
      6.   
      7.     public static <T> T getMockApi(Class<T> dataApiClass) {  
      8.         if (dataApiClass == null) return null;  
      9.   
      10.         String key = dataApiClass.getName();  
      11.   
      12.         try {  
      13.             T mock = (T) getInstance().mockApis.get(key);  
      14.             return mock;  
      15.         } catch (Exception e) {  
      16.             return null;  
      17.         }  
      18.     }  
      19.   
      20.     private void initApiTable() {  
      21.         mockApis = new HashMap<>();  
      22.         mockApis.put(ITaskApi.class.getName(), new MockTaskApi());  
      23.     }  
      24.   
      25.     private static MockApiManager getInstance() {  
      26.         if (INSTANCE.mockApis == null) {  
      27.             synchronized (MockApiManager.class) {  
      28.                 if (INSTANCE.mockApis == null) {  
      29.                     INSTANCE.initApiTable();  
      30.                 }  
      31.             }  
      32.         }  
      33.   
      34.         return INSTANCE;  
      35.     }  
      36. }  

    静态方法getMockApi()根据传递的接口类型信息从mockApis中获取可能的mock实例,mockApis中注册了需要mock的那些接口的实现类对象。

    • release构建时的MockApiManager
      [java] view plain copy
       
      1. public class MockApiManager {  
      2.   
      3.     public static <T> T getMockApi(Class<T> dataApiClass) {  
      4.         return null;  
      5.     }     
      6. }  

    因为最终release构建时是不需要任何mock接口的,所以此时getMockApi()一律返回null。也没有任何和提供mock接口相关的类型。

    通过为debug和release构建提供不同的MockApiManager代码,就彻底实现了MockApi代码的动态添加和移除。

    MockApi的实现

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

    IMockApiStrategy

    接口IMockApiStrategy的作用就是抽象对请求返回不同响应结果的策略,响应结果由IMockApiStrategy.Response表示。

    [java] view plain copy
     
    1. public interface IMockApiStrategy {  
    2.     void onResponse(int callCount, Response out);  
    3.   
    4.     /** 
    5.      * Mock响应返回结果,表示响应的状态 
    6.      */  
    7.     class Response {  
    8.         public static final int STATE_NETWORK_ERROR = 1;  
    9.         public static final int STATE_SERVER_ERROR = 2;  
    10.         public static final int STATE_SUCCESS = 3;  
    11.   
    12.         public int state = STATE_SUCCESS;  
    13.         public int delayMillis = 600;  
    14.     }  
    15. }  

    Response表示的响应结果包含结果状态和延迟时间。

    作为一个默认的实现,WheelApiStrategy类根据请求次数,不断返回上述的三种结果:

    [java] view plain copy
     
    1. public class WheelApiStrategy implements IMockApiStrategy {  
    2.   
    3.     @Override  
    4.     public void onResponse(int callCount, Response out) {  
    5.         if (out == null) return;  
    6.   
    7.         int step = callCount % 10;  
    8.   
    9.         switch (step) {  
    10.             case 0:  
    11.             case 1:  
    12.             case 2:  
    13.             case 3:  
    14.                 out.state = Response.STATE_SUCCESS;  
    15.                 break;  
    16.             case 4:  
    17.             case 5:  
    18.                 out.state = Response.STATE_SERVER_ERROR;  
    19.                 break;  
    20.             case 6:  
    21.             case 7:  
    22.                 out.state = Response.STATE_SUCCESS;  
    23.                 break;  
    24.             case 8:  
    25.             case 9:  
    26.                 out.state = Response.STATE_NETWORK_ERROR;  
    27.                 break;  
    28.         }  
    29.   
    30.         out.delayMillis = 700;  
    31.     }  
    32. }  

    方法onResponse()的参数out仅仅是为了避免多次创建小对象,对应debug构建,倒也没太大意义。

    BaseMockApi

    针对每一个数据访问接口,都可以提供一个mock实现。比如为接口ITaskApi提供MockTaskApi实现类。

    为了简化代码,抽象基类BaseMockApi完成了大部分公共的逻辑。

    [java] view plain copy
     
    1. public abstract class BaseMockApi {  
    2.     protected int mCallCount;  
    3.     private IMockApiStrategy mStrategy;  
    4.     private Response mResponse = new Response();  
    5.   
    6.     public Response onResponse() {  
    7.         if (mStrategy == null) {  
    8.             mStrategy = getMockApiStrategy();  
    9.         }  
    10.   
    11.         if (mStrategy != null) {  
    12.             mStrategy.onResponse(mCallCount, mResponse);  
    13.             mCallCount++;  
    14.         }  
    15.   
    16.         return mResponse;  
    17.     }  
    18.   
    19.     protected IMockApiStrategy getMockApiStrategy() {  
    20.         return new WheelApiStrategy();  
    21.     }  
    22.   
    23.     protected void giveErrorResult(final DataApiCallback<?> callback, Response response) {  
    24.         Action1<Object> onNext = null;  
    25.   
    26.         AndroidSchedulers.mainThread().createWorker().schedule(new Action0() {  
    27.             @Override  
    28.             public void call() {  
    29.                 callback.onStart();  
    30.             }  
    31.         });  
    32.   
    33.         switch (response.state) {  
    34.             case Response.STATE_NETWORK_ERROR:  
    35.                 onNext = new Action1<Object>() {  
    36.                     @Override  
    37.                     public void call(Object o) {  
    38.                         callback.onError(new IOException("mock network error."));  
    39.                     }  
    40.                 };  
    41.   
    42.                 break;  
    43.             case Response.STATE_SERVER_ERROR:  
    44.                 onNext = new Action1<Object>() {  
    45.                     @Override  
    46.                     public void call(Object o) {  
    47.                         callback.onError(new IOException("mock server error."));  
    48.                     }  
    49.                 };  
    50.                 break;  
    51.         }  
    52.   
    53.         if (onNext != null) {  
    54.             Observable.just(10086)  
    55.                     .delay(response.delayMillis, TimeUnit.MILLISECONDS)  
    56.                     .subscribeOn(Schedulers.io())  
    57.                     .observeOn(AndroidSchedulers.mainThread())  
    58.                     .subscribe(onNext);  
    59.         }  
    60.     }  
    61.   
    62.      public <T> void giveSuccessResult(final Func0<T> dataMethod, final DataApiCallback<T> callback, final Response response) {  
    63.         AndroidSchedulers.mainThread().createWorker().schedule(new Action0() {  
    64.             @Override  
    65.             public void call() {  
    66.                 Observable.create(new Observable.OnSubscribe<T>() {  
    67.                     @Override  
    68.                     public void call(Subscriber<? super T> subscriber) {  
    69.                         Log.d("MOCK", "onNext Thread = " + Thread.currentThread().getName());  
    70.                         subscriber.onNext(dataMethod.call());  
    71.                         subscriber.onCompleted();  
    72.                     }  
    73.                 }).  
    74.                 delay(response.delayMillis, TimeUnit.MILLISECONDS)  
    75.                 .subscribeOn(Schedulers.io())  
    76.                 .observeOn(AndroidSchedulers.mainThread())  
    77.                 .subscribe(new ApiSubcriber(callback));  
    78.             }  
    79.         });  
    80.     }  
    81.   
    82.     private static class ApiSubcriber<T> extends Subscriber<T> {  
    83.         private DataApiCallback<T> callback;  
    84.   
    85.         public ApiSubcriber(DataApiCallback<T> callback) {  
    86.             this.callback = callback;  
    87.         }  
    88.   
    89.         @Override  
    90.         public void onStart() {  
    91.             callback.onStart();  
    92.         }  
    93.   
    94.         @Override  
    95.         public void onCompleted() {}  
    96.   
    97.         @Override  
    98.         public void onError(Throwable e) {  
    99.             callback.onError(e);  
    100.         }  
    101.   
    102.         @Override  
    103.         public void onNext(T data) {  
    104.             callback.onSuccess(data);  
    105.         }  
    106.     }  
    107. }  

    • onResponse()
      方法onResponse()根据“响应策略”来针对一次请求返回一个“响应结果”,默认的策略由方法getMockApiStrategy()提供,子类可以重写它提供其它策略。当然策略对象本身也可以作为参数传递(此时此方法本身也没多大意义了)。
      一个想法是,每一个MockApi类都只需要一个实例,这样它的callCount就可以在程序运行期间得到保持。此外,大多数情况下策略对象只需要一个就行了——它是无状态的,封装算法的一个“函数对象”,为了多态,没办法让它是静态方法。

    • giveErrorResult()
      此方法用来执行错误回调,此时是不需要数据的,只需要根据response来执行一定的延迟,然后返回网络错误或服务器错误。
      注意一定要在main线程上执行callback的各个方法,这里算是一个约定,方便UI层直接操作一些View对象。

    • giveSuccessResult()
      此方法用来执行成功回调,此时需要提供数据,并执行response中的delayMillis延迟。
      参数dataMethod用来提供需要的假数据,这里保证它的执行在非main线程中。
      同样,callback的方法都在main线程中执行。

    上面BaseMockApi中的rxjava的一些代码都非常简单,完全可以使用Thread来实现。

    提供MockTaskApi

    作为示例,这里为ITaskApi提供了一个mock实现类:

    [java] view plain copy
     
    1. public class MockTaskApi extends BaseMockApi implements ITaskApi {  
    2.   
    3.     @Override  
    4.     public void getTasks(DataApiCallback<List<Task>> callback) {  
    5.         Response response = onResponse();  
    6.   
    7.         if (response.state == Response.STATE_SUCCESS) {  
    8.             Func0<List<Task>> mockTasks = new Func0<List<Task>>() {  
    9.                 @Override  
    10.                 public List<Task> call() {  
    11.                     // here to give some mock data, you can get it from a json file —— if there is.  
    12.                     ArrayList<Task> tasks = new ArrayList<>();  
    13.                     int start = (mCallCount - 1) * 6;  
    14.                     for (int i = start; i < start + 6; i++) {  
    15.                         Task task = new Task();  
    16.                         task.name = "Task - " + i;  
    17.   
    18.                         tasks.add(task);  
    19.                     }  
    20.   
    21.                     return tasks;  
    22.                 }  
    23.             };  
    24.   
    25.             giveSuccessResult(mockTasks, callback, response);  
    26.         } else {  
    27.   
    28.             giveErrorResult(callback, response);  
    29.         }  
    30.     }  
    31. }  

    它的代码几乎不用过多解释,使用代码提供需要的返回数据是非常简单的——就像你直接在UI层的Activity中写一个方法来造假数据那样。

    小结

    无论如何,经过上面的一系列的努力,模拟数据接口的代码已经稍具模块性质了,它可以被动态的开关,不影响最终的release构建,可以为需要测试的数据接口灵活的提供想要的mock实现。

    很值得一提的是,整个MockApi模块都是建立在纯java代码上的。这样从UI层请求到数据访问方法的执行,都最终是直接的java方法的调用,这样可以很容易获取调用传递的“请求参数”,这些参数都是java类。而如果mock是建立在网络框架之上的,那么额外的http报文的解析是必不可少的。
    仅仅是为了测试的目的,分层设计,让数据访问层可以在真实接口和mock接口间切换,更简单直接些。

    最后,造假数据当然也可以是直接读取json文件这样的方式来完成,如果服务器开发人员有提供这样的文件的话。

     
     
  • 相关阅读:
    查漏补缺:2020年搞定SpringCloud面试(含答案和思维导图)
    如何在半小时搭建一个简单的日志分析平台?
    Flutter | 状态管理特别篇——Provide
    线程池是怎样工作的
    神奇的 SQL 之 联表细节 → MySQL JOIN 的执行过程
    github设置添加ssh
    pytorch中torch.cat(),torch.chunk(),torch.split()函数的使用方法
    八年以后,我选择了创业
    vue源码解读(一)Observer/Dep/Watcher是如何实现数据绑定的
    Ubuntu18.04安装Pytorch
  • 原文地址:https://www.cnblogs.com/ceshi2016/p/7884703.html
Copyright © 2011-2022 走看看