程式設計到介面

程式設計到介面背後的想法是將程式碼主要基於介面,並且在例項化時僅使用具體類。在這種情況下,處理例如 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) {
    ...
  }
}

同時,這種模式在開發所有新程式碼時非常有效,無論功能有多簡單或複雜。