编程到接口

编程到接口背后的想法是将代码主要基于接口,并且在实例化时仅使用具体类。在这种情况下,处理例如 Java 集合的好代码看起来像这样(不是该方法本身有任何用处,只是说明):

public <T> Set<T> toSet(Collection<T> collection) {
  return Sets.newHashSet(collection);
}

虽然坏代码可能如下所示:

public <T> HashSet<T> toSet(ArrayList<T> collection) {
  return Sets.newHashSet(collection);
}

不仅前者可以应用于更广泛的参数选择,其结果将更加兼容其他开发人员提供的代码,这些代码通常遵循编程到接口的概念。但是,使用前者的最重要原因是:

  • 大多数情况下,使用结果的上下文不会也不应该像具体实现所提供的那样需要那么多细节;
  • 坚持一个接口强制更清洁的代码和更少的黑客,例如另一个公共方法被添加到服务于某些特定场景的类;
  • 代码更容易测试,因为接口很容易模拟;
  • 最后,即使只预期一个实现(至少对于可测试性),该概念也有帮助。

那么,在编写考虑到一个特定实现的新代码时,如何轻松地将编程概念应用于接口?我们通常使用的一个选项是以下模式的组合:

  • 编程到接口
  • 建设者

基于这些原则的以下示例是为许多不同协议编写的 RPC 实现的简化和截断版本:

public interface RemoteInvoker {
  <RQ, RS> CompletableFuture<RS> invoke(RQ request, Class<RS> responseClass);
}

上面的接口不应该直接通过工厂实例化,而是我们派生出更具体的接口,一个用于 HTTP 调用,另一个用于 AMQP,每个接口都有一个工厂和一个构建实例的构建器,这些实例也是以上界面:

public interface AmqpInvoker extends RemoteInvoker {
  static AmqpInvokerBuilder with(String instanceId, ConnectionFactory factory) {
    return new AmqpInvokerBuilder(instanceId, factory);
  }
}

与 AMQP 一起使用的 RemoteInvoker 实例现在可以像构建一样简单(或者更多地依赖于构建器):

RemoteInvoker invoker = AmqpInvoker.with(instanceId, factory)
  .requestRouter(router)
  .build();

并且调用请求就像这样简单:

Response res = invoker.invoke(new Request(data), Response.class).get();

由于 Java 8 允许将静态方法直接放入接口,因此中间工厂已经隐含在上面用 AmqpInvoker.with() 替换的代码中。在版本 8 之前的 Java 中,使用内部 Factory 类可以实现相同的效果:

public interface AmqpInvoker extends RemoteInvoker {
  class Factory {
    public static AmqpInvokerBuilder with(String instanceId, ConnectionFactory factory) {
      return new AmqpInvokerBuilder(instanceId, factory);
    }
  }
}

相应的实例化将变为:

RemoteInvoker invoker = AmqpInvoker.Factory.with(instanceId, factory)
  .requestRouter(router)
  .build();

上面使用的构建器可能看起来像这样(虽然这是一个简化,因为实际允许定义多达 15 个偏离默认值的参数)。请注意,该构造不是公共的,因此它只能从上面的 AmqpInvoker 接口中特别使用:

public class AmqpInvokerBuilder {
  ...
  AmqpInvokerBuilder(String instanceId, ConnectionFactory factory) {
    this.instanceId = instanceId;
    this.factory = factory;
  }

  public AmqpInvokerBuilder requestRouter(RequestRouter requestRouter) {
    this.requestRouter = requestRouter;
    return this;
  }

  public AmqpInvoker build() throws TimeoutException, IOException {
    return new AmqpInvokerImpl(instanceId, factory, requestRouter);
  }
}

通常,也可以使用 FreeBuilder 之类的工具生成构建器。

最后,此接口的标准(以及唯一预期的)实现被定义为包本地类,以强制使用接口,工厂和构建器:

class AmqpInvokerImpl implements AmqpInvoker {
  AmqpInvokerImpl(String instanceId, ConnectionFactory factory, RequestRouter requestRouter) {
    ...
  }

  @Override
  public <RQ, RS> CompletableFuture<RS> invoke(final RQ request, final Class<RS> respClass) {
    ...
  }
}

同时,这种模式在开发所有新代码时非常有效,无论功能有多简单或复杂。