为什么需要Reactive Programming?

如果你是一个有经验的工程师,在初次看到一个全新的概念时一定会想:这个东西会不会又是一个强行造出来的轮子?使用起来是不是门槛比较高,适不适合推广?

关于RxSwift/RxJava/RxJS的具体使用介绍网上非常多,这篇文章并不是要再一次介绍如何使用,也不打算讨论函数式、副作用等话题,而是想写一些关于Reactive这个话题一些更本质的思考。

Reactive Programming这个词理解起来很容易让人一知半解,感觉和声明式、函数式有一些关系,而且也感觉和Rx系列有什么关系。那到底它的定义是什么呢?看看维基百科上的解释(如果有更精确的表达欢迎指出):

In computing, reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change.

从定义中我们可以发现,有几个关键点,我们一个一个来看。

声明式(declarative)

什么是声明式?这又是一个比较抽象的概念,似乎大家都知道一部分,但是也没法全部讲清。而它的“对立者”,命令式,似乎更好描述一些。说的通俗一些,命令式编程就是你用一个语句,显式地去改变变量的状态,如a=b+c。任何一个写过代码的人,都是从这些简单的赋值语句开始学习的,早已习以为常。

声明式则不是这样,维基百科的定义如下:

a style of building the structure and elements of computer programs — that expresses the logic of a computation without describing its control flow.

也就是说,写代码的时候会把注意力集中在“逻辑”上,而不是具体如何实现,是一个更高抽象层次的做法。这样一来,下层具体实现就被分离开,使用者可以只关心自己的那部分代码。比如HTML是声明式的,你只需要关心你的.html代码,而每个元素具体如何解析,各个浏览器都是按照自己的理解来独立实现的。

数据流(data streams)

我们时常会遇到这些情况:

  • 从网络上下载数据,数据是一片一片传输的
  • 用户触摸屏幕,touchesMoved事件是随着用户的手指移动,一次一次触发的
  • 定时器触发事件,每个事件是按照一定的间隔,一个一个被抛出的

面对“一个系列的多个数据”这种情况,虽然每个语言都会有类似“迭代器”、“序列”的数据结构或概念,但是一方面这些数据需要调用者主动去拉取,另一方面对于异步的支持往往不够强大,需要语言本身的特性来补充(如async/await,coroutine)

传播变化(propagation of change)

通常一个系统中会有很多职责分工不同的模块,而在一个事件触发后,各个模块可能会依次响应和处理数据,并存储最后的结果。而这就是为什么会带来著名的callback hell的原因:似乎各个模块缺少一种优雅而统一的接口语法,而不得不通过丑陋的callback来把它们挨个连接起来。

除了语法上的问题,还有一些更重要的问题需要解决:

  • 数据在不同的时间点被抛出,如何让各个模块串行/并行处理?
  • 如何用通用的方法来“监听”一个信号源?
  • 如何表示数据异常,或者数据已经全部被抛出?
  • 如何用通用的方法来建立传播通道?如果通道中有分叉,或者信号之间有相互依赖,怎样优雅地表示?

当然我们自己是可以把代码写得漂亮一些,但终究没有从框架层面,甚至更高的概念认知层面,建立一种让所有人都能遵守的约定。

为什么要用Reactive Programming

讲到这里,我们已经发现,以上这些问题是如此的常见,以至于无论你用什么思想概念,什么编程范式,都需要去面对解决。有时我们会用Promise/Future/回调/协程来解决异步的问题,有时我们会用Iterator来解决数据流的问题,但是不足以解决以上的所有问题。

工程师们有一句经典的名言:没有增加一层抽象解决不了的问题,如果有就再增加一层。

试想一下,如果有一种通用的数据结构,甚至是一种通用的思想,把以上的问题都规范化,封装成通用的形式,这样就能省下相当一部分精力,让我们可以集中更多注意在上层业务逻辑上,同时代码的可读性、可维护性都更强,岂不是一个不错的选择?同时解决这些问题的方案的名字,就叫做Reactive Programming。

