App開發:模擬服務器數據接口 - MockApi

爲了方便app開發過程當中,不受服務器接口的限制,便於客戶端功能的快速測試,能夠在客戶端實現一個模擬服務器數據接口的MockApi模塊。本篇文章就嘗試爲使用gradle的android項目設計實現MockApi。java

需求概述

在app開發過程當中,在和服務器人員協做時,通常會第一時間肯定數據接口的請求參數和返回數據格式,而後服務器人員會盡快提供給客戶端可調試的假數據接口。不過有時候就算是假數據接口也來不及提供,或者是接口數據格式來回變更——極可能是客戶端展現的緣由,這個是產品設計決定的,總之帶來的問題就算服務器端的開發進度會影響客戶端。android

因此,若是能夠在客戶端的正常項目代碼中,天然地(不影響最終apk)添加一種模擬服務器數據返回的功能,這樣就能夠很方便的在不依賴服務器的狀況下展開客戶端的開發。並且考慮一種狀況,爲了測試不一樣網絡速度,網絡異常以及服務器錯誤等各類「可能的真實數據請求的場景」對客戶端UI交互的影響,咱們每每須要作不少手動測試——千篇一概!若是本地有一種控制這種服務器響應行爲的能力那真是太好了。git

本文將介紹一種爲客戶端項目增長模擬數據接口功能的方式,但願能減小一些開發中的煩惱。github

設計過程

下面從分層設計、可開關模擬模塊、不一樣網絡請求結果的製造這幾個方面來闡述下模擬接口模塊的設計。
爲了表達方便,這裏要實現的功能表示爲「數據接口模擬模塊」,對應英文爲MockDataApi,或簡寫爲MockApi,正常的數據接口模塊定義爲DataApi。算法

分層思想

說到分層設計,MVC、MVP等模式必定程度上就起到了對代碼所屬功能的一個劃分。分層設計簡單的目標就是讓項目代碼更加清晰,各層相互獨立,好處很少說。json

移動app的邏輯主要就是交互邏輯,而後須要和服務器溝通數據。因此最簡單的情形下能夠將一個功能(好比一個列表界面)的實現分UI層和數據訪問層。api

下面將數據訪問層表述爲DataApi模塊,DataApi層會定義一系列的接口來描述不一樣類別的數據訪問請求。UI層使用這些接口來獲取數據,而具體的數據訪問實現類就能夠在不修改UI層代碼的狀況下進行替換。服務器

例如,有一個ITaskApi定義了方法List<Task> getTasks(),UI層一個界面展現任務列表,那麼它使用ITaskApi來獲取數據,而具體ITaskApi的實現類能夠由DataApi層的一個工廠類DataApiManager來統一提供。網絡

有了上面的分層設計,就能夠爲UI層動態提供真實數據接口或模擬數據接口。app

模擬接口的開關

可能你們都經歷過在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類表示:

public class Task {
  public String name;
}

界面MainActivity使用一個TextView來顯示「加載中、任務列表、網絡錯誤」等效果,並提供一個Button來點擊刷新數據。代碼以下:

public class MainActivity extends Activity {
    private TextView tv_data;
    private boolean requesting = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv_data = (TextView) findViewById(R.id.tv_data);

        getData();
    }

    private void getData() {
        if (requesting) return;
        requesting = true;

        ITaskApi api = DataApiManager.ofTask();
        if (api != null) {
            api.getTasks(new DataApiCallback<List<Task>>() {
                @Override
                public void onSuccess(List<Task> data) {
                    // 顯示數據
                    StringBuilder sb = new StringBuilder("請求數據成功:\n");
                    for (int i = 0; i < data.size(); i++) {
                        sb.append(data.get(i).name).append("\n");
                    }

                    tv_data.setText(sb.toString());
                    requesting = false;
                }

                @Override
                public void onError(Throwable e) {
                    // 顯示錯誤
                    tv_data.setText("錯誤:\n" + e.getMessage());
                    requesting = false;
                }

                @Override
                public void onStart() {
                    // 顯示loading
                    tv_data.setText("正在加載...");
                }
            });
        }
    }

    public void onRefreshClick(View view) {
        getData();
    }
}

在UI層代碼中,使用DataApiManager.ofTask()得到數據訪問接口的實例。
考慮到數據請求會是耗時的異步操做,這裏每一個數據接口方法接收一個DataApiCallback<T> 回調對象,T是將返回的數據類型。

public interface DataApiCallback<T>  {

    void onSuccess(T data);

    void onError(Throwable e);

    void onStart();
}

接口DataApiCallback定義了數據接口請求數據開始和結束時的通知。

DataApiManager

根據分層設計,UI層和數據訪問層之間的通訊就是基於DataApi接口的,每一個DataApi接口提供一組相關數據的獲取方法。獲取Task數據的接口就是ITaskApi:

