在Java 8中如何使用先前链接的thenCompose lambdas中的值?

10

我的同事们偏爱的Java 8编码风格是通过链接异步调用,例如

CompletionStage<E> someMethod() {
    return doSomething().thenCompose(a -> {
      // ...
      return b;
    }).thenCompose(b -> {
      // ...
      return c;
    }).thenCompose(c -> {
      // ...
      return d;
    }).thenApply(d -> {
      // ...
      return e;
    });
}

我有类似上述的东西,但增加了一个挑战:我需要在后面的 lambda 中调用获取到的某些 lambda 中的值。例如,

CompletionStage<E> someMethod() {
    return doSomething().thenCompose(a -> {
      // ...
      Foo foo = fooDAO.getFoos(a);
      // ...
      return b;
    }).thenCompose(b -> {
      // ...
      return c;
    }).thenCompose(c -> {
      // ...
      Bar bar = barDAO.getBars(foo);
      // ...
      return d;
    }).thenApply(d -> {
      // ...
      return someResult(d, bar);
    });
}

当我在外部作用域中声明Foo foo;Bar bar;时,我会收到关于它们不是最终或有效最终的错误。我读到使用包装器使它们有效最终,但对我来说这似乎有些hacky(诡异),我不明白为什么允许这样做...

我读到Java 8没有添加对元组的支持(尽管它考虑了BiVal,也许我可以将其用于传递给BiFunction lambda)。所以我尝试使用一对(例如)

    return doSomething().thenCompose(a -> {
      // ...
      Foo foo = fooDAO.getFoos(a);
      // ...
      return new Pair<>(foo, b);
    }).thenCompose(fooAndB -> {

然后无论我在哪里需要回忆foo,

      Foo foo = fooAndB.getKey();

但这感觉语义上是错误的。而且它不起作用!我不知道为什么,因为我认为lambda参数的作用域与其外部作用域相同,所以所有lambda参数都可以从后续链接的lambda中访问。

lambda参数的真正作用域是什么?在保持链接的同时,是否有一种惯用或至少语义上无害的方法来完成我想做的事情?

基于打破链式结构的答案也可以,因为它们可能对未来的观众有用,但在我的情况下,偏离代码库中主导风格可能会导致拖延的PR会话和延迟的批准,因此我希望有一个保留链接的解决方案。或者,解释或演示保持链接会有多疯狂。谢谢!


尝试在外部范围中声明 AtomicReference<Foo>,然后使用 get 方法获取当前值。 - Hadi J
1
他提到他尝试过使用包装器(这就是在这里使用AtomicReference的方式),但发现它很不专业。 - RedDeckWins
嗯,我实际上找到了一种使用数组的方法,但不是AtomicReference。如果我能理解包装器所做的使编译器突然满意的差异,那么我就可以接受它...没有包装器的限制的目的是什么? - Andrew Cheong
啊,实际上你能把你的建议作为答案添加进来吗,@HadiJ?阅读关于AtomicReference的内容帮助我理解了如果没有正确地将变量包装在外部范围中会出现的并发问题。正是因为我找到的包装器只是一个只有一个元素的数组,所以我觉得这种策略有些巧妙。 - Andrew Cheong
使用AtomicReference也可以,根据您的方法并发性,如果您不想要或不需要使其线程安全的开销,您可能还可以创建自己的包装器。 - RedDeckWins
1
使用AtomicReference的缺点是,现在您的方法除了作为完整链之外无法进行测试。如果将POJO参数传递到链中,您可以将每个函数的逻辑潜在地提取到其自己的方法中并对其进行单元测试。然后,在您的主链接方法中,您提供方法引用。 - RedDeckWins
3个回答

7

既然你提到了同事们喜欢的编码风格,那么你可能已经知道了使用嵌套调用的替代方案:

CompletionStage<E> someMethod() {
    return doSomething().thenCompose(a -> {
        // ...
        Foo foo = fooDAO.getFoos(a);
        // ...
        CompletableFuture<B> resultB = ...;
        return resultB.thenCompose(b -> {
            // ...
            CompletableFuture<C> resultC = ...;
            return resultC;
        }).thenCompose(c -> {
            // ...
            Bar bar = barDAO.getBars(foo);
            // ...
            CompletableFuture<D> resultD = ...;
            return resultD.thenApply(d -> {
                // ...
                return someResult(d, bar);
            });
        });
    });
}

这将立即解决您的问题,但会牺牲一些可读性。不过,这个问题可以通过从您的代码中提取一些方法来轻松解决:

CompletionStage<E> someMethod() {
    return doSomething()
            .thenCompose(this::processA);
}

private CompletionStage<E> processA(final A a) {
    // ...
    Foo foo = fooDAO.getFoos(a);
    // ...
    final CompletableFuture<B> result = ...;
    return result
            .thenCompose(this::processB)
            .thenCompose(c -> processCAndFoo(c, foo));
}

private CompletionStage<C> processB(B b) {
    // ...
    return ...;
}

private CompletionStage<E> processCAndFoo(final C c, final Foo foo) {
    // ...
    Bar bar = barDAO.getBars(foo);
    // ...
    final CompletableFuture<D> result = ...;
    return result
            .thenApply(d -> someResult(d, bar));
}

通过这样做,你可以避免嵌套的lambda表达式(并尊重同事们喜欢的代码风格),同时提高了可读性和可测试性,因为现在有多个较小的方法,更易于理解和单元测试。

3
您可以尝试这种方法(我不一定支持这种做法,但它确实保持了链路)。您可以创建一个POJO,其中包含链路所需的所有参数,并将相同的POJO传递到链路中。这种方法的缺点是,添加额外的方法到链路中有些繁琐,因为现在您还必须向参数类添加属性。
public class SomeMethodContext {

    private Object a;
    private Object b;
    private Object c;
    private Object d;
    private Object foo;
    private Object bar;

    // Getters and setters

}

// ...

CompletionStage<E> someMethod() {
    return doSomething().thenCompose(a -> {
      SomeMethodContext context = new SomeMethodContext();  
      context.setA(a);
      // ...
      context.setFoo(fooDAO.getFoos(context.getA()));
      // ...
      context.setB(b);
      return context;
    }).thenCompose(ctxt -> {
      // ...
      ctxt.setC(c);
      return ctxt;
    }).thenCompose(ctxt -> {
      // ...
      ctxt.setBar(barDAO.getBars(ctxt.getFoo()))
      // ...
      ctxt.setD(d)
      return ctxt;
    }).thenApply(ctxt -> {
      // ...
      return someResult(ctxt.getD(), ctxt.getBar());
    });
}

我懂了,谢谢。这个答案给我暗示,链式编程更多地是一种风格,是由语言用户根据个人口味形成的,而不是由语言工作组认真研究开发的东西... 在我看来,链式编程只在不需要元组时才能很好地运行。 - Andrew Cheong
我看到你开始接受使用AtomicReference来保存值的想法了。那也可以。 - RedDeckWins
使用包装器的方式是完全正确的。在链条之外声明包装器会感觉很不专业(虽然这是可能的)。 - fps
1
问题在于,使用这种包装器/上下文持有者时,您很快就会失去有关设置内容、设置时间和位置的信息。随着代码的增长,很可能会将此包装器传递给多个方法和类,使其非常难以理解依赖关系。 - Didier L

1
作为另一种选择,您可能还可以考虑在项目中引入EA Async。该API为Java提供async/await,特别适合这种问题。下面是使用EA Async重写的代码示例:
CompletionStage<E> someMethodWithEAAsync() {
    final A a = await(doSomething());
    Foo foo = fooDAO.getFoos(a);
    // ...
    CompletableFuture<B> futureB = ...;
    final B b = await(futureB);
    // ...
    CompletableFuture<C> futureC = ...;
    final C c = await(futureC);
    // ...
    Bar bar = barDAO.getBars(foo);
    // ...
    CompletableFuture<D> futureD = ...;
    D d = await(futureD);
    return completedFuture(someResult(d, bar));
    // or alternatively
    return futureD.thenApply(d -> someResult(d, bar));
}

它看起来与同步代码非常相似,但在幕后,EA Async将转换此方法以保持所有内容异步。不再需要担心链接!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接