java 副作用_RxJava 教程第三部分:驯服数据流之副作用

前面两部分,我们学习到了如何创建 Observable以及如何从 Observable 中获取数据。本部分将介绍一些更高级的用法,以及一些在大型项目中的最佳实践。

Side effects(副作用)

没有副作用的函数通过参数和返回值来程序中的其他函数相互调用。当一个函数中的操作会影响其他函数中的返回结果时,我们称该函数有副作用。写数据到磁盘、记录日志、打印调试信息都是常见的副作用表现。Java 中在一个函数里面修改另外一个函数使用的对象的值是合法的。

副作用有时候很有用也有必要使用。但是有时候也有很多坑。 Rx 开发者应避免非必要的副作用,如果必须使用副作用的时候,则应该写下详细的说明文档。

副作用的问题

Functional programming 通常避免副作用。带有副作用的函数(尤其是可以修改参数状态的)要求开发者了解跟多实现的细节。增加了函数的复杂度并且导致函数被错误理解和使用,并且难以维护。副作用有故意的和无意的。可以通过封装或者使用不可变对象来避免副作用。有一些明智的封装规则可以显著的提高你 Rx 代码的可维护性。

我们使用一个带有副作用的示例来演示。 Java 中不可以在 Lambda 或者 匿名函数中引用外层的非 final 变量。 但是 Java 中的 final 变量只是保证了该编译引用的对象地址不变,但是对象本身的状态还是可以改变的。例如,下面是一个用来计数的一个类:

Java

class Inc {

private int count = 0;

public void inc() {

count++;

}

public int getCount() {

return count;

}

}

1

2

3

4

5

6

7

8

9

10

classInc{

privateintcount=0;

publicvoidinc(){

count++;

}

publicintgetCount(){

returncount;

}

}

即使是一个 final 的 Inc 变量,还是可以通过调用其函数来修改他的状态。 注意 Java 并没有强制显式使用 final ,如果在你 Lambda 表达式中修改外层变量的引用对象地址(把外层变量重新复制为其他对象),则会出错。

Java

Observable values = Observable.just("请", "不要", "有", "副作用");

Inc index = new Inc();

Observable indexed =

values.map(w -> {

index.inc();

return w;

});

indexed.subscribe(w -> System.out.println(index.getCount() + ": " + w));

1

2

3

4

5

6

7

8

9

10

Observablevalues=Observable.just("请","不要","有","副作用");

Incindex=newInc();

Observableindexed=

values.map(w->{

index.inc();

returnw;

});

indexed.subscribe(w->System.out.println(index.getCount()+": "+w));

结果:

Java

1: 请

2: 不要

3: 有

4: 副作用

1

2

3

4

5

1:请

2:不要

3:有

4:副作用

目前还来看不出来问题。但是如果我们在该 Observable 上再次订阅一个 subscriber,则问题就出来了。

Java

Observable values = Observable.just("请", "不要", "有", "副作用");

Inc index = new Inc();

Observable indexed =

values.map(w -> {

index.inc();

return w;

});

indexed.subscribe(w -> System.out.println("1st observer: " + index.getCount() + ": " + w));

indexed.subscribe(w -> System.out.println("2nd observer: " + index.getCount() + ": " + w));

1

2

3

4

5

6

7

8

9

10

11

Observablevalues=Observable.just("请","不要","有","副作用");

Incindex=newInc();

Observableindexed=

values.map(w->{

index.inc();

returnw;

});

indexed.subscribe(w->System.out.println("1st observer: "+index.getCount()+": "+w));

indexed.subscribe(w->System.out.println("2nd observer: "+index.getCount()+": "+w));

结果:

Java

1st observer: 1: 请

1st observer: 2: 不要

1st observer: 3: 有

1st observer: 4: 副作用

2nd observer: 5: 请

2nd observer: 6: 不要

2nd observer: 7: 有

2nd observer: 8: 副作用

1

2

3

4

5

6

7

8

9

1stobserver:1:请

1stobserver:2:不要

1stobserver:3:有

1stobserver:4:副作用

2ndobserver:5:请

2ndobserver:6:不要

2ndobserver:7:有

2ndobserver:8:副作用

第二个 Subscriber 的索引是从 5 开始的。这明显不是我们想要的结果。这里的副作用很容易发现,但是真实应用中的副作用有些很难发现。

在数据流中组织数据

可以通过 scan 函数来计算每个数据的发射顺序:

Java

class Indexed {

public final int index;

public final T item;

public Indexed(int index, T item) {

this.index = index;

this.item = item;

}

}

1

2

3

4

5

6

7

8

9

classIndexed{

publicfinalintindex;

publicfinalTitem;

publicIndexed(intindex,Titem){

this.index=index;

this.item=item;

}

}

Java

Observable values = Observable.just("No", "side", "effects", "please");

Observable> indexed =

values.scan(

new Indexed(0, null),

(prev,v) -> new Indexed(prev.index+1, v))

.skip(1);

indexed.subscribe(w -> System.out.println("1st observer: " + w.index + ": " + w.item));

