RxJava与常见控制结构

在编程语言里面,控制结构是比较基础的一块内容。常见的是顺序,分支和循环。部分语言有try catch finally结构等。在使用RxJava时,如何对应既有的控制结构,或者说既有代码如何转换成RxJava的代码,是学习RxJava时必须熟练掌握的东西。

顺序结构

在三种常见控制结构里面,顺序其实是比较难的一种结构。比如说以下两行顺序代码

和另外一类顺序代码

operation2是否需要上一行操作的结果对如何转换成RxJava的代码有很大的影响。
对于第一种有依赖关系的代码来说,需要使用flatMap

因为有依赖关系,operation2不能与operation1同时进行。
相反,如何operation2不依赖operation1的结果的话,理论上可以同时执行。
比如说上面第二类顺序代码

这里通过zip让两个操作同时进行。这是一般顺序代码难以表达的结构。
不过,仍旧存在一类代码,虽然两个操作没有直接依赖关系,但是隐藏着第一个操作失败之后,第二个操作不应该执行的错误依赖要求。对于这类代码,就不应该使用zip让两个操作并行,你需要退回flatMap的方式。

分支结构

分支结构相对简单,比如说最常见的单个if

if加else

if加else if

当然还有switch表达式,由于switch可以用if来替换,这里不再展开

不管是哪种if,在RxJava里都必须转换为一个有返回值的表达式才能使用。比如说

用RxJava的话

可以看到这里使用了flatMap,并且if判断被放到了flatMap内部。
和顺序结构时用的flatMap不同,顺序结构时的flatMap主要是考虑到数据依赖,以及异常情况的fail fast,分支结构时用的if,既可以抛出异常,也可以返回不同的值。

抛出异常的例子

改写为RxJava风格

这里由于a来自一个Single流,使用flatMap串了起来。如果a只是一个普通方法参数,直接写if判断可能更好,不是所有时候都需要改成异步。

返回不同的值,也比较简单,看情况你可以使用map而不是flatMap。比如说开始的例子

可以改成

甚至更短

如果没有异常,或者一次肯定返回一个值的话,用map,并且在map里加上判断逻辑即可。

需要注意的是,在写同步代码的时候,有一种风格是优先把异常情况排除掉,在转换为RxJava风格时你可能需要灵活处理,包括使用异常,使用中间结果等等。比如对下面的代码

在转换的时候,你你可以选择设计一个特殊的异常用于区分,也可以把statusCode直接作为结果

循环结构

接下来一个控制结构是循环。一般的循环转换为RxJava都很简单。比如说

如果有条件,可以通过filter/takeWhile等
循环的目的是聚合,比如说计算总和的话,可以用reduce
因为函数式语言本身在这块支持比较丰富,表达性也比同步代码要清晰

不过如果你的代码中有break/continue之类的话,特别是比较复杂的break/continue时,最坏情况下你需要退回flatMap,在flatMap中处理复杂逻辑。

trycatch结构

最后讲一下如何转换try catch finally。这个结构中比较难的是finally,比如典型的IO处理

转换成RxJava风格时,你需要注意正常情况和异常情况下都需要调用close

这里使用了onErrorResumeNext。onErrorResumeNext其实放在哪个位置其实不是很重要,但是放在最后可以组成一个可以复用的operator。

总的来说,学会如何把既有结构转换成RxJava风格的代码之后,可以说学会了RxJava的一半。希望上述内容对你有用。

使用RxJava把你的异步调用同步化

每周的定期博客。

本周个人认为的亮点之一是在测试中使用RxJava,把异步调用同步化。让代码变得“清晰”了许多。

如果你使用基于异步的编程方式的话,肯定会碰到一个如何测试的问题。最简单的比如说

或者基于回调方式的异步代码

前一种还好,后一种在多个异步调用时很容易碰到callback hell的问题。

比如说上面这种嵌套比较深的代码。一种解决方法是类似JavaScript的Promise的链式调用。

但是链式调用的一个问题是,在方法调用之间是非简单类型的依赖关系时,你需要一些中间类,而且调用顺序有所变化时,中间类也必须随之改变。比如说上述代码中bar依赖A,baz依赖A与B时

相比之前的写法会显得繁琐。对此,你可以改用JavaScript的async/await,完全把代码改成同步风格,免去些中间类的需要。

async/await是协程的一种实现,但是Java没有协程,该怎么办?方法很简单,利用类似Future的get把代码同步化就可以了。特别是在测试代码中,异步代码转开成同步代码并没有太大问题。相反的,同步风格的测试代码更好理解。

于是在测试代码中可以这么做

  1. 把回调式代码转为Observable事件源
  2. 使用Observable的blockGet,blockAwait转换为同步代码

顺便说一句,如果你不会用RxJava,也没有关系,你可以使用CompletableFuture,也可以达到同样的效果。

希望以上的点子对你有用。

尝试使用GTD

