web-dev-qa-db-fra.com

Enchaînant plusieurs CompletionStage uniquement si une condition est remplie

J'ai plusieurs méthodes CompletionStage que j'aimerais enchaîner. Le problème est que le résultat du premier déterminera si les suivants doivent être exécutés. À l'heure actuelle, le seul moyen d'y parvenir semble être de passer des arguments "spéciaux" à la prochaine variable CompletionStage, de sorte qu'elle n'exécute pas le code complet. Par exemple:

public enum SomeResult {
    RESULT_1,
    RESULT_2,
    RESULT_3
}

public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {

    return CompletableFuture.supplyAsync(() -> {
        // loooooong operation
        if (someCondition)
            return validValue;
        else
            return null;
    }).thenCompose(result -> {
        if (result != null)
            return someMethodThatReturnsACompletionStage(result);
        else
            return CompletableFuture.completedFuture(null);
    }).thenApply(result -> {
        if (result == null)
            return ChainingResult.RESULT_1;
        else if (result.someCondition())
            return ChainingResult.RESULT_2;
        else
            return ChainingResult.RESULT_3;
    });
}

Puisque tout le code dépend de la première someCondition (si c'est false, le résultat sera RESULT_1; sinon, tout le code devrait être exécuté), cette construction me semble un peu moche. Existe-t-il un moyen de décider si les méthodes 2nd (thenCompose(...)) et 3rd (thenApply(...)) doivent être exécutées?

14
Pelocho

Vous pouvez le faire comme ça:

public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {
    CompletableFuture<SomeResult> shortCut = new CompletableFuture<>();
    CompletableFuture<ResultOfFirstOp> withChain = new CompletableFuture<>();

    CompletableFuture.runAsync(() -> {
        // loooooong operation
        if (someCondition)
            withChain.complete(validValue);
        else
            shortCut.complete(SomeResult.RESULT_1);
    });
    return withChain
        .thenCompose(result -> someMethodThatReturnsACompletionStage(result))
        .thenApply(result ->
                   result.someCondition()? SomeResult.RESULT_2: SomeResult.RESULT_3)
        .applyToEither(shortCut, Function.identity());
}

Au lieu d'un CompletableFuture, nous en créons deux, représentant les différents chemins d'exécution que nous pourrions emprunter. L’opération loooooong est alors soumise comme exécutable et complétera délibérément l’une de ces CompletableFuture. Les étapes de suivi sont chaînées à l'étape représentant la condition remplie, puis les deux chemins d'exécution se rejoignent à la dernière étape applyToEither(shortCut, Function.identity()).

Le shortCut future a déjà le type du résultat final et sera complété par le RESULT_1, résultat de votre chemin nullpassing, ce qui entraînera la fin immédiate de toute l'opération. Si vous n’aimez pas la dépendance entre la première étape et la valeur réelle du raccourci, vous pouvez la rétracter comme ceci:

public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {
    CompletableFuture<Object> shortCut = new CompletableFuture<>();
    CompletableFuture<ResultOfFirstOp> withChain = new CompletableFuture<>();

    CompletableFuture.runAsync(() -> {
        // loooooong operation
        if (someCondition)
            withChain.complete(validValue);
        else
            shortCut.complete(null);
    });
    return withChain
        .thenCompose(result -> someMethodThatReturnsACompletionStage(result))
        .thenApply(result ->
                   result.someCondition()? SomeResult.RESULT_2: SomeResult.RESULT_3)
        .applyToEither(shortCut.thenApply(x -> SomeResult.RESULT_1), Function.identity());
}

Si votre troisième étape n’est pas exemplaire, mais ressemble exactement à celle présentée dans la question, vous pouvez la fusionner avec l’étape de jonction du chemin de code:

public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {
    CompletableFuture<ResultOfSecondOp> shortCut = new CompletableFuture<>();
    CompletableFuture<ResultOfFirstOp> withChain = new CompletableFuture<>();

    CompletableFuture.runAsync(() -> {
        // loooooong operation
        if (someCondition)
            withChain.complete(validValue);
        else
            shortCut.complete(null);
    });
    return withChain
        .thenCompose(result -> someMethodThatReturnsACompletionStage(result))
        .applyToEither(shortCut, result -> result==null? SomeResult.RESULT_1:
            result.someCondition()? SomeResult.RESULT_2: SomeResult.RESULT_3);
}

alors nous ne faisons que sauter la deuxième étape, l'invocation someMethodThatReturnsACompletionStage, mais cela peut encore représenter une longue chaîne d'étapes intermédiaires, le tout étant ignoré sans qu'il soit nécessaire de déployer un saut manuel via nullcheck.

10
Holger

Par souci d'exhaustivité, j'ajoute une nouvelle réponse

Bien que la solution proposée par @Holger fonctionne à merveille, c'est un peu étrange pour moi. La solution que j'ai utilisée implique de séparer différents flux dans différents appels de méthode et de les chaîner avec thenCompose

public enum SomeResult {
    RESULT_1,
    RESULT_2,
    RESULT_3
}

public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {

    return CompletableFuture.supplyAsync(() -> {
        // loooooong operation
        if (someCondition)
            return operateWithValidValue(value);
        else
            return CompletableFuture.completedValue(ChainingResult.RESULT_1);
    })
        .thenCompose(future -> future);

public CompletionStage<SomeResult> operateWithValidValue(... value) {
     // more loooong operations...
     if (someCondition)
         return CompletableFuture.completedValue(SomeResult.RESULT_2);
     else
         return doFinalOperation(someOtherValue);   
}

public CompletionStage<SomeResult> doFinalOperation(... value) {
     // more loooong operations...
     if (someCondition)
         return CompletableFuture.completedValue(SomeResult.RESULT_2);
     else
         return CompletableFuture.completedValue(SomeResult.RESULT_3);
}

NOTE: J'ai changé l'algorithme de la question dans le but d'une réponse plus complète

Toutes les opérations longues pourraient être potentiellement intégrées à un autre CompletableFuture.supplyAsync avec peu d'effort

0
Pelocho