Соединение нескольких CompletionStage только в том случае, если условие достигнуто

У меня есть несколько методов CompletionStage, которые я хотел бы связать. Проблема в том, что результат первого будет определять, должны ли выполняться следующие. Прямо сейчас единственный способ добиться этого - это передать "специальные" аргументы в следующий CompletionStage, чтобы он не выполнял полный код. Например:

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;
    });
}

Так как весь код зависит от первого someCondition (если он false, тогда результат будет RESULT_1, если не тогда весь код должен быть выполнен), эта конструкция выглядит немного уродливой для меня. Есть ли способ решить, должны ли выполняться методы 2-го (thenCompose(...)) и 3-го (thenApply(...))?

Ответ 1

Вы можете сделать это следующим образом:

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());
}

Вместо одного CompletableFuture мы создаем два, представляя различные пути выполнения, которые мы могли бы предпринять. Операция loooooong представляется в виде runnable then и намеренно завершает один из этих CompletableFuture. Последующие этапы привязаны к кадру, представляющей выполненное условие, то оба пути выполнения соединяются на последнем шаге applyToEither(shortCut, Function.identity()).

Будущее будущего shortCut уже является типом конечного результата и будет завершено с RESULT_1, результатом вашего пути прохождения null, что приведет к немедленному завершению всей операции. Если вам не нравится зависимость между первым этапом и фактическим значением результата короткого замыкания, вы можете отменить его следующим образом:

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());
}

Если ваш третий шаг не был образцовым, но выглядел точно так, как показано в вопросе, вы можете объединить его с шагом присоединения кодового пути:

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);
}

то мы пропустим только второй шаг, вызов someMethodThatReturnsACompletionStage, но он все еще может стоять за длинной цепочкой промежуточных шагов, все пропущенные без необходимости развертывания ручного пропуска через nullcheck.

Ответ 2

Для полноты я добавляю новый ответ

Несмотря на то, что решение, предложенное @Holger, прекрасно работает для меня. Решение, которое я использовал, включает разделение различных потоков в разных вызовах методов и их объединение с помощью 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);
}

ПРИМЕЧАНИЕ: я изменил алгоритм из вопроса ради более полного ответа

Все длительные операции могут быть потенциально завернуты внутри другого CompletableFuture.supplyAsync с небольшим усилием

Ответ 3

Если вам нужно проверять только нулевые значения, вы можете решить, используя Optional. Например, вы должны сделать:

public Bar execute(String id) {

      return this.getFooById(id)
            .thenCompose(this::checkFooPresent)
            .thenCompose(this::doSomethingElse)
            .thenCompose(this::doSomethingElseMore)
            .thenApply(rankRes -> new Bar(foo));

}


private Optional<Foo> getFooById(String id) {

    // some better logic to retrieve foo

    return Optional.ofNullable(foo);
}


private CompletableFuture<Foo> checkFooPresent(Optional<Foo> optRanking) {

    CompletableFuture<Foo> future = new CompletableFuture();
    optRanking.map(future::complete).orElseGet(() -> future.completeExceptionally(new Exception("Foo not present")));
    return future;
}

checkFooPresent() получает Optional, и, если его значение равно null, оно исключительно завершает CompletableFuture.

Очевидно, вам нужно управлять этим исключением, но если вы ранее установили ExceptionHandler или что-то подобное, оно должно прийти бесплатно.