RxJava 沉思录(四):总结

本文是 "RxJava 沉思录" 系列的最后一篇分享。本系列全部分享:java

咱们在本系列开篇中,曾经留了一个问题:RxJava 是否可让咱们的代码更简洁?做为本系列的最后一篇分享,咱们将详细地探讨这个问题。承接前面两篇 “时间维度” 和 “空间维度” 的探讨,咱们首先从 RxJava 的维度 开始提及。git

RxJava 的维度

在前面两篇分享中,咱们解读了不少案例,最终得出结论:RxJava 经过 Observable 这个统一的接口,对其相关的事件,在空间维度和事件维度进行从新组织,来简化咱们平常的事件驱动编程github

前文中提到:编程

有了 Observable 之后的 RxJava 才刚刚插上了想象力的翅膀。api

RxJava 全部想象力的基石和源泉在于 Observable 这个统一的接口,有了它,配合咱们各类各样的操做符,才能够在时间空间维度玩出花样。网络

咱们回想一下原先咱们基于 Callback 的编程范式:数据结构

btn.setOnClickListener(v -> {
    // handle click event
})
复制代码

在基于 Callback 的编程范式中,咱们的 Callback没有维度 的。它只可以 响应孤立的事件,即来一个事件,我处理一个事件。假设同一个事件先后存在依赖关系,或者不一样事件之间存在依赖关系,不管是时间维度仍是空间维度,若是咱们仍是继续用 Callback 的方式处理,咱们必然须要新增许多额外的数据结构来保存中间的上下文信息,同时 Callback 自己的逻辑也须要修改,观察者的逻辑会变得不那么纯粹。架构

可是 RxJava 给咱们的事件驱动型编程带来了新的思路,RxJava 的 Observable 一会儿把咱们的维度拓展到了时间和空间两个维度。若是事件与事件间存在依赖关系,原先咱们须要新增的数据结构以及在 Callback 内写的额外的控制逻辑的代码,如今均可以不用写,咱们只须要利用 Observable 的操做符对事件在时间和空间维度进行从新组织,就能够实现同样的效果,而观察者的逻辑几乎不须要修改。app

因此若是把 RxJava 的编程思想和传统的面向 Callback 的编程思想进行对比,用一个词形容的话,那就是 降维打击框架

这是我认为目前大多数与 RxJava 有关的技术分享没有提到的一个很是重要的点,而且我认为这才是 RxJava 最精髓最核心的思想。RxJava 对咱们平常编程最重要的贡献,就是提高了咱们原先对于事件驱动型编程的思考的维度,给人一种大梦初醒的感受,和这点比起来,所谓的 “链式写法” 这种语法糖什么的,根本不值一提。

生产者消费者模式中 RxJava 扮演的角色

不管是同步仍是异步,咱们平常的事件驱动型编程能够被当作是一种 “生产者——消费者” 模型:

Callback

在异步的状况下,咱们的代码能够被分为两大块,一块生产事件,一块消费事件,二者经过 Callback 联系起来。而 Callback 是轻量级的,大多数和 Callback 相关的逻辑就仅仅是设置回调和取消设置的回调而已。

若是咱们的项目中引入了 RxJava ,咱们能够发现,“生产者——消费者” 这个模型中,中间多了一层 RxJava 相关的逻辑层:

RxJava

而这一层的做用,咱们在以前的讨论中已经明确,是用来对生产者产生的事件进行从新组织的。这个架构之下,生产者这一层的变化不会很大,直接受影响的是消费者这一层,因为 RxJava 这一层对事件进行了“预处理”,消费者这一层代码会比以前轻不少。同时因为 RxJava 取代了原先的 Callback 这一层,RxJava 这一层的代码是会比原先 Callback 这一层更厚。

这么作还会有什么其余的好处呢?首先最直接的好处即是代码会更易于测试。原先生产者和消费者之间是耦合的,因为如今引入了 RxJava,生产者和消费者之间没有直接的耦合关系,测试的时候能够很方便的对生产者和消费者分开进行测试。好比原先网络请求相关逻辑,测试就不是很方便,可是若是咱们使用 RxJava 进行解耦之后,观察者仅仅只是耦合 Observable 这个接口而已,咱们能够本身手动建立用于测试的 Observable,这些 Observable 负责发射 Mock 的数据,这样就能够很方便的对观察者的代码进行测试,而不须要真正的去发起网络请求。

