The Java 8 coding style preferred by my colleagues is chaining asynchronous calls all the way through, e.g.
CompletionStage<E> someMethod() {
return doSomething().thenCompose(a -> {
// ...
return b;
}).thenCompose(b -> {
// ...
return c;
}).thenCompose(c -> {
// ...
return d;
}).thenApply(d -> {
// ...
return e;
});
}
I have something like the above, but with an added challenge: I need to recall values retrieved within some of the lambdas, in later lambdas. For example,
CompletionStage<E> someMethod() {
return doSomething().thenCompose(a -> {
// ...
Foo foo = fooDAO.getFoos(a);
// ...
return b;
}).thenCompose(b -> {
// ...
return c;
}).thenCompose(c -> {
// ...
Bar bar = barDAO.getBars(foo);
// ...
return d;
}).thenApply(d -> {
// ...
return someResult(d, bar);
});
}
When I declare Foo foo;
and Bar bar;
in the outside scope, I get errors about them not being final or effectively final. And I read about using a wrapper to make them effectively final, but it seems rather hacky to me to do this (I don't understand why that's allowed...)
I read that Java 8 didn't add support for tuples (though it considered BiVal
, which maybe I could have used to pass to BiFunction
lambdas). So I tried using pairs, e.g.
return doSomething().thenCompose(a -> {
// ...
Foo foo = fooDAO.getFoos(a);
// ...
return new Pair<>(foo, b);
}).thenCompose(fooAndB -> {
and then wherever I needed to recall foo
,
Foo foo = fooAndB.getKey();
But this feels so semantically wrong. Also, it doesn't work! I don't know why, because I thought a lambda parameter's scope is the same as its outside scope, and so all lambda parameters would be accessible from within later-chained lambdas.
What really is the scope of lambda parameters, and is there an idiomatic or at least semantically unoffensive way to do what I'd like to do while keeping the chaining?
Answers based on breaking the chain are fine as they may be useful for future viewers, but in my case, deviations from the dominant style in this repo may result in drawn out PR conversations and delayed approval, so I'd love a solution that preserves the chaining. Or, an explanation or demonstration as to how insane it would be to try to keep the chaining. Thank you!
Since you mention the coding style preferred by your colleagues, you probably already know the alternative of using nested calls instead:
CompletionStage<E> someMethod() {
return doSomething().thenCompose(a -> {
// ...
Foo foo = fooDAO.getFoos(a);
// ...
CompletableFuture<B> resultB = ...;
return resultB.thenCompose(b -> {
// ...
CompletableFuture<C> resultC = ...;
return resultC;
}).thenCompose(c -> {
// ...
Bar bar = barDAO.getBars(foo);
// ...
CompletableFuture<D> resultD = ...;
return resultD.thenApply(d -> {
// ...
return someResult(d, bar);
});
});
});
}
This immediately fixes your issue, at the cost of a bit less readable code. But this problem can easily be fixed by extracting some methods from your code:
CompletionStage<E> someMethod() {
return doSomething()
.thenCompose(this::processA);
}
private CompletionStage<E> processA(final A a) {
// ...
Foo foo = fooDAO.getFoos(a);
// ...
final CompletableFuture<B> result = ...;
return result
.thenCompose(this::processB)
.thenCompose(c -> processCAndFoo(c, foo));
}
private CompletionStage<C> processB(B b) {
// ...
return ...;
}
private CompletionStage<E> processCAndFoo(final C c, final Foo foo) {
// ...
Bar bar = barDAO.getBars(foo);
// ...
final CompletableFuture<D> result = ...;
return result
.thenApply(d -> someResult(d, bar));
}
By doing this, you avoid nested lambdas (and respect the preferred code style of your colleagues), but you also gain in readability and testability since you now have several small methods that are easier to understand and unit test.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With