indexed.subscribe(w -> System.out.println("2nd observer: " + w.index + ": " + w.item));

1

2

3

4

5

6

7

8

9

10

Observablevalues=Observable.just("No","side","effects","please");

Observable>indexed=

values.scan(

newIndexed(0,null),

(prev,v)->newIndexed(prev.index+1,v))

.skip(1);

indexed.subscribe(w->System.out.println("1st observer: "+w.index+": "+w.item));

indexed.subscribe(w->System.out.println("2nd observer: "+w.index+": "+w.item));

结果:

Java

1st observer: 1: No

1st observer: 2: side

1st observer: 3: effects

1st observer: 4: please

2nd observer: 1: No

2nd observer: 2: side

2nd observer: 3: effects

2nd observer: 4: please

1

2

3

4

5

6

7

8

9

1stobserver:1:No

1stobserver:2:side

1stobserver:3:effects

1stobserver:4:please

2ndobserver:1:No

2ndobserver:2:side

2ndobserver:3:effects

2ndobserver:4:please

上面的结果为正确的。 我们把两个 Subscriber 共享的属性给删除了,这样他们就没法相互影响了。

do

像记录日志这样的情况是需要副作用的。subscribe 总是有副作用,否则的话这个函数就没啥用了。虽然可以在 subscriber 中记录日志信息,但是这样做有缺点:

1. 在核心业务代码中混合了不太重要的日志代码

2. 如果想记录数据流中数据的中间状态,比如 执行某个操作之前和之后,则需要一个额外的 Subscriber 来实现。这样可能会导致最终 Subscriber 和 日志 Subscriber 看到的状态是不一样的。

下面的这些函数让我们可以更加简洁的实现需要的功能:

Java

public final Observable doOnCompleted(Action0 onCompleted)

public final Observable doOnEach(Action1> onNotification)

public final Observable doOnEach(Observer super T> observer)

public final Observable doOnError(Action1 onError)

public final Observable doOnNext(Action1 super T> onNext)

public final Observable doOnTerminate(Action0 onTerminate)

1

2

3

4

5

6

7

publicfinalObservabledoOnCompleted(Action0onCompleted)

publicfinalObservabledoOnEach(Action1>onNotification)

publicfinalObservabledoOnEach(Observer<?superT>observer)

publicfinalObservabledoOnError(Action1onError)

publicfinalObservabledoOnNext(Action1<?superT>onNext)

publicfinalObservabledoOnTerminate(Action0onTerminate)

这些函数在 Observable 每次事件发生的时候执行,并且返回 Observable。 这些函数明确的表明了他们有副作用,使用起来更加不易混淆:

Java

Observable values = Observable.just("side", "effects");

values

.doOnEach(new PrintSubscriber("Log"))

.map(s -> s.toUpperCase())

.subscribe(new PrintSubscriber("Process"));

1

2

3

4

5

6

7

Observablevalues=Observable.just("side","effects");

values

.doOnEach(newPrintSubscriber("Log"))

.map(s->s.toUpperCase())

.subscribe(newPrintSubscriber("Process"));

结果:

Java

Log: side

Process: SIDE

Log: effects

Process: EFFECTS

Log: Completed

Process: Completed

1

2

3

4

5

6

7

Log:side

Process:SIDE

Log:effects

Process:EFFECTS

Log:Completed

Process:Completed

这里使用了上一章使用的帮助类 PrintSubscriber 。这些 do 开头的函数并不影响最终的 Subscriber。 例如:

Java

static Observable service() {

return Observable.just("First", "Second", "Third")

.doOnEach(new PrintSubscriber("Log"));

}

1

2

3

4

5

staticObservableservice(){

returnObservable.just("First","Second","Third")

.doOnEach(newPrintSubscriber("Log"));

}

可以这样使用该函数:

Java

service()

.map(s -> s.toUpperCase())

.filter(s -> s.length() > 5)

.subscribe(new PrintSubscriber("Process"));

1

2

3

4

5

service()

.map(s->s.toUpperCase())

.filter(s->s.length()>5)

.subscribe(newPrintSubscriber("Process"));

结果:

Java

Log: First

Log: Second

Process: SECOND

Log: Third

Log: Completed

Process: Completed

1

2

3

4

5

6

7

Log:First

Log:Second

Process:SECOND

Log:Third

Log:Completed

Process:Completed

即便最终使用的时候过滤了一些数据,但是我们记录了服务器返回的所有结果。

这些函数中 doOnTerminate 在 Observable 结束发射数据之前发生。不管是因为 onCompleted 还是 onError 导致数据流结束。 另外还有一个 finallyDo 函数在 Observable 结束发射之后发生。

doOnSubscribe, doOnUnsubscribe

Java

public final Observable doOnSubscribe(Action0 subscribe)

public final Observable doOnUnsubscribe(Action0 unsubscribe)

1

2

3

publicfinalObservabledoOnSubscribe(Action0subscribe)

publicfinalObservabledoOnUnsubscribe(Action0unsubscribe)

