Что такое хорошая идиома для вложенных flatMaps в Java Reactor? - PullRequest
1 голос
/ 23 сентября 2019

Я унаследовал ответственность за службу REST, написанную на Java с использованием Spring и связанных библиотек, включая Reactor.Для дорогостоящих операций, таких как вызовы REST или операции с базой данных, код тщательно оборачивает результаты в Reactor Mono.

В коде есть все, что нужно учитывать, но то, что постоянно появляется, является вложенным flatMap s больше Mono s для последовательностей дорогостоящих операций, которые в конечном итоге смещают несколько уровней глубоко в нечитаемый беспорядок.Я нахожу это очень утомительным, потому что я пришел из Scala, где этот способ использования flatMap не так плох, потому что синтаксический сахар для понимания поддерживает все примерно на одном уровне, а не углубляется.

IДо сих пор не удалось найти способ приблизиться к этому, чтобы сделать его более читабельным, кроме массивного рефакторинга (и даже тогда я не уверен, с чего начать такой рефакторинг).

Анонимный пример на основе кода, (все синтаксические ошибки от анонимизации):

public Mono<OutputData> userActivation(InputData input) {
    Mono<DataType1> d1 = service.expensiveOp1(input);

    Mono<OutputData> result =
        d1
          .flatMap(
            d1 -> {
              return service
                  .expensiveOp2(d1.foo())
                  .flatMap(
                      d2 -> {
                        if (Status.ACTIVE.equals(d2.getStatus())) {
                          throw new ConflictException("Already active");
                        }

                        return service
                            .expensiveOp3(d1.bar(), d2.baz())
                            .flatMap(
                                d3 -> {
                                  d2.setStatus(Status.ACTIVE);

                                  return service
                                      .expensiveOp5(d1, d2, d3)
                                      .flatMap(
                                          d4 -> {
                                            return service.expensiveOp6(d1, d4.foobar())
                                          });
                                });
                      });
            })

    return result;
}

1 Ответ

2 голосов
/ 24 сентября 2019

Тьфу.Несколько вещей, которые мне не нравятся в этом фрагменте, но я начну с большого - вложения.

Единственная причина вложения заключается в том, что в (например) expensiveOp5() вам нуженссылка на d1, d2 и d3, а не просто d4 - так что вы не можете просто отобразить "нормально", потому что вы потеряете эти более ранние ссылки.Иногда возможно рефакторинг этих зависимостей в определенном контексте, поэтому я сначала изучу этот маршрут.

Однако, если это невозможно или нежелательно, я склонен находить глубоко вложенные flatMap() вызовы, такие какэто лучше всего заменить промежуточными объектами через композицию.

Если у вас есть несколько классов, например, таких как:

@Data
class IntermediateResult1 {
    private DataType1 d1;
    private DataType2 d2;
}

@Data
class IntermediateResult2 {
    public IntermediateResult2(IntermediateResult1 i1, DataType3 d3) {
        this.d1 = i1.getD1();
        this.d2 = i1.getD2();
        this.d3 = d3;
    }
    private DataType1 d1;
    private DataType2 d2;
    private DataType3 d3;
}

... и так далее, тогда вы можете просто сделать что-токак:

return d1.flatMap(d1 -> new IntermediateResult1(d1, service.expensiveOp2(d1.foo())))
         .flatMap(i1 -> new IntermediateResult2(i1, service.expensiveOp3(i1.getD1().bar(), i1.getD2().baz())))
         //etc.

Конечно, вы также можете затем разбить вызовы на их собственные методы, чтобы сделать его более понятным (что я, вероятно, посоветует в этом случае):

return d1.flatMap(this::doOp1)
         .flatMap(this::doOp2)
         .flatMap(this::doOp3)
         .flatMap(this::doOp4)
         .flatMap(this::doOp5);

Очевидно, что имена, которые я использовал выше, следует рассматривать только как заполнители - вы должны тщательно обдумать эти имена, поскольку хорошее именование здесь сделает рассуждения и объяснения реактивного потока гораздо более естественными.

ПомимоВложенность, два других момента, которые стоит отметить в этом коде:

  • Используйте return Mono.error(new ConflictException("Already active")); вместо того, чтобы бросать явно, так как это сильноБолее того, вы имеете дело с явным Mono.error в потоке.
  • Никогда не использует изменяемые методы, такие как setStatus() на полпути через реактивную цепочку, - это требует проблем позже.Вместо этого используйте что-то вроде with pattern , чтобы сгенерировать новый экземпляр d2 с обновленным полем.Затем вы можете позвонить по номеру expensiveOp5(d1, d2.withStatus(Status.ACTIVE), d3), лишившись этого звонка сеттера.
...