在《使用 UT 高效地玩轉 RxJava 的操作符》一文中,筆者介紹了一種學習 RxJava 操作符的方式,除了文中提到的操作符之外,還有幾個細節較多,彈珠圖不能?完全詮釋操作符含義的,在這篇文章里繼續來講解。
defer
defer
是創建型的操作符,字面上有「推遲」的意思,推遲創建數據流的規則是:一開始不會馬上創建 Observable
,直到有訂閱者訂閱時才會創建,且每次都創建全新的 Observable
。
跟上一篇文章一樣,自頂向下來看這張彈珠圖:
- 操作符:這個長框內有很多數據流,要表達的含義是:每次都創建全新的數據流
Observable
。 - 輸入:圖中產生了兩條全新的數據流,且發送的數據可能不一樣(彈珠顏色不一樣)
- 輸出:創建型的操作符基本上都沒有輸出的圖示,根據對操作符的大概理解,為了驗證輸入,需要訂閱兩次。
- 實現思路:
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 來表達文中的一些觀點,這個測試用例的思路有以下兩點:
- 按照這樣的流程來實現:使用
defer
創建數據流->訂閱一次->改變數據流的數據->再訂閱一次,由于defer
可以推遲創建數據流,第二次訂閱時創建的數據流與第一次是不一樣的,因此訂閱到數據也將不一樣。 - 使用一個普通的創建型操作符,如
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
retry
和 retryWhen
是錯誤處理型的操作符,當數據流發送了錯誤的數據時,將根據既定的規則發起重新訂閱。
有了之前的鋪墊,實現這張彈珠圖并不復雜:數據流第一次發送了一個 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));
}
限制次數的延時錯誤重試
- 當數據流產生錯誤的數據時,會觸發
retryWhen
,并輸入Observable<Throwable> error
。 - 將
Observable<Throwable> error
與Observable.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/