Looking for better ways to compose RxJava Observables

时间:2017-06-20 12:31:11

标签: java observable reactive-programming rx-java2 reactivex

I am developing a tool that collects data from multiple sources and applies a few transformations sequentially. I am currently converting this functionality from Java 8 streams to use ReactiveX/RxJava.

Below, you can see a unit test that demos the current RxJava implementation.

While it works, I am not happy enough with the result and am looking for guidance on how to improve it!


My two questions are:

1. Each source returns a list of results (List<List<String>>). Because transformations need to be performed on the complete dataset, I need to merge multiple lists into a single one.

Right now the code looks like this:

Observable<List<List<String>>> stage = Observable.merge(src1, src2, src3, src4);

final List<List<String>> collector = new ArrayList<>();
Single<List<List<String>>> combinedData = stage.reduce(collector, (list, items) -> {
    list.addAll(items);
    return list;
});

Is there a way to get rid of the List<List<String>> collector that lives outside the observable flow?


2. To apply transformations in order, I am using a for-loop; I tried multiple variations (i.e.: flatMap, zipWith), however, what ends up happening is that the transformations are not applied in order; how can I model this without a for-loop?

for (Transform t : transforms) {
    stage = stage.flatMap(t::applyAsync);
}

Basically, I need a way to apply Observable<List<List<String>>> applyAsync(List<List<String>> input) on the input List<List<String>> and recursively keep doing so on each transformation (Observable<List<List<String>>>).

It's similar to Observable.reduce, but the accumulator function needs to change on every iteration.


Here is the complete unit-test code I wrote:

import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import org.mockito.ArgumentCaptor;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;

public class ObservableTest {
    @Test
    public void testObservable() throws Exception {
        // ARRANGE
        final CountDownLatch DONE = new CountDownLatch(1);

        // init source objects
        Observable<List<List<String>>> src1 = Observable.just(makeMatrix(Arrays.asList("src11")));
        Observable<List<List<String>>> src2 = Observable.just(makeMatrix(Arrays.asList("src21", "src22")));
        Observable<List<List<String>>> src3 = Observable.just(makeMatrix(Arrays.asList("src31", "src32", "src33")));
        Observable<List<List<String>>> src4 = Observable.just(makeMatrix(Arrays.asList("src41"), Arrays.asList("src51")));

        // prepare transformations and processor
        List<Transform> transforms = Arrays.asList(new Transform(1, 100), new Transform(2, 0));
        Processor processor = spy(new Processor());


        // ACT

        // Concat sources
        Observable<List<List<String>>> stage = Observable.merge(src1, src2, src3, src4);

        // Merge individual into matrix

        // (#1) Can the reduce operation be written without the accumulator?
        final List<List<String>> collector = new ArrayList<>();
        Single<List<List<String>>> combinedData = stage.reduce(collector, (list, items) -> {
            list.addAll(items);
            return list;
        });


        // Transform
        stage = combinedData.toObservable();
        for (Transform t : transforms) {
            // (#2) Can a series of transforms be applied sequentially to a Single (List<List<String>>), without the use of a for-loop?
            stage = stage.flatMap(t::applyAsync);
        }

        // Process
        stage.doOnComplete(DONE::countDown)
                .subscribeOn(Schedulers.computation())
                .subscribe(o -> System.out.println(processor.printList(o)));

        // wait for processing to complete
        DONE.await();


        // ASSERT

        // The sources should be combined in a single matrix
        @SuppressWarnings("unchecked")
        ArgumentCaptor<List<List<String>>> resultCaptor = ArgumentCaptor.forClass(List.class);

        verify(processor, times(1)).printList(resultCaptor.capture());
        List<List<String>> resultMatrix = resultCaptor.getValue();

        // result matrix should not be null and all transformations should be applied in order (T1, T2, etc.)
        assertThat(resultMatrix, notNullValue());
        assertThat(resultMatrix.stream().flatMap(Collection::stream).collect(Collectors.toList()), everyItem(containsString("T1-T2")));
        assertThat(resultMatrix, not(hasItem(hasItem(containsString("T2-T1")))));
   }


    private List<List<String>> makeMatrix(List<String> items) {
        return Collections.singletonList(items);
    }

    private List<List<String>> makeMatrix(List<String> items, List<String> moreItems) {
        return Arrays.asList(items, moreItems);
    }

    static class Processor {
        String printList(List<List<String>> input) {
            return input.stream().map(rows -> rows.stream().collect(Collectors.joining(" | ")))
                    .collect(Collectors.joining(System.lineSeparator()));
        }
    }

    static class Transform {
        final int n;
        private final int delay;

        Transform(int n, int delay) {
            this.n = n;
            this.delay = delay;
        }

        private Observable<List<List<String>>> applyAsync(List<List<String>> input) {
            return Observable.just(input).map(this::apply).delay(delay, TimeUnit.MILLISECONDS);
        }

        private List<List<String>> apply(List<List<String>> input) {
            return input.stream()
                    .map(row -> row.stream()
                            .map(this::transform)
                            .collect(Collectors.toList())
                    )
                    .collect(Collectors.toList());
        }

        private String transform(String input) {
            return input + "-T" + n;
        }
    }
}

Import the following Maven dependencies if you want to run it:

<dependency>
    <groupId>io.reactivex.rxjava2</groupId>
    <artifactId>rxjava</artifactId>
    <version>2.1.0</version>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.4.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>6.10</version>
    <scope>test</scope>
</dependency>

1 个答案:

答案 0 :(得分:0)

免责声明:我没有任何RxJava特定的经验,只有RxJS,Rx.NET和RxSwift

1

您应该能够直接传递ArrayList的新实例:

Single<List<List<String>>> combinedData = stage.reduce(new ArrayList<>(), (list, items) -> {
    list.addAll(items);
    return list;
});

它只能用作累加器的初始种子;在第一次调用lambda之后,这是不相关的。

2

我想你想要一些这个问题的递归。以下解决方案可能适合您:

// Put this somewhere
public IObservable<List<List<String>> handleTransforms(
    Observable<List<List<String>>> currentStage
    List<Transform> ts){

    return currentStage.flatMap(t[0]::applyAsync)
        .flatMap(newStage -> handleTransforms(newStage, ts.stream().skip(1).toList()))
}

// And then use it like this
stage = handleTransforms(stage, transforms);