How to refactor chain of asynchronous calls in vertx to avoid the callback hell

oscar :

I have the following code with several asynchronous calls depending on each other (calls can be apis REST, for example) and in the end process all the results. This is my sample code:

private void foo1(String uuid, Handler<AsyncResult<JsonObject>> aHandler) {
    //call json api, for example
    JsonObject foo1 = new JsonObject();
    foo1.put("uuid", "foo1");
    aHandler.handle(Future.succeededFuture(foo1));
 }

private void foo2(String uuid, Handler<AsyncResult<JsonObject>> aHandler) {

    //call json api, for example
    JsonObject foo2 = new JsonObject();
    foo2.put("uuid", "foo2");
    aHandler.handle(Future.succeededFuture(foo2));
 }

private void foo3(String uuid, Handler<AsyncResult<JsonObject>> aHandler) {

    //call json api, for example
    JsonObject foo3 = new JsonObject();
    foo3.put("uuid", "foo3");
    aHandler.handle(Future.succeededFuture(foo3));
 }

private void doSomething(JsonObject result1, JsonObject result2, JsonObject result3, Handler<AsyncResult<JsonObject>> aHandler) {
    JsonObject finalResult =new JsonObject();
    aHandler.handle(Future.succeededFuture(finalResult));
}

private void processToRefactor (String uuid, Handler<AsyncResult<JsonObject>> aHandler) {

    foo1(uuid, ar -> {
        if (ar.succeeded()) {
            JsonObject foo1 = ar.result();
            foo2(foo1.getString("uuid"), ar2 ->{
                if (ar2.succeeded()) {
                    JsonObject foo2 = ar2.result();
                    foo3(foo2.getString("uuid"), ar3 -> {
                        if (ar3.succeeded()) {
                            JsonObject foo3 = ar3.result();
                            doSomething(foo1, foo2, foo3, aHandler);
                        } else {
                            ar3.cause().printStackTrace();
                        }
                    });
                } else {
                    ar2.cause().printStackTrace();
                }
            });
        } else {
            ar.cause().printStackTrace();
        }

    });
}

In the previous code I have all the results available to use in the "doSomething" method if all the calls have been successful. I have tried to refactor this code using the simple "HelloWord" example from the following link https://streamdata.io/blog/vert-x-and-the-async-calls-chain/

This is my result:

    private void process(String uuid, Handler<AsyncResult<JsonObject>> aHandler) {

        Future<JsonObject> future = Future.future();
        future.setHandler(aHandler);

        Future<JsonObject> futureFoo1 = Future.future();

        foo1(uuid, futureFoo1);

        futureFoo1.compose(resultFoo1 -> {
            Future<JsonObject> futureFoo2 = Future.future();
            foo2(resultFoo1.getString("uuid"), futureFoo2);

            return futureFoo2; 
        }).compose(resultFoo2 ->{
            Future<JsonObject> futureFoo3 = Future.future();
            foo3(resultFoo2.getString("uuid"), futureFoo3);

            return futureFoo3;

        }).compose(resultFoo3 -> {

            // How to get result1, result2 and result3?
//            doSomething(resultFoo1, resultFoo2, resultFoo3, aHandler);

        }, future);
    }

The new code is cleaner and clearer but when using compose, at the moment of calling the function "doSomething" I do not have all the results of the calls available. How do I get all the results at the end of the chain?

On the other hand, how do you do if one of the apis call methods returns an array? That is to say, for each element of the array, a chain of functions is applied, independently of the fact that some have results and others do not. For example:

private void foo1Array(String uuid, Handler<AsyncResult<JsonArray>> aHandler) {

    //call json api that return array, for example
    JsonArray result = new JsonArray();
    JsonObject foo1 = new JsonObject();
    foo1.put("uuid", "foo1");

    JsonObject foo2 = new JsonObject();
    foo1.put("uuid", "foo2");

    JsonObject foo3 = new JsonObject();
    foo1.put("uuid", "foo3");

    result.add(foo1);
    result.add(foo2);
    result.add(foo3);

    aHandler.handle(Future.succeededFuture(result));
 }   
private void processArray(String uuid, Handler<AsyncResult<JsonObject>> aHandler) {

    Future<JsonObject> future = Future.future();
    future.setHandler(aHandler);

    Future<JsonArray> futureFoo1 = Future.future();

    foo1Array(uuid, futureFoo1);

    futureFoo1.compose(resultArray -> {
        List<Future> futures = new ArrayList<Future>();
        for (int i = 0; i < resultArray.size(); i ++) {
            JsonObject resultFoo1 = resultArray.getJsonObject(i);

            Future<JsonObject> futureFoo2 = Future.future();
            foo2(resultFoo1.getString("uuid"), aHandler);
            futures.add(futureFoo2);
        }

        CompositeFuture.any(futures).setHandler(ar -> {
            //What to do here?
        });

    }, future);
}

How to call functions foo2, foo3, ... with the result of foo1Array and then use it in doSomething?

Gerald Mücke :

Your initial approach is not too bad actually.

To improve code for better "composability", you should change the handler input arg of each fooX method to something that extends Handler<AsyncResult<JsonObject>> (such as a Future) and returns the same handler as a result, so it becomes better usable in the `Future.compose because the passed-in handler could be used as return value for each compose:

 private <T extends Handler<AsyncResult<JsonObject>>> T foo1(String uuid, T aHandler) {
    JsonObject foo1 = new JsonObject().put("uuid", "foo1");
    aHandler.handle(Future.succeededFuture(foo1));
    return aHandler; //<-- return the handler here
}

Second, in order to access all three results in final stage, you have to declare the three futures outside the chain. Now you can chain the futures quiet nicely using the output of each foo method as result for each compose.

Future<JsonObject> futureFoo1 = Future.future();
Future<JsonObject> futureFoo2 = Future.future();
Future<JsonObject> futureFoo3 = Future.future();


foo1(uuid, futureFoo1).compose(resultFoo1 -> foo2(resultFoo1.getString("uuid"), futureFoo2))
                      .compose(resultFoo2 -> foo3(resultFoo2.getString("uuid"), futureFoo3))
                      .compose(resultFoo3 -> doSomething(futureFoo1.result(), //access results from 1st call
                                                         futureFoo2.result(), //access results from 2nd call 
                                                         resultFoo3,
                                                         Future.<JsonObject>future().setHandler(aHandler))); //pass the final result to the original handler

If you can't live with the "impurity" of this approach (defining the futures outside chain and modify them inside the function), you have to pass the original input values for each method (=the output of the previous call) along with result, but I doubt this would make the code more readable.

In order to change type in one compose method, you fooX method has to make the conversion, not returning the original handler, but a new Future with the different type

private Future<JsonArray> foo2(String uuid, Handler<AsyncResult<JsonObject>> aHandler) {
    JsonObject foo2 = new JsonObject();
    foo2.put("uuid", "foo2" + uuid);
    aHandler.handle(Future.succeededFuture(foo2));
    JsonArray arr = new JsonArray().add("123").add("456").add("789");
    return Future.succeededFuture(arr);
}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=468265&siteId=1