取消订阅与 Scheduler

取消订阅这个功能也是咱们在观察者模式中常常用到的一个功能点,尤为是在 Android 开发领域,因为 Activity 生命周期的关系,咱们常常须要将网络请求与 Activity 生命周期绑定,即在 Activity 销毁的时候取消全部未完成的网络请求。

常规面向 Callback 的编程方式咱们没法在观察者这一层完成取消订阅这一逻辑,咱们经常须要找到事件生产者这一层才能完成取消订阅。例如咱们须要取消点击事件的订阅时,咱们不得不找到点击事件产生的源头,来取消订阅:

btn.setOnClickListener(null);
复制代码

然而在 RxJava 的世界里,取消订阅这个逻辑终于下放到观察者这一层了。事件的生产者须要在提供 Observable 的同时,实现当它的观察者取消订阅时,它应该实现的逻辑(例如释放资源);事件的观察者当订阅一个 Observable 时,它同时会获得一个 Disposable ,观察者但愿取消订阅事件的时候,只须要经过这个接口通知事件生产者便可,彻底不须要了解事件是如何产生的、事件的源头在哪里。

至此,生产者和消费者在 RxJava 的世界里已经完成了完全的解耦。除此之外,RxJava 还提供了好用的线程池,在 生产者——消费者 这个模型里,咱们经常会要求二者工做在不一样的线程中,切换线程是刚需,RxJava 彻底考虑到了这一点,而且把切换线程的功能封装成了 subscribeOnobserverOn 两个操做符,咱们能够在事件流处理的任什么时候机随意切换线程,鉴于这一块已经有不少资料了,这里再也不详细展开。

面向 Observable 的 AOP:compose 操做符

这一块不属于 RxJava 的核心 Feature,可是若是掌握好这块,可让咱们使用 RxJava 编程效率大大提高。

咱们举一个实际的例子,Activity 内发起的网络请求都须要绑定生命周期,即咱们须要在 Activity 销毁的时候取消订阅全部未完成的网络请求。假设我目前已经能够得到一个 Observable<ActivityEvent>, 这是一个能接收到 Activity 生命周期的 Observable(获取方法能够借鉴三方框架 RxLifecycle,或者本身内建一个不可见 Fragment,用来接收生命周期的回调)。

那么用来保证每个网络请求都能绑定 Activity 生命周期的代码应以下所示:

public interface NetworkApi {
    @GET("/path/to/api")
    Call<List<Photo>> getAllPhotos();
}

public class MainActivity extends Activity {

    Observable<ActivityEvent> lifecycle = ...
    NetworkApi networkApi = ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // 发起请求同时绑定生命周期
        networkApi.getAllPhotos()
            .compose(bindToLifecycle())
            .subscribe(result -> {
                // handle results
            });
    }

    private <T> ObservableTransformer<T, T> bindToLifecycle() {
        return upstream -> upstream.takeUntil(
            lifecycle.filter(ActivityEvent.DESTROY::equals)
        );
    }
}
复制代码

若是您以前没有接触过 ObservableTransformer, 这里作一个简单介绍,它一般和 compose 操做符一块儿使用,用来把一个 Observable 进行加工、修饰,甚至替换为另外一个 Observable

在这里咱们封装了一个 bindToLifecycle 方法,它的返回类型是 ObservableTransformer,在 ObservableTransformer 内部,咱们修饰了原 Observable, 使其能够在接收到 Activity 的 DESTROY 事件的时候自动取消订阅,这个逻辑是由 takeUntil 这个操做符完成的。其实咱们能够把这个 bindToLifecycle 方法抽取出来,放到公共的工具类,这样任何的 Activity 内部发起的网络请求时,都只须要加一行 .compose(bindToLifecycle()) 就能够保证绑定生命周期了,今后不再必担忧因为网络请求引发的内存泄漏和崩溃了。

