Vert.x 是一个异步框架。因此,它需要一种方法来表示可能尚未准备好但将来可用的值,也称为延迟值(deferred values)。您可能熟悉不同名称的延迟值:Promise, Future, Deferred, Mono, Uni 都是延迟值设计模式的实现。
Vert.x 有自己的延迟值实现,简称为 Future
,是Vert.x的核心概念之一。不过,这不应该与Java的 Future
混淆。Vert.x的Future实现是完全独立的。
💡提示: 尽管 Vert.x
Future
可以使用toCompletionStage()
方法转换为 Java的Future
。
Vert.x Future
有两个描述非常相似且可能会让您感到困惑的方法: compose
和 map
。
我想深入研究它们中的每一个是如何工作的,以及何时应该使用它们。
让我们从最简单的场景开始,看看一个完整的future:
Future<String> f = Future.succeededFuture("a");
此处, f
表示已成功计算的延迟值。
现在,让我们看看 compose()
和 map()
方法在延迟值上的行为,如果我们向它们传递一个简单的函数:给定 X,返回 X
Future<String> composeResult = f.compose(a -> Future.succeededFuture(a));
Future<String> mapResult = f.map(a -> a);
虽然 map()
允许我们按原样返回值,但 compose()
强制我们将结果包装在新的 Future
中。这是因为 map()
可以返回任何类型的结果,而 compose()
必须返回某种 Future
。
乍一看,compose()
似乎没有明显的好处。 两种方法都接收String
类型的值,但要返回普通的结果,compose()
需要一个额外的步骤。
但在我们完全放弃 compose()
方法之前,让我们看看另一个场景:
Future<Future<String>> f = Future.succeededFuture(Future.succeededFuture("a"));
在这里,我们将 Future
包装在另一个 Future
中。这个例子乍一看似乎有些牵强,但稍后我将向您展示这种嵌套在异步框架中非常常见。
当我们尝试在这个嵌套的 Future
上调用 map()
和 compose()
时会发生什么?
Future<String> composeResult = f.compose(a -> a);
Future<String> mapResult = f.map(a -> a.result());
虽然以前的 compose()
代码更复杂,但现在是 map()
代码需要执行一些额外的工作。
To unnest the Future
using map()
, we need to invoke the result()
method. With composeResult
, though, the value is readily accessible. That’s because compose flattens the Future
for us.
要使用 map()
取消嵌套 Future
,我们需要调用 result()
方法。但是,使用 composeResult
,该值很容易访问。那是因为compose 为我们扁平化了 Future
。
💡提示: 出于同样的原因,在 Vert.x 中
flatMap()
只是compose()
的别名。
因此,无论何时使用 Futures,compose()
实际上都是比 map()
更好的处理方式。
另一种查看方式是比较我们将identity function传递给两种方法时的结果:
Future<String> composeResult = f.compose(a -> a);
Future<Future<String>> mapResult = f.map(a -> a);
可视化此处发生的情况可能很有用:
注意颜色。 仅根据结果类型判断,您可以假设 compose()
将返回嵌套的 Future,而 map 将返回它接收到的相同对象。 这是不正确的。 compose()
和 map()
都返回一个新的 Future
。
但是 compose 将实际值包装成一个Future
,而 map 将底层的Future
包装成一个新值。
我之前承诺过,我将演示在真实的 Vert.x 代码中可以在何处进行Future
嵌套。 让我们看一下官方文档中的以下稍微简化的示例:
var future = client
.request(HttpMethod.GET, "some-uri")
.map(request -> request.send()
.map(response -> response
.body()
.map(buffer -> buffer.toJsonObject())));
此代码使用标准 Vert.x HttpClient 发送请求,然后读取响应正文并将其解析为 JSON。我相信你已经写了数百次类似的代码。
问题是,这里的结果是什么类型?
让我们明确类型:
Future<Future<Future<JsonObject>>> future = client
.request(HttpMethod.GET, "some-uri")
.map(request -> request.send()
.map(response -> response
.body()
.map(buffer -> buffer.toJsonObject())));
因为我们使用了 map()
,所以没有取消嵌套。 因此,为了读取 JSON,我们需要手动取消嵌套:
var json = future.result().result().result();
现在让我们用 compose() 替换 map() ,保持结果类型明确:
Future<JsonObject> future = client
.request(HttpMethod.GET, "some-uri")
.compose(request -> request.send()
.compose(response -> response
.body()
.map(buffer -> buffer.toJsonObject())));
结果类型现在很浅,并且易于使用:
var json = future.result();
请注意,最后一个方法仍然是 map()
。这是因为 body()
方法返回的是 Buffer
对象,而不是 Future
,因此无需平展它。
总结: 当 lambda 的参数为 Future
时,请使用 compose()
。当它是一个简单的对象时,请使用 map()
。
原文链接: https://alexey-soshin.medium.com/understanding-vert-x-compose-vs-map-d464dee78980