Java Language Programmation à une interface


Exemple

L'idée derrière la programmation d'une interface est de baser le code principalement sur des interfaces et d'utiliser uniquement des classes concrètes au moment de l'instanciation. Dans ce contexte, un bon code traitant par exemple des collections Java ressemblera à ceci (non pas que la méthode elle-même soit d'aucune utilité, mais seulement une illustration):

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

alors que le code incorrect peut ressembler à ceci:

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

Non seulement le premier peut être appliqué à un plus grand choix d'arguments, mais ses résultats seront plus compatibles avec le code fourni par d'autres développeurs qui adhèrent généralement au concept de programmation à une interface. Cependant, les raisons les plus importantes pour utiliser le premier sont:

  • la plupart du temps, le contexte dans lequel le résultat est utilisé ne nécessite pas et ne devrait pas nécessiter autant de détails que la mise en œuvre concrète le prévoit;
  • adhérer à une interface force le code plus propre et moins de piratage, mais une autre méthode publique est ajoutée à une classe desservant un scénario spécifique;
  • le code est plus testable car les interfaces sont facilement modifiables;
  • enfin, le concept aide même si une seule implémentation est attendue (au moins pour la testabilité).

Alors, comment peut-on facilement appliquer le concept de programmation à une interface lors de l'écriture d'un nouveau code en ayant à l'esprit une implémentation particulière? Une option couramment utilisée est la combinaison des modèles suivants:

  • programmation à une interface
  • usine
  • constructeur

L'exemple suivant basé sur ces principes est une version simplifiée et tronquée d'une implémentation RPC écrite pour différents protocoles:

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

L'interface ci-dessus n'est pas supposée être instanciée directement via une fabrique, au lieu de cela nous dérivons d'autres interfaces plus concrètes, une pour l'invocation HTTP et une pour AMQP, chacune ayant une fabrique et un générateur pour construire des instances. l'interface ci-dessus:

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

Les instances de RemoteInvoker pour l'utilisation avec AMQP peuvent maintenant être construites aussi facilement (ou plus impliquées selon le constructeur):

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

Et l'invocation d'une demande est aussi simple que:

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

Java 8 permettant de placer des méthodes statiques directement dans les interfaces, la fabrique intermédiaire est devenue implicite dans le code ci-dessus remplacé par AmqpInvoker.with() . En Java avant la version 8, le même effet peut être obtenu avec une classe Factory interne:

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

L'instanciation correspondante deviendrait alors:

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

Le générateur utilisé ci-dessus pourrait ressembler à ceci (bien qu'il s'agisse d'une simplification car celle-ci permet de définir jusqu'à 15 paramètres différents des valeurs par défaut). Notez que la construction n'est pas publique, elle n'est donc utilisable que depuis l'interface AmqpInvoker ci-dessus:

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);
  }
}

Généralement, un générateur peut également être généré à l'aide d'un outil tel que FreeBuilder.

Enfin, l'implémentation standard (et la seule attendue) de cette interface est définie comme une classe locale de package pour appliquer l'utilisation de l'interface, de la fabrique et du générateur:

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

Pendant ce temps, ce modèle s'est avéré très efficace pour développer tout notre nouveau code, quelle que soit la simplicité ou la complexité de la fonctionnalité.