RxJava与常见控制结构


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

顺序结构

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

a = operation1()
b = operation2(a)

和另外一类顺序代码

a = operation1()
b = operation2()

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

Single<A> operation1() {...}
Single<B> operation2(A a) {...}

operation1
   .flatMap(a -> operation2(a))

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

Single<A> operation1() {...}
Single<B> operation2() {...}

Single.zip(operation1(), operation2(), (a, b) -> {...})

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

分支结构

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

if(condition) {
  // do something
}

if加else

if(condition) {
  // do something
} else {
  // do something
}

if加else if

if(condition) {
  // do something
} else if(condition2) {
  // do something
} else {
  // do something
}

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

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

int b = 0;
if(a == 1) {
  b = 1;
}

用RxJava的话

Single<Integer> sourceA = Single.just(1);

sourceA.flatMap(a -> {
  if(a == 1) {
    return Single.just(1);
  }
  return Single.just(0);
})

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

抛出异常的例子

int operation(int a) {
  if(a < 0) {
    throw new IllegalArgumentException("a < 0");
  }
  // do stuff
}

改写为RxJava风格

Single<Integer> sourceA = ...

sourceA.flatMap(a -> {
  if(a < 0) {
    return Single.error(new IllegalArgumentException("a < 0"));
  }
  // other case
})

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

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

Single<Integer> sourceA = Single.just(1);

sourceA.flatMap(a -> {
  if(a == 1) {
    return Single.just(1);
  }
  return Single.just(0);
});

可以改成

Single<Integer> sourceA = Single.just(1);

sourceA.map(a -> {
  if(a == 1) {
    return 1;
  }
  return 0;
});

甚至更短

Single<Integer> sourceA = Single.just(1);

sourceA.map(a -> a == 1 ? 1 : 0);

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

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

void operation(int a, Context ctx) {
    if(a < 0) {
        ctx.statusCode(403);
        return;
    }

    operation2(a);
    ctx.statusCode(200);
    ctx.write("done");
}

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

class OperationException extends RuntimeException {
    private final int errorCode;

    OperationException(int errorCode) {
        super();
        this.errorCode = errorCode;
    }

    int getErrorCode() {
        return this.errorCode;
    }
}

Single<Integer> sourceA = ...;
sourceA
    .flatMap(a -> {
        if(a < 0) {
            return Single.error(new OperationException(403));
            // or Single.just(403)
        }
        return operation2(a).andThen(Single.just(200));
    });

循环结构

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

for(int n : ns) {
    int m = operation(n);
    // do something with m
}

Observable.fromIterable(ns)
    .map(n -> operation(n))
    .subscribe(ms -> ...)

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

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

trycatch结构

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

IOResource ior = ...
try {
  // do with ior
} catch(IOException e) {
  // do with exception
} finally {
  ior.close();
}

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

Single<IOResource> sourceIor = ...

sourceIor.flatMap(ior -> 
  ior.operationWithResult()
    .onErrorResumeNext(e -> ior.close().andThen(Single.error(e))
    .flatMap(r -> ior.close().andThen(Single.just(r))
);

sourceIor.flatMap(ior -> 
  ior.operationWithoutResult()
    .onErrorResumeNext(e -> ior.close().andThen(Completable.error(e))
    .andThen(() -> ior.close())
);

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

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