使用 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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • RxJava 博大精深,想要入门和进阶,操作符是一个切入点。 所以,我们希望寻找一种可以把操作符写得比较爽,同时可...
    geniusmart阅读 6,268评论 3 32
  • 本篇文章介主要绍RxJava中操作符是以函数作为基本单位,与响应式编程作为结合使用的,对什么是操作、操作符都有哪些...
    嘎啦果安卓兽阅读 2,780评论 0 10
  • 响应式编程简介 响应式编程是一种基于异步数据流概念的编程模式。数据流就像一条河:它可以被观测,被过滤,被操作,或者...
    长夜西风阅读 3,001评论 0 5
  • RxJava正在Android开发者中变的越来越流行。唯一的问题就是上手不容易,尤其是大部分人之前都是使用命令式编...
    刘启敏阅读 1,801评论 1 7
  • 作者: maplejaw本篇只解析标准包中的操作符。对于扩展包,由于使用率较低,如有需求,请读者自行查阅文档。 创...
    maplejaw_阅读 45,326评论 8 93