如何安全地装饰现有的回调?

How can I safely decorate an existing callback?

假设我正在使用以下回调 API:

/**
 * Registers a new action which will be run at some later time on
 * some other thread, clearing any previously set callback action.
 *
 * @param callback an action to be run later.
 * @returns the previously registered action.
 */
public Runnable register(Runnable callback);

我想注册我自己的操作,但我想保留任何设置的行为。换句话说,我希望我的操作看起来像:

new Runnable() {
  public void run() {
    // do my work

    originalCallback.run();
  }
}

向我的 Runnable 提供 originalCallback 的最干净的方法是什么?

我想到的幼稚解决方案有引入 window 时间的风险,其中 originalCallback 在调用回调时不可用,或者涉及一些复杂的锁定。

经过更多挖掘,我发现了 Guava 的 SettableFuture and Java 8's CompletableFuture。我会把我的 BlockingSupplier 留给后代,但这些 Future 实现中的任何一个都将更标准,并且工作方式相同。


您基本上需要一个带阻塞 get() 方法的支架 class。像这样:

public class BlockingSupplier<E> implements Supplier<E> {
  private final CountDownLatch latch = new CountDownLatch(1);
  private volatile E value;

  public synchronized void set(E value) {
    checkState(latch.getCount() > 0, "Cannot call set more than once.");
    this.value = value;
    latch.countDown();
  }

  @Override
  public E get() {
    latch.await(); // will block until set() is called
    return value;
  }
}

然后就可以这样使用了:

BlockingSupplier<Runnable> supplier = new BlockingSupplier<>();
// Pass the BlockingSupplier to our callback
DecoratorCallback myAction = new DecoratorCallback(supplier);
// Register the callback, and set the BlockingSupplier to the old callback
supplier.set(register(myAction));

其中 DecoratorCallbackrun() 看起来像这样:

public void run() {
  // do my work

  // This will block until supplier.set() returns
  originalCallbackSupplier.get().run();
}

正如 durron597 提到的,有更好的方法来设计回调 API,但考虑到问题中的 API,这似乎是合理的。

这是获得 API 的糟糕方式。 Single Responsibility Principle 适用于此。按照您现在的操作方式,您的可运行对象负责:

  1. 无论其他工作是什么
  2. 正在调用另一个回调。

您的 API 设计本身就违反了 SRP!每个使用这个 API 的 class 都已经从一开始就坏了。

幸运的是,您可以使用 Guava 的 ListenableFuture 轻松解决这个问题,其工作原理如下:

  1. 提交任务
  2. 取回 ListenableFuture 对象
  3. 使用 Futures.addCallback
  4. 附加回调

这样做可以确保您的系统将管理多线程和 happensBefore 关系的代码放在一个地方,而实际执行工作的代码放在另一个地方。