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>
答案 0 :(得分:0)
免责声明:我没有任何RxJava特定的经验,只有RxJS,Rx.NET和RxSwift
您应该能够直接传递ArrayList
的新实例:
Single<List<List<String>>> combinedData = stage.reduce(new ArrayList<>(), (list, items) -> {
list.addAll(items);
return list;
});
它只能用作累加器的初始种子;在第一次调用lambda之后,这是不相关的。
我想你想要一些这个问题的递归。以下解决方案可能适合您:
// 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);