Java Reactor中嵌套flatMap的一个好习惯是什么? [英] What is a good idiom for nested flatMaps in Java Reactor?

查看:45
本文介绍了Java Reactor中嵌套flatMap的一个好习惯是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我继承了使用Spring和相关库(包括Reactor)以Java编写的REST服务的职责.对于诸如REST调用或数据库操作之类的昂贵操作,代码会将结果广泛包装在Reactor Mono中.

代码中需要解决各种问题,但不断出现的是嵌套在 Mono 上的 flatMap s,用于执行一系列昂贵的操作,最终缩进了几个层次,陷入了难以理解的混乱之中.我觉得这特别令人讨厌,因为我来自Scala,在这里使用 flatMap 的这种方式并不那么糟糕,因为它具有理解语法糖,可以将所有内容保持在大致相同的范围内,而不是深入了解.

到目前为止,我没有找到成功的方法来使它变得更可读,除了进行大规模的重构外,还没有成功(即使那样,我不确定从哪里开始这种重构).

基于代码的匿名示例(所有语法错误均来自匿名):

  public Mono< OutputData>userActivation(InputData输入){Mono< DataType1>d1 = service.expensiveOp1(input);Mono< OutputData>结果=d1.flatMap(d1->{退货服务.expensiveOp2(d1.foo()).flatMap(d2->{如果(Status.ACTIVE.equals(d2.getStatus())){抛出新的ConflictException("Already active");}退货服务.expensiveOp3(d1.bar(),d2.baz()).flatMap(d3->{d2.setStatus(Status.ACTIVE);退货服务.expensiveOp5(d1,d2,d3).flatMap(d4->{返回service.expensiveOp6(d1,d4.foobar())});});});})返回结果;} 

解决方案

糟糕.我不喜欢该片段的一些内容,但是我将从大的片段开始-嵌套.

嵌套的唯一原因是,例如在 expensiveOp5()中,您需要引用 d1 d2 d3 ,而不仅仅是 d4 -因此您不能仅通过正常"进行映射,因为您会丢失这些先前的引用.有时可以在特定的上下文中重构这些依赖关系,因此我将首先检查该路由.

但是,如果不可能或不理想,我倾向于找到这样的深层嵌套的 flatMap()调用,最好通过合成将其替换为中间对象.

例如,如果您有一堆类,例如:

  @Data类IntermediateResult1 {私有DataType1 d1;私有DataType2 d2;}@数据类IntermediateResult2 {public IntermediateResult2(IntermediateResult1 i1,DataType3 d3){this.d1 = i1.getD1();this.d2 = i1.getD2();this.d3 = d3;}私有DataType1 d1;私有DataType2 d2;私有DataType3 d3;} 

...等等,那么您可以执行以下操作:

 返回d1.flatMap(d1-> service.expensiveOp2(d1.foo()).map(d2-> new IntermediateResult1(d1,d2))).flatMap(i1-> service.expensiveOp3(i1).map(s3-> new IntermediateResult2(i1,d3)))//等等. 

当然,您还可以将调用分为自己的方法,以使其更加清晰(在这种情况下,我可能会建议这样做):

 返回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 模式之类的东西来生成 d2 ,其中包含一个已更新的字段.然后,您可以调用 expensiveOp5(d1,d2.withStatus(Status.ACTIVE),d3),同时取消该设置器调用.

I inherited responsibility for a REST service written in Java using Spring and associated libraries including Reactor. For expensive operations like REST calls out or database operations the code is extensively wrapping the results in Reactor Mono.

There are all sorts of things that need to be addressed in the code but one that keeps showing up is nested flatMaps over Monos for sequences of expensive operations that end up up indented several levels deep into an unreadable mess. I find it extra irksome because I came from Scala where this way of using flatMap is not as bad because of the for comprehension syntactic sugar keeping everything at roughly the same level of scope instead of going deeper.

I have so far had no success finding a way to approach this to make it more readable apart from a massive refactor (and even then I am not sure where to start such a refactor).

Anonymized example based on the code, (all syntax errors are from anonymization):

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

解决方案

Yuck. A few things I don't like about that snippet, but I'll start with the big one - the nesting.

The only reason for the nesting is that in (for example) expensiveOp5() you need a reference to d1, d2 and d3, not just d4 - so you can't just map through "normally", because you lose those earlier references. Sometimes it's possible to refactor these dependencies away in a particular context, so I'd examine that route first.

However, if it's not possible or desirable, I've tended to find deeply nested flatMap() calls like this are best replaced with intermediate objects through composition.

If you have a bunch of classes as follows for instance:

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

...and so on, then you can just do something like:

    return d1.flatMap(d1 -> service.expensiveOp2(d1.foo()).map(d2 -> new IntermediateResult1(d1, d2)))
             .flatMap(i1 -> service.expensiveOp3(i1).map(s3 -> new IntermediateResult2(i1, d3)))
             //etc.

Of course, you can also then break out the calls into their own methods to make it clearer (which I'd probably advise in this case):

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

Obviously, the names I've used above should be considered nothing but placeholders - you should think carefully about these names, as good naming here will make reasoning about and explaining the reactive stream much more natural.

Aside from the nesting, two other points worth noting in that code:

  • Use return Mono.error(new ConflictException("Already active")); rather than throwing explicitly, as it makes it much clearer that you're dealing with an explicit Mono.error in the stream.
  • Never use mutable methods like setStatus() half way through a reactive chain - that's asking for problems later on. Instead, use something like the with pattern to generate a new instance of d2 with an updated field. You can then call expensiveOp5(d1, d2.withStatus(Status.ACTIVE), d3) whilst forfeiting that setter call.

这篇关于Java Reactor中嵌套flatMap的一个好习惯是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