事实上咱们还能够有更多玩法, 上面 ObservableTransformer 内部的 upstream 对象,就是一个 Observable,也就是说能够调用它的 doOnSubscribedoOnTerminate 方法,咱们能够在这两个方法里实现 Loading 动画的显隐:

private <T> ObservableTransformer<T, T> applyLoading() {
    return upstream -> upstream
        .doOnSubscribe(() -> {
            loading.show();
        })
        .doOnTerminae(() -> {
            loading.dismiss();
        });    
    );
}
复制代码

这样,咱们的网络请求只要调用两个 compose 操做符,就能够完成生命周期的绑定以及与之对应的 Loading 动画的显隐了:

networkApi.getAllPhotos()
    .compose(bindToLifecycle())
    .compose(applyLoading())
    .subscribe(result -> {
        // handle results
    });
复制代码

操做符 compose 是 RxJava 给咱们提供的能够面向 Observable 进行 AOP 的接口,善加利用就能够帮咱们节省大量的时间和精力。

RxJava 真的让你的代码更简洁?

在前文中,咱们还留了一个问题还没有解答:RxJava 真的更简洁吗?本文中列举了不少实际的例子,咱们也看到了,从代码量看,有时候使用 RxJava 的版本比 Callback 的版本更少,有时候二者差很少,有时候 Callback 版本的代码反而更少。因此咱们可能没法从代码量上对二者作出公正的考量,因此咱们须要从其余方面,例如代码的阅读难度、可维护性上去评判了。

首先我想要明确一点,RxJava 是一个 “夹带了私货” 的框架,它自己最重要的贡献是提高了咱们思考事件驱动型编程的维度,可是它与此同时又逼迫咱们去接受了函数式编程。函数式编程在处理集合、列表这些数据结构时相比较指令式编程具备先天的优点,我理解框架的设计者,因为框架自己提高了咱们对事件思考的维度,那么不管是时间维度仍是空间维度,一连串发射出来的事件其实就能够被当作许许多多事件的集合,既然是集合,那确定是使用函数式的风格去处理更加优雅。

原先的时候,咱们接触的函数式编程只是用于处理静态的数据,当咱们接触了 RxJava 以后,发现动态的异步事件组成的集合竟然也可使用函数式编程的方式去处理,我不禁地佩服框架设计者的脑洞大开。事实上,RxJava 不少操做符都是直接照搬函数式编程中处理集合的函数,例如:map, filter, flatMap, reduce 等等。

可是,函数式编程是一把双刃剑,它也会给你带来不利的因素,一方面,这意味着你的团队都须要了解函数式编程的思想,另外一方面,函数式的编程风格,意味着代码会比原先更加抽象。

好比在前面的分享中 “实现一个具备多种类型的 RecyclerView” 这个例子中, combineLatest 这个操做符,完成了原先 onOk() 方法、resultTypesresponseList 一块儿配合才完成的任务。虽然原先的版本代码不够内聚,不如 RxJava 版本的简练,可是若是从可阅读性和可维护性上来看,我认为原先的版本更好,由于我看到这几个方法和字段,能够推测出这段代码的意图是什么,但是若是是 combineLatest 这个操做符,也许我写的那个时候我知道我是什么意图,一旦过一段时间回来看,我对着这个这个 combineLatest 操做符可能就一脸懵逼了,我必须从这个事件流最开始的地方从上往下捋一遍,结合实际的业务逻辑,我才能回想起为何当时要用 combineLatest 这个操做符了。

再举一个例子,在 “社交软件上消息的点赞与取消点赞” 这个例子中,若是我不是对这种“把事件流中相邻事件进行比较”的编码方式了如指掌的话,一旦隔一段时间,我再次面对这几个 debouncezipWithflatMap 操做符时,我可能会怀疑本身写的代码。本身写的代码都如此,更况且大多数状况下咱们须要面对别人写的代码。