那么为什么这个方案被称作是Reactive(响应式)?我个人的理解是:因为我们日常写的绝大多数代码,其实都是在“响应”某种事件或数据变化。而为了达到这个目的,你需要恰到好处地解决以上问题。

我们来看一些事件传播例子:

  • 用户触发点击/滑动->UI控件状态变化->业务逻辑响应->发出网络请求->回调处理数据变化->UI更新
  • 系统网络断开->app监听到事件->UI展示网络错误
  • 服务端发送推送信息->网络层收到数据->业务逻辑响应->UI更新

我们会发现,这些逻辑的链路,都是始于一个外部的“信号源”,经过层层接力传播到app逻辑内,最后再反映到UI/数据层。这样的例子还有很多很多。

我们当然可以用回调,或者其他命令式的语法来达到同样的目的,但是因为缺少了一层抽象,导致我们在编写代码时总是不得不一次又一次去面对同样的场景(如信号依赖、管道建立、错误处理等等),违反了DRY原则。

这么一来,大家可能会跃跃欲试,写一个自己的轮子来解决这些问题。这也就是ReactiveX诞生的原因。

关于ReactiveX

ReactiveX最初被微软应用在.NET上,而后慢慢的在衍生出了各种不同语言的实现,诸如RxSwift/RxJava/RxJS,它们的使用和优缺点我想应该不用赘述,网上有各种各样的tutorial可以参考。但是这些教程似乎很少讲到一些更本质的原因,或者说它们更聚焦在异步语法的便利上——如果只是异步语法便利或者操作符多,Promise和Future一样能解决相当一部分问题,为什么还需要更复杂的Rx?我认为应该更多地从认识上,从概念上做一些改变。

Rx带来的一些变化

即刻在15年底引入Rx。一开始大家都赞叹Rx带来语法上的革命,享受链式调用的方便和优雅,但随着理解的深入,我们渐渐地发现Rx也带来了其他几点更深层次的变化:

  • 我们思考问题的时候更多的像上面提到的那样,把事件当做一个数据流(可能流里有一个或多个事件),把事件的来源看成是一个可观测信号源(Observable),而后面的每一环都是监听者(Observer)。它们静静地等待事件从上游一步一步流到自己这边,在自己内部逻辑处理完后,像接力一样传递给下一个人。
  • 编写代码时我们会更少地定义上帝模块(如“XXManager”),而是帮各个模块分别定义好自己内部的逻辑,最后在业务层统一组装。
  • 集中组装的代码一般只有寥寥数行,言简意赅,而且非常便于维护和调整,就好像把不同功能的水管方便地连接起来,有时也可以把它们拆开来互换位置,而不用担心他们的接口不兼容,协议会帮你把关。
  • 各个模块都会定义自己内部的“监听”逻辑,而这样一来他们彼此之间就减少了依赖关系,变得更加functional。虽然定义observer方法与定义callback有一些类似,但是也有一些微妙的差别 — — 每个模块现在都有了一个正式身份,叫做“监听者”,来“响应”外部的变化。

总结

  • Reactive programming是声明式的,和函数式概念不同,更多的是专注于处理(异步)数据流的变化
  • 不管你是否采用Reactive的思想,或者是否使用Rx库,都需要面对一些关于异步、数据流、传播的问题。可能Rx以及它定义的Observable-Observer-Scheduler模型并不是唯一解,但是仍然能给我们非常多的思考。
  • Rx带来的好处不仅仅是语法上的便利(尽管几十个功能各异的操作符真的帮我们减少了非常多的负担!),更是潜移默化改变思考问题的角度,从主动的命令式,变成了被动的响应式,而这其实更加符合程序的本质,因为app的每一次状态改变,本质上都是响应事件的结果
  • 增加一层抽象或许会增加使用者的学习成本和门槛,也会有一些诸如调用链过长、增加性能开销的问题。但是事实证明,在即刻三年多RxSwift/RxJava的实践中,获得的收益远大于其成本。如果只能保留一个第三方库,我们一定会选择Rx。

推荐阅读