public interface ITaskApi {
    void getTasks(DataApiCallback<List<Task>> callback);
}

UI層經過DataApiManager來得到各個DataApi接口的實例。也就是在這裏,會根據當前項目構建是debug仍是release來選擇性提供MockApi或最終的DataApi。

public class DataApiManager {
    private static final boolean MOCK_ENABLE = BuildConfig.DEBUG;

    public static ITaskApi ofTask() {
        if (MOCK_ENABLE) {
            ITaskApi api = MockApiManager.getMockApi(ITaskApi.class);
            if (api != null) return api;
        }

        return new NetTaskApi();
    }
}

當MOCK_ENABLE爲true時,會去MockApiManager檢索一個所需接口的mock實例,若是沒找到,會返回真實的數據接口的實現,上面的NetTaskApi就是。假若如今服務器還沒法進行聯合調試,它的實現就簡單的返回一個服務器錯誤:

public class NetTaskApi implements ITaskApi {
    @Override
    public void getTasks(DataApiCallback<List<Task>> callback) {
        // 暫時沒用實際的數據接口實現
        callback.onError(new Exception("數據接口未實現"));
    }
}

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
public class MockApiManager {
    private static final MockApiManager INSTANCE = new MockApiManager();
    private HashMap<String, BaseMockApi> mockApis;

    private MockApiManager() {}

    public static <T> T getMockApi(Class<T> dataApiClass) {
        if (dataApiClass == null) return null;

        String key = dataApiClass.getName();

        try {
            T mock = (T) getInstance().mockApis.get(key);
            return mock;
        } catch (Exception e) {
            return null;
        }
    }

    private void initApiTable() {
        mockApis = new HashMap<>();
        mockApis.put(ITaskApi.class.getName(), new MockTaskApi());
    }

    private static MockApiManager getInstance() {
        if (INSTANCE.mockApis == null) {
            synchronized (MockApiManager.class) {
                if (INSTANCE.mockApis == null) {
                    INSTANCE.initApiTable();
                }
            }
        }

        return INSTANCE;
    }
}

靜態方法getMockApi()根據傳遞的接口類型信息從mockApis中獲取可能的mock實例,mockApis中註冊了須要mock的那些接口的實現類對象。

  • release構建時的MockApiManager
public class MockApiManager {

    public static <T> T getMockApi(Class<T> dataApiClass) {
        return null;
    }   
}

由於最終release構建時是不須要任何mock接口的,因此此時getMockApi()一概返回null。也沒有任何和提供mock接口相關的類型。

經過爲debug和release構建提供不一樣的MockApiManager代碼,就完全實現了MockApi代碼的動態添加和移除。

MockApi的實現

模擬數據接口的思路很是簡單:根據請求的次數callCount,運行必定的策略來不斷地返回不一樣的響應結果。
響應結果包括「網絡錯誤、服務器錯誤、成功」三種狀態,並且還提供必定的網絡時間延遲的模擬。

IMockApiStrategy

接口IMockApiStrategy的做用就是抽象對請求返回不一樣響應結果的策略,響應結果由IMockApiStrategy.Response表示。

public interface IMockApiStrategy {
    void onResponse(int callCount, Response out);

    /**
     * Mock響應返回結果,表示響應的狀態
     */
    class Response {
        public static final int STATE_NETWORK_ERROR = 1;
        public static final int STATE_SERVER_ERROR = 2;
        public static final int STATE_SUCCESS = 3;

        public int state = STATE_SUCCESS;
        public int delayMillis = 600;
    }
}

Response表示的響應結果包含結果狀態和延遲時間。

做爲一個默認的實現,WheelApiStrategy類根據請求次數,不斷返回上述的三種結果:

public class WheelApiStrategy implements IMockApiStrategy {

    @Override
    public void onResponse(int callCount, Response out) {
        if (out == null) return;

        int step = callCount % 10;

        switch (step) {
            case 0:
            case 1:
            case 2:
            case 3:
                out.state = Response.STATE_SUCCESS;
                break;
            case 4:
            case 5:
                out.state = Response.STATE_SERVER_ERROR;
                break;
            case 6:
            case 7:
                out.state = Response.STATE_SUCCESS;
                break;
            case 8:
            case 9:
                out.state = Response.STATE_NETWORK_ERROR;
                break;
        }

        out.delayMillis = 700;
    }
}

方法onResponse()的參數out僅僅是爲了不屢次建立小對象,對應debug構建,倒也沒太大意義。

BaseMockApi

針對每個數據訪問接口,均可以提供一個mock實現。好比爲接口ITaskApi提供MockTaskApi實現類。

