Reputation: 11513
Lately I have been applying a pattern where I chain futures together where for some future I don't care about the result but I do care it completed without exception before continuing the futures chain.
What I do is using thenCompose() and then supply it with a CompletableFuture that always ends with .thenApply(a). Example:
futureR
.thenCompose(r -> sequentialFutureTask1(r).thenApply(ignored -> r))
.thenCompose(r -> sequentialFutureTask2(r).thenApply(ignored -> r))
.thenCompose(r -> sequentialFutureTask3(r).thenApply(ignored -> r))
I've even streamlined this with a helper method:
futureFoo
.thenCompose(identityComposing(r -> sequentialFutureTask1(r)))
.thenCompose(identityComposing(r -> sequentialFutureTask2(r)))
.thenCompose(identityComposing(r -> sequentialFutureTask3(r)))
public static <T> Function<T, CompletableFuture<T>> identityComposing(Function<T, CompletableFuture<?>> c) {
return r -> cf.apply(r).thenApply(ignored -> r);
}
Is this an anti-pattern? It looks concise enough to me, but somehow I feel I missed some point.
Upvotes: 2
Views: 594
Reputation: 20608
Outside of the invisible side-effects pointed out by Antoniossss in the comments, which drives this a bit away from functional programming, the other issue is that you are still repeating yourself with all these identityComposing()
wrapping calls.
As you said, the requirement is that those methods are called in sequence, so semantically you don’t really need the "composition" result. The intent is really to chain the calls themselves.
This would be best represented by composing those calls directly instead:
futureR.thenAccept(r -> sequentialFutureTask1(r)
.thenCompose(ignored -> sequentialFutureTask2(r))
.thenCompose(ignored -> sequentialFutureTask3(r)));
Of course, there is still a lot of repetition here with the thenCompose(ignored -> …(r))
. If this is a frequent pattern you have, you could wrap this in the following method:
@SafeVarargs
public final <R> void executeInSequence(R r,
Function<? super R, ? extends CompletionStage<?>> firstTask,
Function<? super R, ? extends CompletionStage<?>>... otherTasks) {
CompletionStage<?> prev = firstTask.apply(r);
for (Function<? super R, ? extends CompletionStage<?>> task : otherTasks) {
prev = prev.thenCompose(ignored -> task.apply(r));
}
}
(I also wanted to do this with Stream
but I didn’t find a nice, clean solution)
which you would use as:
futureR.thenAccept(r -> executeInSequence(r,
this::sequentialFutureTask1,
this::sequentialFutureTask2,
this::sequentialFutureTask3));
or if you prefer to pass futureR
as parameter:
@SafeVarargs
public final <R> void executeAfter(CompletionStage<R> futureR,
Function<? super R, ? extends CompletionStage<?>> firstTask,
Function<? super R, ? extends CompletionStage<?>>... otherTasks) {
futureR.thenAccept(r -> {
CompletionStage<?> prev = firstTask.apply(r);
for (Function<? super R, ? extends CompletionStage<?>> task : otherTasks) {
prev = prev.thenCompose(ignored -> task.apply(r));
}
});
}
used as:
executeAfter(futureR,
this::sequentialFutureTask1,
this::sequentialFutureTask2,
this::sequentialFutureTask3);
Alternatively, this executeAfter()
can be implemented with one less parameter:
@SafeVarargs
public final <R> void executeAfter2(CompletionStage<R> futureR,
Function<? super R, ? extends CompletionStage<?>>... otherTasks) {
futureR.thenAccept(r -> {
CompletionStage<?> prev = futureR;
for (Function<? super R, ? extends CompletionStage<?>> task : otherTasks) {
prev = prev.thenCompose(ignored -> task.apply(r));
}
});
}
but this prev = futureR
causes an additional, useless thenCompose()
that might be confusing (performance impact should be negligible since futureR
is already completed when this will be invoked).
Upvotes: 1