Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 334
oscar Avatar asked Apr 12 '18 15:04

oscar


2 Answers

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);
}
like image 181
Gerald Mücke Avatar answered Nov 02 '22 09:11

Gerald Mücke


If you want simpler and no callback hell code then switch to rx, your above code will turn into something like this:

private Single<JsonObject> foo1(String uuid) {
    JsonObject foo1 = new JsonObject();
    foo1.put("uuid", "foo1");
    return Single.just(foo1);
 }

private Single<JsonObject> foo2(String uuid) {
    JsonObject foo2 = new JsonObject();
    foo2.put("uuid", "foo2");
    return Single.just(foo2);
 }

private Single<JsonObject> foo3(String uuid) {
    JsonObject foo3 = new JsonObject();
    foo3.put("uuid", "foo3");
    return Single.just(foo3);
 }

private Single<JsonObject> doSomething(String uuid) {
    JsonObject finalResult =new JsonObject();
    return Single.zip(foo1(uuid), foo2(uuid), foo3(uuid))
    .map(results -> {
        // Map zip results to finalResult
        return finalResult;
    });
}

private void processToRefactor (String uuid, Handler<AsyncResult<JsonObject>> aHandler) {
    doSomething(uuid).subscribe();
}
like image 29
Niraj Chauhan Avatar answered Nov 02 '22 10:11

Niraj Chauhan