爲了簡化代碼,抽象基類BaseMockApi完成了大部分公共的邏輯。

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<Object> 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<Object>() {
                    @Override
                    public void call(Object o) {
                        callback.onError(new IOException("mock network error."));
                    }
                };

                break;
            case Response.STATE_SERVER_ERROR:
                onNext = new Action1<Object>() {
                    @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 <T> void giveSuccessResult(final Func0<T> dataMethod, final DataApiCallback<T> callback, final Response response) {
        AndroidSchedulers.mainThread().createWorker().schedule(new Action0() {
            @Override
            public void call() {
                Observable.create(new Observable.OnSubscribe<T>() {
                    @Override
                    public void call(Subscriber<? super T> subscriber) {
                        Log.d("MOCK", "onNext Thread = " + Thread.currentThread().getName());
                        subscriber.onNext(dataMethod.call());
                        subscriber.onCompleted();
                    }
                }).
                delay(response.delayMillis, TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new ApiSubcriber(callback));
            }
        });
    }

    private static class ApiSubcriber<T> extends Subscriber<T> {
        private DataApiCallback<T> callback;

        public ApiSubcriber(DataApiCallback<T> callback) {
            this.callback = callback;
        }

        @Override
        public void onStart() {
            callback.onStart();
        }

        @Override
        public void onCompleted() {}

        @Override
        public void onError(Throwable e) {
            callback.onError(e);
        }

        @Override
        public void onNext(T data) {
            callback.onSuccess(data);
        }
    }
}
  • onResponse()
    方法onResponse()根據「響應策略」來針對一次請求返回一個「響應結果」,默認的策略由方法getMockApiStrategy()提供,子類能夠重寫它提供其它策略。固然策略對象自己也能夠做爲參數傳遞(此時此方法自己也沒多大意義了)。
    一個想法是,每個MockApi類都只須要一個實例,這樣它的callCount就能夠在程序運行期間獲得保持。此外,大多數狀況下策略對象只須要一個就好了——它是無狀態的,封裝算法的一個「函數對象」,爲了多態,沒辦法讓它是靜態方法。

  • giveErrorResult()
    此方法用來執行錯誤回調,此時是不須要數據的,只須要根據response來執行必定的延遲,而後返回網絡錯誤或服務器錯誤。
    注意必定要在main線程上執行callback的各個方法,這裏算是一個約定,方便UI層直接操做一些View對象。

  • giveSuccessResult()
    此方法用來執行成功回調,此時須要提供數據,並執行response中的delayMillis延遲。
    參數dataMethod用來提供須要的假數據,這裏保證它的執行在非main線程中。
    一樣,callback的方法都在main線程中執行。

上面BaseMockApi中的rxjava的一些代碼都很是簡單,徹底可使用Thread來實現。

提供MockTaskApi

做爲示例,這裏爲ITaskApi提供了一個mock實現類:

public class MockTaskApi extends BaseMockApi implements ITaskApi {

    @Override
    public void getTasks(DataApiCallback<List<Task>> callback) {
        Response response = onResponse();

        if (response.state == Response.STATE_SUCCESS) {
            Func0<List<Task>> mockTasks = new Func0<List<Task>>() {
                @Override
                public List<Task> call() {
                    // here to give some mock data, you can get it from a json file —— if there is.
                    ArrayList<Task> tasks = new ArrayList<>();
                    int start = (mCallCount - 1) * 6;
                    for (int i = start; i < start + 6; i++) {
                        Task task = new Task();
                        task.name = "Task - " + i;

                        tasks.add(task);
                    }

                    return tasks;
                }
            };

            giveSuccessResult(mockTasks, callback, response);
        } else {

            giveErrorResult(callback, response);
        }
    }
}

它的代碼幾乎不用過多解釋,使用代碼提供須要的返回數據是很是簡單的——就像你直接在UI層的Activity中寫一個方法來造假數據那樣。

小結

不管如何,通過上面的一系列的努力,模擬數據接口的代碼已經稍具模塊性質了,它能夠被動態的開關,不影響最終的release構建,能夠爲須要測試的數據接口靈活的提供想要的mock實現。

很值得一提的是,整個MockApi模塊都是創建在純java代碼上的。這樣從UI層請求到數據訪問方法的執行,都最終是直接的java方法的調用,這樣能夠很容易獲取調用傳遞的「請求參數」,這些參數都是java類。而若是mock是創建在網絡框架之上的,那麼額外的http報文的解析是必不可少的。
僅僅是爲了測試的目的,分層設計,讓數據訪問層能夠在真實接口和mock接口間切換,更簡單直接些。

最後,造假數據固然也能夠是直接讀取json文件這樣的方式來完成,若是服務器開發人員有提供這樣的文件的話。

示例源碼

以上所述代碼能夠在這裏獲取到:
https://github.com/everhad/AndroidMockApi

若是你的項目裏有模擬服務器接口這樣的須要,try it out!

(本文使用Atom編寫)

相關文章
相關標籤/搜索