2020年初,个人开始尝试一种新的日常任务管理的方式:GTD(Getting Things Done)。对于我来说,GTD其实并不算一个新的事物,几年前就尝试过,当时还用了一个以GTD为卖点的软件,软件的名字有点忘记了,只记得使用GTD管理事务有种束手束脚的感觉,以致于最后放弃了。

重新拿起GTD,一方面是因为自己一直使用的RTM(Remember The Milk)在2019年有点荒废,毕竟自己花了钱的,不用有点浪费,另一方面,觉得自己平时管理任务的方式需要改进。个人之前就了解过与一些任务管理相关的方法论,比如说在做计划前把自己心里想的事情全部写出来,给自己减压之类。还有任务最好按照S.M.A.R.T原则进行细化、量化等。这些方法本来没有问题,但是只能算一部分的解决方案,没有成为一个系统的方法论。

于是乎找了GTD相关资料。GTD有书,比如说无压工作之类的。个人按照一篇日文的读写笔记学习了GTD的主要方法论。

个人认为GTD的一个核心,是写下任务之后,怎么处理这个任务。这里说的处理不是“完成”任务,而是怎么分类,怎么列出,怎么管理的问题。刚才提到做计划前把自己心里想的事情都写出来。在GTD中,这些事情都被写入一个叫做Inbox的任务列表中。Inbox类似邮箱,是一个任务第一个被放入的地方。不管你在做计划前,还是突发奇想,心血来潮,在你忘记之前,请写下你的任务。

把你心里所想的东西全部输出后,接下来逐个处理这些任务。如果你在处理过程中发现新的任务,原则上请加到Inbox最后,之后再处理。从Inbox中处理这些任务时,首先判断是否可以立刻行动,如果2分钟内能解决的任务,就立刻行动。你可以想象,假如大部分任务都是2分钟内能完成的话,那么你的Inbox的任务就是一个普通的任务列表,从上到下一件件处理。

如果不是2分钟内能解决的,你需要判断这个任务是不是比较复杂的任务。比较复杂的任务指的是需要多个步骤才能完成的任务,也就是判断是不是类似S.M.A.R.T里所说的足够小,可以实施的任务。比较复杂的任务被放到“项目”的列表中,不算复杂的任务被放到“待办任务”。把复杂任务和普通任务分开之后,你可以直接从待办任务列表中接着处理你要的任务,而不是卡在某个没有被分解的大任务上。

在分发任务的时候,除了待办任务列表和项目列表之外,还有直接丢弃,之后会做的任务列表以及委派给别人的多种方式。在处理Inbox里的任务时,实际上已经把任务进行了分类。我一开始是按照主题,或者说内容分类,比如学习,个人,工作等。这样有一个问题,就是你不知道哪些是接下来要做的任务。在GTD中,你直接有一个待办任务列表,你只需要关注这个任务列表就行了。

对于项目列表中的任务来说,你需要分解并把其中某个子任务加入待办列表中。如果你没法分解,这个任务就不是项目。如果你加入不了待办列表,说明你这个任务现在还不是时候,你应该放到之后会做的任务列表中。这个之后会做的任务列表,作用是把存放你暂时不想做,或者需要时间孵化的任务。那些某天你要做什么的事情,你需要使用日历,而不是任务列表。日历上精确了哪天你需要做什么事情,个人理解任务列表上的任务倾向于没有明显的执行日期。

在分类和分解任务之后,你不会有“我忘记了什么”,“接下来该做什么”的疑问,因为答案就在你的面前。当你有新的想法或者任务之后,把任务加入Inbox,然后按照上面的步骤再执行一次。如果你严格按照上面的步骤执行的话,你不会感觉有压力(记忆上的,工作上的等),因为你把接下来要做什么开始管理起来了。

为了更好地管理你的任务,你可以尝试给你的待办任务,加上tag,这些tag可以是表明任务执行的场景,或者某种关键字,然后可以进一步分类任务,并按照场景筛选任务,按部就班地完成任务。另一方面,你需要定期清理你的任务。比如说个人感觉待办任务不能太多,虽然可能没有结束时间,但是仍旧会给人一种压力。除了常见的优先级,场景分类之外,将任务数保持在一定数量可能更好。在之后再做的任务列表中的任务,如果自己多次判断不会做的话建议直接删除。

GTD的方法老实说,并没有特别依赖TODO工具的功能,你可以用一个纸质的笔记本完成上述的流程。当然,有功能更强大一些的TODO工具肯定更好。比如说,我在Remember The Milk里面,使用Smart List代替普通的List筛选待办列表的任务。筛选的条件如下

1.待办列表里的Action
2.项目列表中的任务的子任务,并且带有next-action这个tag

满足上面一个条件的任务即可显示在Smart List版的待办任务中。这样就不用从项目列表移动任务到待办列表,而且你点击子任务可以看到关联的项目任务。这种列表+tag的管理方式比单纯列表的方式要灵活一些。当然你不能忘记你的目的是完成这些任务,任务管理的目的是没有心理负担并且快速地列出你接下来要做的事情。

