Java Reactor 中嵌套 flatMap 的好习惯用法是什么?
What is a good idiom for nested flatMaps in Java Reactor?
我继承了使用 Spring 和相关库(包括 Reactor)以 Java 编写的 REST 服务的职责。对于 REST 调出或数据库操作等昂贵的操作,代码将结果广泛包装在 Reactor Mono 中。
代码中有各种各样的事情需要解决,但不断出现的事情是嵌套 flatMap
s over Mono
s 用于最终缩进的昂贵操作序列几个层次深入到一个不可读的混乱中。我发现它特别令人讨厌,因为我来自 Scala,在 Scala 中,这种使用 flatMap
的方式并没有那么糟糕,因为 for comprehension 语法糖将所有内容保持在大致相同的范围级别而不是更深入。
到目前为止,除了大规模重构之外,我还没有成功找到一种方法来解决这个问题以使其更具可读性(即便如此,我也不确定从哪里开始这样的重构)。
基于代码的匿名示例,(所有语法错误均来自匿名):
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;
}
哎呀。我不喜欢该片段的一些地方,但我将从大的地方开始 - 嵌套。
嵌套的唯一原因是,在(例如)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 -> service.expensiveOp2(d1.foo()).map(d2 -> new IntermediateResult1(d1, d2)))
.flatMap(i1 -> service.expensiveOp3(i1).map(s3 -> new IntermediateResult2(i1, d3)))
//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)
,同时放弃 setter 调用。
我继承了使用 Spring 和相关库(包括 Reactor)以 Java 编写的 REST 服务的职责。对于 REST 调出或数据库操作等昂贵的操作,代码将结果广泛包装在 Reactor Mono 中。
代码中有各种各样的事情需要解决,但不断出现的事情是嵌套 flatMap
s over Mono
s 用于最终缩进的昂贵操作序列几个层次深入到一个不可读的混乱中。我发现它特别令人讨厌,因为我来自 Scala,在 Scala 中,这种使用 flatMap
的方式并没有那么糟糕,因为 for comprehension 语法糖将所有内容保持在大致相同的范围级别而不是更深入。
到目前为止,除了大规模重构之外,我还没有成功找到一种方法来解决这个问题以使其更具可读性(即便如此,我也不确定从哪里开始这样的重构)。
基于代码的匿名示例,(所有语法错误均来自匿名):
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;
}
哎呀。我不喜欢该片段的一些地方,但我将从大的地方开始 - 嵌套。
嵌套的唯一原因是,在(例如)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 -> service.expensiveOp2(d1.foo()).map(d2 -> new IntermediateResult1(d1, d2)))
.flatMap(i1 -> service.expensiveOp3(i1).map(s3 -> new IntermediateResult2(i1, d3)))
//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)
,同时放弃 setter 调用。