Java Language Programmazione su un'interfaccia


Esempio

L'idea alla base della programmazione di un'interfaccia è di basare il codice principalmente sulle interfacce e utilizzare solo classi concrete al momento dell'istanziazione. In questo contesto, un buon codice che riguarda, ad esempio, le raccolte Java avrà un aspetto simile a questo (non che il metodo stesso sia di qualche utilità, solo illustrazione):

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

mentre il codice errato potrebbe apparire come questo:

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

Non solo il primo può essere applicato a una più ampia scelta di argomenti, i suoi risultati saranno più compatibili con il codice fornito da altri sviluppatori che generalmente aderiscono al concetto di programmazione di un'interfaccia. Tuttavia, i motivi più importanti per utilizzare il primo sono:

  • il più delle volte il contesto in cui viene utilizzato il risultato non richiede e non dovrebbe richiedere molti dettagli come prevede l'implementazione concreta;
  • aderire a un'interfaccia costringe un codice più pulito e meno hacks come un altro metodo pubblico viene aggiunto a una classe che serve uno scenario specifico;
  • il codice è più testabile in quanto le interfacce sono facilmente raggirabili;
  • infine, il concetto aiuta anche se è prevista solo un'implementazione (almeno per testabilità).

Quindi, come si può facilmente applicare il concetto di programmazione a un'interfaccia quando si scrive un nuovo codice avendo in mente una particolare implementazione? Un'opzione che usiamo comunemente è una combinazione dei seguenti modelli:

  • programmazione su un'interfaccia
  • fabbrica
  • costruttore

L'esempio seguente basato su questi principi è una versione semplificata e troncata di un'implementazione RPC scritta per un numero di protocolli diversi:

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

L'interfaccia di cui sopra non dovrebbe essere istanziata direttamente tramite una factory, ma deriviamo ulteriori interfacce concrete, una per l'invocazione HTTP e una per AMQP, ognuna delle quali ha una factory e un builder per costruire istanze, che a loro volta sono anche esempi di l'interfaccia di cui sopra:

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

Le istanze di RemoteInvoker per l'uso con AMQP possono ora essere costruite con la stessa facilità con cui (o sono più coinvolte a seconda del costruttore):

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

E l'invocazione di una richiesta è facile come:

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

A causa di Java 8 che consente il posizionamento di metodi statici direttamente nelle interfacce, lo stabilimento intermedio è diventato implicito nel codice sopra sostituito con AmqpInvoker.with() . In Java precedente alla versione 8, lo stesso effetto può essere ottenuto con una classe Factory interna:

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

L'istanziazione corrispondente si trasformerebbe quindi in:

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

Il builder usato sopra potrebbe apparire come questo (sebbene si tratti di una semplificazione in quanto quello attuale consente di definire fino a 15 parametri che si discostano dai valori predefiniti). Nota che il costrutto non è pubblico, quindi è specificamente utilizzabile solo dalla precedente interfaccia di 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);
  }
}

Generalmente, un builder può anche essere generato usando uno strumento come FreeBuilder.

Infine, l'implementazione standard (e l'unica prevista) di questa interfaccia è definita come una classe package-local per imporre l'uso dell'interfaccia, della factory e del builder:

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) {
    ...
  }
}

Nel frattempo, questo schema si è dimostrato molto efficace nello sviluppo di tutto il nostro nuovo codice, non importa quanto semplice o complessa sia la funzionalità.