这就是为何 RxJava 写出的代码会更加抽象,由于 RxJava 的操做符是咱们平时处理业务逻辑时经常使用方法的高度抽象combineLatest 是对咱们本身写的 onOk 等方法的抽象,zipWith 帮咱们省略了原本要写的中间变量,debounce 操做符替代了咱们原本要写的计时器逻辑。从功能上来说二者实际上是等价的,只不过 RxJava 给咱们提供了高度抽象凝练,更加具备普适性的写法。

在本文前半部分,咱们说到过,有的人认为 RxJava 是简洁的,而有的人的见解则彻底相反,这件事的本质在于你们对 简洁 的指望不一样,大多数人认为的简洁指得是代码简单好理解,而高度抽象的代码是不知足这一点的,因此不少人最后发现理解抽象的 RxJava 代码须要花更多的时间,反而不 “简洁” 。认为 RxJava 简洁的人所认为的 简洁 更像是那种相似数学概念上的那种 简洁,这是由于函数式编程的抽象风格与数学更接近。咱们举个例子,你们都知道牛顿第二定律,但是你知道牛顿在《天然哲学的数学原理》上发表牛顿二定律的时候的原始公式表示是什么样的吗:

Newton's second law

公式中的 p 表示动量,这是牛顿所认为的"简洁",而咱们大多数人认为简单好记的版本是 “物体的加速度等于施加在物体上的力除以物体的质量”。

这就是为何,我在前面提早下了那个结论:对于大多数人,RxJava 不等于简洁,有时候甚至是更难以理解的代码以及更低的项目可维护性。

而目前大多数我看到的有关 RxJava 的技术文章举例说明的所谓 “逻辑简洁” 或者是 “随着程序逻辑的复杂性提升,依然可以保持简洁” 的例子大多数都是不恰当的。一方面他们仅仅停留在 Callback 的维度,举那种依次执行的异步任务的例子,彻底没有点到 RxJava 对处理问题的维度的提高这一点;二是举的那些例子实在难以使人信服,至少我并无以为那些例子用了 RxJava 相比 Callback 有多么大的提高。

RxJava 是否适合你的项目

综上所述,咱们能够得出这样的结论,RxJava 是一个思想优秀的框架,并且是那种在工程领域少见的带有学院派气息和理想主义色彩的框架,他是一种新型的事件驱动型编程范式。 RxJava 最重要的贡献,就是提高了咱们原先对于事件驱动型编程的思考的维度,容许咱们能够从时间和空间两个维度去从新组织事件。

此外,RxJava 好在哪,真的和“观察者模式”、“链式编程”、“线程池”、“解决 Callback Hell”等等关系没那么大,这些特性相比上面总结的而言,都是微不足道的。

我是不会用“简洁”、“逻辑简洁”、“清晰”、“优雅” 那样空洞的字眼去描述 RxJava 这个框架的,这确实是一个学习曲线陡峭的框架,并且若是团队成员总体对函数式编程认识不够深入的话,项目的后期维护也是充满风险的。

固然我但愿你也不要所以被我吓到,我我的是推崇 RxJava 的,在我本人参与的项目中已经大规模铺开使用了 RxJava。本文前面提到过:

RxJava 是一种新的 事件驱动型 编程范式,它以异步为切入点,试图一统 同步异步 的世界。

在我参与的项目中,我已经渐渐能感觉到这种 “天下大同” 的感受了。这也是为何我能听到不少人都会说 “一旦用了 RxJava 就很难再放弃了”。

也许这时候你会问我,到底推不推荐你们使用 RxJava ?我认为是这样,若是你认为在你的项目里,Callback 模式已经不能知足你的平常须要,事件之间存在复杂的依赖关系,你须要从更高的维度空间去从新思考你的问题,或者说你须要常常在时间或者空间维度上去从新组织你的事件,那么恭喜你, RxJava 正是为你打造的;若是你认为在你的项目里,目前使用 Callback 模式已经很好知足了你的平常开发须要,简单的业务逻辑也根本玩不出什么新花样,那么 RxJava 就是不适合你的。

(完)

本文属于 "RxJava 沉思录" 系列,欢迎阅读本系列的其余分享:


若是您对个人技术分享感兴趣,欢迎关注个人我的公众号:麻瓜日记,不按期更新原创技术分享,谢谢!:)

相关文章
相关标签/搜索