Callback Hell and CompletableFuture FTW
Async is sexy
Everyone knows that asynchronous is sexy, fancy and jazzy. That could be the end of this post.
But the truth is, writing async code is hard. Understanding async code is even harder. There are many mature frameworks/libraries which use promises, mostly in JavaScript world. AngularJS services: $q and $http - are my favourites ones.
Callback Hell in Vert.x world
Vert.x is also quite nice. But it’s written in callback manner. Some time ago I wanted to use Redis inside Verticle. Nothing special. But picture this scenario:
- Receive EventBus event
- Put something to Redis
- Put something other to Redis
- Then read something from Redis
- Finally return some value in EventBus event
It may look like hell…. Callback Hell. Just look at this example from JUnit test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void will_register_player(TestContext context) {
Async async = context.async();
hammerThrowTournament.addTournamentPlayer(new Player(1000, "Anita Wlodarczyk"), (AsyncResult<String> player1Handler) -> {
hammerThrowTournament.addTournamentPlayer(new Player(1001, "Zhang Wenxiu"), (AsyncResult<String> player2Handler) -> {
hammerThrowTournament.addTournamentPlayer(new Player(1002, "Alexandra Tavernier"), (AsyncResult<String> player3Handler) -> {
log.info("All players has been added");
hammerThrowTournament.registerdPlayer((AsyncResult<JsonArray> resultHandler) -> {
log.info("Looking for results");
int registerdPlayers = resultHandler.result().size();
context.assertEquals(registerdPlayers, 3, "All Players were registered");
async.complete();
});
});
});
});
}
Such code is even dificult to display in a blog post [sic!].
What is happening here? Poland is World best country in Hammer Throwing (which could also be the end of this post) :). We want to register 3 players in Hammer Throw Tournament. After registering we are checking that everything is ok.
That code looks ugly. My HammerThrowTournament
class in every method takes a Handler as a parameter which is passed into RedisClient
and is executed
when Redis operation ends. To be sure that one operation comes after another we end up with classic example of Callback Hell. Is there anything we can do with such code?
CompletableFuture FTW
Yes, we can!
Let’s start with wrapping all HammerThrowTournament
methods with CompletableFuture
. So instead of this:
1
2
3
4
5
public void addTournamentPlayer(Player p, Handler<AsyncResult<String>> handler) {
Map<String, String> hmset = new HashMap<>();
hmset.put("name", p.getName());
redisClient.hmset("PLAYERS:" + p.getId(), hmset, handler);
}
we have this:
1
2
3
4
5
6
7
8
9
public CompletableFuture<Void> addTournamentPlayer(Player p) {
CompletableFuture<Void> promise = new CompletableFuture<>();
Map<String, String> hmset = new HashMap<>();
hmset.put("name", p.getName());
redisClient.hmset("PLAYERS:" + p.getId(), hmset, (AsyncResult<String> event) -> {
promise.complete(null);
});
return promise;
}
Few lines of code more. But just take a look how my test is organized now.
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void will_register_player(TestContext context) {
Async async = context.async();
hammerThrowTournament.addPlayer(new Player(1000, "Anita Wlodarczyk"))
.thenCompose(v -> hammerThrowTournament.addPlayer(new Player(1001, "Zhang Wenxiu")))
.thenCompose(v -> hammerThrowTournament.addPlayer(new Player(1002, "Alexandra Tavernier")))
.thenCompose(v -> hammerThrowTournament.registerdPlayer())
.thenAccept(numberOfRegisteredPlayer -> {
context.assertEquals(numberOfRegisteredPlayer, 3, "All Player were registerd");
async.complete();
});
}
How beautiful it is! How easy to read! Where’s the magic?
Every addPlayer
method returns CompletableFuture<Void>
. With thenCompose
method we can pass CompletableFuture
result into another CompletableFuture
and achieve so nice chaining.
I’m not an CompletableFuture
expert. You can read more about it here
and if you know how to pronounce Anita Włodarczyk
correctly you can watch this presentation.
Code is available here: https://github.com/qrman/defeat-callback-hell. You can read commit history to track code transformation.
Good night and Good luck!