最后,虽然个人还调整自己管理任务的方式,但是核心的内容没有变化,并且GTD让我能够没有压力地Getting Things Done。如果你觉得自己被各种事情搞得焦头烂额的话,建议试一下GTD。

2019年技术小节

转眼之间2019年已经过去,2020年已经到来。在还未正式开始上班之前,个人想小结一下2019年自己技术相关的学习与感想。

2018年下半年的时候,突然奇想开始实现raft算法之后,个人感觉自己的技术视野一下子拓展了开来。特别是之后补充了学习自己不擅长的多核编程,逐渐看得懂Java并发库中的实现,并撰写了一些分析的文章。个人觉得并发算法如果你只是去看代码,你很难一下子理解代码在做什么,你需要循序渐进地分析和理解,在这其中自己尝试去实现最有效果。

因为觉得自己的这些文章,可以分享到自己一直看的《开发者头条》上,所以就尝试了放了几篇,比如说有关ReentrantLock的三篇实现分析。很幸运,几乎都出现在了次日的精选文章上。之后有图书编辑联系我是否有兴趣写书,于是我花了4个多月写了300多页的书。现在书还在编辑中,内容暂时不能公开。不过对于我来说,是一个很好的输出自己技术能力的一个机会。

写书的同时,其实我仍旧在寻找和尝试更好的实现raft算法的技术。这期间,学习了C++11下的多线程编码,以及尝试了Golang和Rust。从结论上来说,个人都不是很满意。C++的缺点比较明显,难。新版本的C++就是不断加新语法,而且是在没有简单易懂的依赖库机制下。写C++仍旧是必须从零开始的感觉,对个人开发来说很花时间。

相比C++,Rust要好很多,有crate,有标准的测试,有编译器保证你的代码不出现C/C++各种奇怪的问题。但是Rust的Lifecycle机制导致并发算法的代码很难写,越是复杂的代码越是容易编译不过。如果与编译器做斗争的话,我还不如退一步写C++,因为我知道我在做什么。个人知道Rust仍旧在发展,或许将来可以满足我的要求。

Golang是一个看起来不错,但是实际深入之后放弃的语言。我对Golang的协程很有兴趣,查了很多资料,也基本理解了协程的目的、做法以及长处短处。协程的短处是一方面,另一个让我放弃的原因是Golang在Memory Model的部分,文档第一句话是把程序员当笨蛋的赶脚(虽然Golang明显就是为那些入了Google但是不精通C++的人开发的),所谓的Memory Model也只是介绍了Golang提供的并发工具,给人的感觉就是C语言的翻版(你可以想想Google的C++ Guideline里尽量不用C++特有的功能,而是与C兼容的部分)。综合考虑了一下,个人项目里不是很想使用这种矫枉过正的语言。如果工作上要用,则是另外一回事情。

编程语言方面,2019年个人学的Kotlin可能是最好的了,在Java上做加法,特别是extension function,property的delegate等等(当然,也有协程)。个人觉得,比起解决语言痛点(C++对象生命周期)和大肆做减法(Golang对C++的做法),解决业务上的痛点和引入其他语言优秀的语法(C#的property delegate)可能更好。因为构建一个不太过严格但是又不是太过宽容的模型很难,Rust的生命周期管理难以处理复杂场景,Golang的全面协程化导致运行模型的不灵活。老实说,个人觉得这里面也有这两个都是命令语言的原因,如果是函数式语言,使用起来不会有这些问题。

说到函数式编程语言,不得不说现在编程语言不断在融入来自函数式语言的概念,从Scala到Java的lambda,从C++的lambda到Rust天生支持函数式。函数式编程语言的思维方式除了可以简化遍历,还有数据有无(Option/Some/None)和专注于正常流程的异常处理方式(Golang的C语言处理方式绝对是一个反例)。有空学习一些函数式语言的内容将来肯定会有一些收获,毕竟将来多范式编程语言会原来越多。

另一个和函数式编程语言相关的是ReactiveX,个人在2019年年末的时候花了两个星期的时间重新学习了一下(以前学过一次,太难放弃了)。个人觉得如ReactiveX所说,ReactiveX编程方式确实可以作为程序的主要处理方式,而不是命令式语言的从上到下的处理方式。这里面有ReactiveX所对应的异步编程的原因,也有命令式语言在复杂问题上的抽象不足(比如Golang的协程无法处理同时IO处理,必须退化为CSP编程方式)。顺便说一句,ReactiveX一开始来自C#,然后发展到多语言。这其实说明好的思路与语言以及背后的公司没有直接关系。

2019年另外一件事情是,与ReactiveX同时找到了一个框架Vert.x,综合考虑了一下,这可能是我想要的东西。当你把网络,文件,DNS等IO的部分全部异步化之后,基本上和协程没有区别了,而且作为类库形式比直接语言内置的感觉灵活性更好。2020年如果我有空的话想把xraft重写一下,至少把xgossip的核心部分尝试重写一次。

最后,2020年个人的目标,是继续学习一些个人比较重要的东西,比如说

  • SSTable
  • MerkleTree
  • STM(软件事务内存)
  • 多核编程

等等。