使用 UT 玩轉 defer 和 retryWhen

《使用 UT 高效地玩轉 RxJava 的操作符》一文中,筆者介紹了一種學習 RxJava 操作符的方式,除了文中提到的操作符之外,還有幾個細節較多,彈珠圖不能?完全詮釋操作符含義的,在這篇文章里繼續來講解。

defer

defer 是創建型的操作符,字面上有「推遲」的意思,推遲創建數據流的規則是:一開始不會馬上創建 Observable,直到有訂閱者訂閱時才會創建,且每次都創建全新的 Observable

上一篇文章一樣,自頂向下來看這張彈珠圖:

  1. 操作符:這個長框內有很多數據流,要表達的含義是:每次都創建全新的數據流 Observable
  2. 輸入:圖中產生了兩條全新的數據流,且發送的數據可能不一樣(彈珠顏色不一樣)
  3. 輸出:創建型的操作符基本上都沒有輸出的圖示,根據對操作符的大概理解,為了驗證輸入,需要訂閱兩次。
  4. 實現思路:defer 在每次產生 Observable 時,都保存起來,最終驗證這些數據流不會相等。代碼如下:
@Test
public void defer1() {

    List<Observable<Integer>> list = new ArrayList<>();

    Observable<Integer> deferObservable = Observable.defer(() -> {
        Observable<Integer> observable = Observable.just(1, 2, 3);
        list.add(observable);
        return observable;
    });

    // 兩次訂閱,每次都將產生全新的Observable
    deferObservable.subscribe();
    deferObservable.subscribe();

    assertNotSame(list.get(0), list.get(1));
}

寫完這個測試用例后,仍然覺得不過癮,雖然驗證了每次都創建全新的數據流 Observable,但是操作符本身所代表的「推遲」的能力尚未體現,我們需要更多的資料來了解這個能力。

官方文章告訴我們可以查閱這篇文章:Deferring Observable code until subscription in RxJava ,國內也有相關的譯文。仔細閱讀發完這篇文章后,筆者用 UT 來表達文中的一些觀點,這個測試用例的思路有以下兩點:

  1. 按照這樣的流程來實現:使用 defer 創建數據流->訂閱一次->改變數據流的數據->再訂閱一次,由于 defer 可以推遲創建數據流,第二次訂閱時創建的數據流與第一次是不一樣的,因此訂閱到數據也將不一樣。
  2. 使用一個普通的創建型操作符,如 just,按照第1點的方式,對比和 defer 的區別。完整的代碼實現如下:
@Test
public void defer2() {

    class Person {
        public String name = "nobody";

        public Observable<String> getJustObservable() {
            //創建的時候便獲取name值
            return Observable.just(name);
        }

        public Observable<String> getDeferObservable() {
            //訂閱的時候才獲取name值
            return Observable.defer(this::getJustObservable);
        }
    }

    Person person = new Person();
    Observable<String> justObservable = person.getJustObservable();
    Observable<String> deferObservable = person.getDeferObservable();

    // 數據改變之前
    justObservable.subscribe(mList::add);
    assertEquals(mList, Collections.singletonList("nobody"));

    mList.clear();
    deferObservable.subscribe(mList::add);
    assertEquals(mList, Collections.singletonList("nobody"));

    person.name = "geniusmart";

    // 數據改變之后
    mList.clear();
    justObservable.subscribe(mList::add);
    assertEquals(mList, Collections.singletonList("nobody"));

    mList.clear();
    deferObservable.subscribe(mList::add);
    assertEquals(mList, Collections.singletonList("geniusmart"));

}

通過這個例子我所要表達的意思是:彈珠圖本身包含了很多細節,有些細節并沒辦法完整詮釋,此時我們可以通過閱讀更多的文章,通過 UT 的形式來驗證觀點,深入學習每一個操作符。

retry

retryretryWhen 是錯誤處理型的操作符,當數據流發送了錯誤的數據時,將根據既定的規則發起重新訂閱。

有了之前的鋪墊,實現這張彈珠圖并不復雜:數據流第一次發送了一個 Error 數據,retry 執行,訂閱者重新發起訂閱,數據流第二次發送正常的數據。具體代碼實現如下:

@Test
public void retry() {

    final Integer[] arrays = {0};

    Observable.create(new Observable.OnSubscribe<Integer>() {
        @Override
        public void call(Subscriber<? super Integer> subscriber) {
            subscriber.onNext(1);
            subscriber.onNext(2);
            subscriber.onNext(3 / arrays[0]++);
            subscriber.onCompleted();
        }
    })
            .retry()
            .subscribe(mList::add);

    assertEquals(mList, Arrays.asList(1, 2, 1, 2, 3));
}

retryWhen

retry 只是小試牛刀,接下來看看 retryWhen

這張圖很難理解,既有錯誤重試,還有延時策略,實在無從下手,我們需要查閱更多的文章,幸運是剛剛 defer 篇的那位作者寫了相關的另外一篇文章 RxJava's repeatWhen and retryWhen, explained,也有相應的譯文 。仔細閱讀之后,梳理下 retryWhen 的套路,當錯誤重試需要延時策略時,實現流程大概是這樣子的:

理清楚這個流程后,實現起來就比較輕松了,代碼如下:

@Test
public void retryWhen_flatMap_timer() {

    Observable.create(subscriber -> {
        System.out.println("subscribing");
        subscriber.onNext(1);
        subscriber.onNext(2);
        subscriber.onError(new RuntimeException("RuntimeException"));
    })
            .retryWhen(observable ->
                    observable.flatMap(
                            (Func1<Throwable, Observable<?>>) throwable ->
                                    //延遲5s重新訂閱
                                    Observable.timer(5, TimeUnit.SECONDS, mTestScheduler)
                    )
            )
            .subscribe(num -> {
                System.out.println(num);
                mList.add(num);
            });

    //時間提前10s,將發生1次訂閱+2次重新訂閱
    mTestScheduler.advanceTimeBy(10, TimeUnit.SECONDS);

    assertEquals(mList, Arrays.asList(1, 2, 1, 2, 1, 2));
}

除此之外,文中還介紹了其他一些經驗之談,如不能破壞數據流,如何實現限制次數的延時錯誤重試等,這里分別用 UT 來實現。

破壞數據流

如果 retryWhen 的輸入 Observable<Throwable> ,被粗暴的直接返回一個普通的數據流,則鏈式結構將被打斷,如下代碼:

@Test
public void retryWhen_break_sequence() {

    // 錯誤的做法:破壞數據流,打斷鏈式結構
    Observable.just(1, 2, 3)
            .retryWhen(throwableObservable -> Observable.just(1, 1, 1))
            .subscribe(mList::add);
    //數據流被打斷,訂閱不到數據
    assertTrue(mList.isEmpty());

    // 正確的做法:至少將throwableObservable作為返回結果,此時的retryWhen()等價于retry()
    Observable.just(1, 2, 3)
            .retryWhen(throwableObservable -> throwableObservable).
            subscribe(mList::add);
    //此處的數據流不會觸發error,因此正常輸出1,2,3的數列
    assertEquals(mList, Arrays.asList(1, 2, 3));
}

限制次數的延時錯誤重試

  1. 當數據流產生錯誤的數據時,會觸發 retryWhen,并輸入 Observable<Throwable> error
  2. Observable<Throwable> errorObservable.range(1, 3)zip 聚合,range 作為創建型的操作符,將產生 1,2,3 的數據流,因此前3次 error 將會正常配對并調用 onCompleted(),不再接收第四次的 error。

具體的代碼實現如下:

@Test
public void retryWhen_zip_range_timer() {

    Observable.create((Subscriber<? super Integer> subscriber) -> {
        System.out.println("subscribing");
        subscriber.onNext(1);
        subscriber.onNext(2);
        subscriber.onError(new RuntimeException("always fails"));
    })
            .retryWhen(observable ->
                    observable.zipWith(
                            Observable.range(1, 3),
                            (Func2<Throwable, Integer, Integer>) (throwable, num) -> num
                    )
                            .flatMap((Func1<Integer, Observable<?>>) num -> {
                                System.out.println("delay retry by " + num + " second(s)");
                                return Observable.timer(num, TimeUnit.SECONDS);
                            }))
            .doOnNext(System.out::println)
            .doOnCompleted(() -> System.out.println("completed"))
            .toBlocking()
            .forEach(mList::add);

    //正常訂閱一次,重新訂閱3次
    assertEquals(mList, Arrays.asList(1, 2, 1, 2, 1, 2, 1, 2));
}

總結

使用 UT 來實現彈珠圖(marble diagrams),過癮而且高效,研究操作符時事半功倍,對于一些彈珠圖無法完整詮釋的,可以多查閱一些文章,并將文中的觀點用 UT 來實現。總而言之,這是一種很好的學習方式,強烈推薦大家使用。

參考文章

http://blog.danlew.net/2015/07/23/deferring-observable-code-until-subscription-in-rxjava/
http://blog.danlew.net/2016/01/25/rxjavas-repeatwhen-and-retrywhen-explained/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • RxJava 博大精深,想要入門和進階,操作符是一個切入點。 所以,我們希望尋找一種可以把操作符寫得比較爽,同時可...
    geniusmart閱讀 6,469評論 3 32
  • 本篇文章介主要紹RxJava中操作符是以函數作為基本單位,與響應式編程作為結合使用的,對什么是操作、操作符都有哪些...
    嘎啦果安卓獸閱讀 2,892評論 0 10
  • 響應式編程簡介 響應式編程是一種基于異步數據流概念的編程模式。數據流就像一條河:它可以被觀測,被過濾,被操作,或者...
    說碼解字閱讀 3,115評論 0 5
  • RxJava正在Android開發者中變的越來越流行。唯一的問題就是上手不容易,尤其是大部分人之前都是使用命令式編...
    劉啟敏閱讀 1,925評論 1 7
  • 作者: maplejaw本篇只解析標準包中的操作符。對于擴展包,由于使用率較低,如有需求,請讀者自行查閱文檔。 創...
    maplejaw_閱讀 45,832評論 8 93