Subscription 和 unsubscription 并不是 Observable 发射的事件。而是 该 Observable 被 Observer 订阅和取消订阅的事件。

Java

ReplaySubject subject = ReplaySubject.create();

Observable values = subject

.doOnSubscribe(() -> System.out.println("New subscription"))

.doOnUnsubscribe(() -> System.out.println("Subscription over"));

Subscription s1 = values.subscribe(new PrintSubscriber("1st"));

subject.onNext(0);

Subscription s2 = values.subscribe(new PrintSubscriber("2st"));

subject.onNext(1);

s1.unsubscribe();

subject.onNext(2);

subject.onNext(3);

subject.onCompleted();

1

2

3

4

5

6

7

8

9

10

11

12

13

14

ReplaySubjectsubject=ReplaySubject.create();

Observablevalues=subject

.doOnSubscribe(()->System.out.println("New subscription"))

.doOnUnsubscribe(()->System.out.println("Subscription over"));

Subscriptions1=values.subscribe(newPrintSubscriber("1st"));

subject.onNext(0);

Subscriptions2=values.subscribe(newPrintSubscriber("2st"));

subject.onNext(1);

s1.unsubscribe();

subject.onNext(2);

subject.onNext(3);

subject.onCompleted();

结果:

Java

New subscription

1st: 0

New subscription

2st: 0

1st: 1

2st: 1

Subscription over

2st: 2

2st: 3

2st: Completed

Subscription over

1

2

3

4

5

6

7

8

9

10

11

12

Newsubscription

1st:0

Newsubscription

2st:0

1st:1

2st:1

Subscriptionover

2st:2

2st:3

2st:Completed

Subscriptionover

使用 AsObservable 函数来封装

Rx 使用面向对象的 Java 语言来实现 functional programming 风格编码。 需要注意 面向对象中的问题。 例如下面一个天真版的返回 observable 的服务:

Java

public class BrakeableService {

public BehaviorSubject items = BehaviorSubject.create("Greet");

public void play() {

items.onNext("Hello");

items.onNext("and");

items.onNext("goodbye");

}

}

1

2

3

4

5

6

7

8

9

publicclassBrakeableService{

publicBehaviorSubjectitems=BehaviorSubject.create("Greet");

publicvoidplay(){

items.onNext("Hello");

items.onNext("and");

items.onNext("goodbye");

}

}

上面的实现中, 调用者可以自己修改 items 引用的对象,也可以修改 Observable 发射的数据。所以需要对调用者隐藏 Subject 接口,只暴露 Observable 接口:

Java

public class BrakeableService {

private final BehaviorSubject items = BehaviorSubject.create("Greet");

public Observable getValues() {

return items;

}

public void play() {

items.onNext("Hello");

items.onNext("and");

items.onNext("goodbye");

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

publicclassBrakeableService{

privatefinalBehaviorSubjectitems=BehaviorSubject.create("Greet");

publicObservablegetValues(){

returnitems;

}

publicvoidplay(){

items.onNext("Hello");

items.onNext("and");

items.onNext("goodbye");

}

}

上面的改进版本,看起来我们返回的是一个 Observable,但该返回的对象是不安全的,返回的其实是一个 Subject。

asObservable

由于 Observable 是不可变的,所以 asObservable 函数是为了把一个 Observable 对象包装起来并安全的分享给其他人使用。

Java

public Observable getValues() {

return items.asObservable();

}

1

2

3

4

publicObservablegetValues(){

returnitems.asObservable();

}

这样的话,我们的 Subject 对象就被合理的保护起来了。这样其他恶意人员也无法修改你的 Observable 返回的数据了,在使用的过程中也可以避免出现错误了。

无法保护可变对象

在 RxJava 中, Rx 传递的是对象引用 而不是 对象的副本。在一个 地方修改了对象,在传递路径的其他地方上也是可见的。例如下面一个可变的对象:

Java

class Data {

public int id;

public String name;

public Data(int id, String name) {

this.id = id;

this.name = name;

}

}

1

2

3

4

5

6

7

8

9

classData{

publicintid;

publicStringname;

publicData(intid,Stringname){

this.id=id;

this.name=name;

}

}

使用该对象的一个 Observable 和两个 Subscriber:

Java

Observable data = Observable.just(

new Data(1, "Microsoft"),

new Data(2, "Netflix")

);

data.subscribe(d -> d.name = "Garbage");

data.subscribe(d -> System.out.println(d.id + ": " + d.name));

1

2

3

4

5

6

7

8

Observabledata=Observable.just(

newData(1,"Microsoft"),

newData(2,"Netflix")

);

data.subscribe(d->d.name="Garbage");

data.subscribe(d->System.out.println(d.id+": "+d.name));

结果:

Java

1: Garbage

2: Garbage

1

2

3

1:Garbage

2:Garbage

第一个 Subscriber 先处理每个数据,在第一个 Subscriber 完成后第二个 Subscriber 开始处理数据,由于 Observable 在传递路径中使用的是对象引用,所以 第一个 Subscriber 中对对象做的修改,第二个 Subscriber 也会看到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值