Publié par

Il y a 11 ans -

Temps de lecture 10 minutes

Commencer l’injection de dépendances avec Tapestry IoC

Quand on parle d’injection de dépendances, on pense tout de suite à Spring qui se tient sous les feux de la rampe. On peut aussi penser au petit dernier Guice abordé dans l’article Google Guice 2 : Les bases de l’injection de dépendances. Mais il ne faudrait pas oublier Tapestry 5 qui, lui aussi, fournit sa solution pour l’injection de dépendances. Tapestry IoC, à ne pas confondre avec le framework de développement Web Tapestry 5, est très fortement inspiré de Guice. Son but affiché est de tirer le meilleur de Guice tout en apportant l’héritage de son grand frère défunt Hivemind. On garde donc l’objectif zéro XML en le remplacant par du code Java. Parmi les avantages de cette technique, on citera :

  • Le démarrage d’une application est plus rapide avec une configuration IOC en annotations Java qu’avec une configuration XML à parser
  • On peut tester unitairement les modules d’injection puisqu’il s’agit de classe Java simple
  • Finie la configuration laborieuse en XML

J’ai même envie de rajouter que l’apprentissage d’une API Java est bien plus rapide que celui d’une syntaxe XML. Tout comme Guice, Tapestry IoC se concentre sur l’injection de dépendances et ne tente pas de fournir une stack complète de développement comme le fait Spring. On ne trouve ni les Aspects, ni le pattern Template cher à Spring. Pour répondre aux besoins de programmation par aspect, Tapestry IoC propose des interceptors qui peuvent décorer les services. D’autre part, dans Tapestry IoC, l’injection est définie dans un ou plusieurs module(s) chacun d’eux pouvant contribuer à la configuration et aux services de l’application.

Dans ce premier article de la série Tapestry 5 qui commence aujourd’hui, nous verrons les bases de l’injection avec Tapestry IoC et les différences par rapport à Spring et Guice.

L’injection par Builder

Pour commencer, prenons un service qui utilise un DAO.

public MyServiceImpl implements MyService {

  private final MyDao dao;

  public MyServiceImpl(MyDao dao){
     this.dao = dao;
  }
}

public MyAppModule {

  public static MyService build(MyDao dao){
    return new MyServiceImpl(dao);
  }
}

C’est la forme la plus simple de l’injection supportée par Tapestry. Ici on a directement la main sur l’instanciation du service. Notons que la classe MyAppModule n’implémente ou n’hérite d’aucune classe. Au lieu de cela, il suffit de fournir une méthode nommée ‘build’ que Tapestry utilise pour enregistrer le nouveau service. Si on veut fournir plusieurs services, on peut créer des méthodes buildXxxXxx prendra le nom à donner au service par exemple ‘buildMyDao’.

Pour ce qui est du paramètre dao, Tapestry injecte le premier service du même type qu’il trouve dans le registre. Cette méthode a le mérite d’être simple, mais ce n’est pas la méthode recommandée car elle force l’instanciation de tous les services au démarrage d’une part. D’autre part la méthode build est static ce qui n’est pas non plus recommandé notamment pour les tests unitaires.

Pour comprendre, il faut savoir que dans Tapestry, tous les services injectés sont en fait des proxies qui vont, par défaut, instancier le service à la première utilisation. Cela permet de charger l’application encore plus rapidement, de n’instancier que ce qui est réellement utilisé, et d’éliminer tout problème de dépendances cyclique.

Attention: Les services sont construits dans un scope ‘singleton’ par défaut où ‘per-thread’.

On joue ici sur les terres de Guice qui de son côté propose le bind to instance, équivalent. Bien sûr, on pourra faire un équivalent avec Spring en passant soit par une BeanFactory et du XML, soit par Spring JavaConfig @Bean.

Les Binders

Ici comme ailleurs, on peut activer le chargement retardé des services en passant par le Binder Tapestry. On va aussi pouvoir distinguer les différents services qui implémentent la même interface.

public MyAppModule {

  public void bind(ServiceBinder binder){
    binder.bind(MyService.class, MyServiceDaoImpl.class).withId("MyServiceDao");
    binder.bind(MyService.class, MyServiceXMLImpl.class).withId("MyServiceXML");
  }
}

Le ServiceBinder permet d’associer une classe à son implémentation. En plus de l’association interface-implémentation, on spécifie un identifiant qui distinguera notre service. Attention : l’identifiant du service doit être unique pour toute l’application (utilisez une ou plusieurs classes de constantes pour votre identifiants).

En revenant à notre classe MyServiceImpl, on peut se demander comment Tapestry va injecter le dao. C’est le constructeur avec le plus de paramètres qui est appelé. L’injection peut aussi être gérée par setter ou par des annotations de champs.

Si on imagine maintenant que plusieurs services implémentent l’interface MyDao, il faut récupérer la bonne instance avec @Inject.

public MyServiceImpl implements MyService {

  private final MyDao dao;

  @Inject
  private AnotherService otherService;

  public MyServiceImpl (@Inject("MyDao") MyDao dao){
     this.dao = dao;
  }
}

Avec l’annotation, on a choisi de récupérer le dao ayant pour identifiant « MyDao » dans le constructeur. On notera que le champ otherService est directement injecté sans passer par le constructeur. La méthode par constructeur est toutefois préférable pour obtenir des champs finaux et permettre l’injection de Mock dans les unitaires.
C’est également obligatoire pour les tests vraiment unitaires (en isolation) qui souhaitent remplacer « otherService » par un Mock ou un bouchon.

Faut-il le dire ? On voit clairement l’inspiration donnée par Guice, tant sur le ServiceBinder que sur l’annotation @Inject.

Attention: Contrairement à Guice et Spring, Tapestry IoC n’est pas sensible à la casse.

Tapestry recommande encore une autre méthode pour distinguer les services en utilisant les Markers. Il s’agit de créer une annotation qu’on utilisera à la place d’@Inject pour récupérer la bonne instance.

@Target(
{ PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
public @interface MyDaoHibernate
{

}

public MyAppModule {

  public static void bind(ServiceBinder binder){
    binder.bind(MyDao.class, MyDaoHibernateImpl.class).withId("MyDao").withMarkerId(MyDaoHibernate.class);
  }
}

public MyServiceImpl implements MyService {

  private final MyDao dao;

  public MyServiceImpl (@MyDaoHibernate MyDao dao){
     this.dao = dao;
  }
}

On a donc d’abord créé une annotation Runtime pouvant être appliquée sur des paramètres ou des champs. On l’a ensuite associée à la déclaration du service via le ServiceBinder. Enfin, on a injecté le paramètre du constructeur avec notre annotation marker.

Cette nouvelle approche peut paraître un peu lourde, mais elle a l’avantage de faciliter le refactoring et de réduire encore les risques liés à la configuration. Quoi que l’on fasse, il restera toujours des annotations à mettre dans le code source de nos services. Même si cette fois l’annotation utilisée vient de nos sources et pas d’un import tapestry5.ioc.XXX ce qui est moins intrusif il faut bien le reconnaître.
A mi-chemin entre la String de configuration et le Marker, utiliser une bonne vieille constante semble être un bon compromis.

Les modules

On en a vu un exemple plus haut, un Module est une classe POJO qui doit contenir des méthodes utilisant une convention de nom. Jusqu’à maintenant tous les exemples ont utilisé des méthodes statiques. Ce n’est absolument pas une obligation. On peut très bien utiliser des méthodes d’instance et stocker les services dans des champs pour réaliser un cache de services. Dans ce cas, l’injection des dépendances du module pourra se faire comme d’habitude dans le constructeur ou dans les champs.

Avec un peu d’aide, Tapestry IoC est capable de charger automatiquement tous les modules du classpath. Il suffit de les lister dans le Manifest du Jar. On ajoute donc une ligne à notre Manifest :

Tapestry-Module-Classes: org.example.mylib.MyAppModule, org.example.mylib.internal.InternalModule

C’est déjà bien, mais la liste des modules s’allongeant, cela peut vite devenir désagréable. Pour éviter de trop rallonger le Manifest, et pour se simplifier la vie, on va plutôt fournir une seule classe. Avec l’annotation @SubModule, on ajoute directement dans notre module ancêtre la liste des « sous-modules » à charger.

@SubModule({InternalModule.class})
public MyAppModule {

 /* ... */
}

Le registre, une fois lancé, pourra charger tous les modules du classpath. On voit déjà le gros avantage pour des plugins. Ils pourront par exemple être livrés séparément de notre application principale.

Les configurations

Un des points forts de Tapestry IoC est la notion de configuration distribuée. Le principe est de permettre d’une part, de passer une configuration à la construction d’un service. D’autre part, chaque module peut contribuer à la construction de la configuration de tous les services.

Imaginons par exemple un système d’export de fichiers associant un type à un FileExporter qui permet de générer l’export. Notre service fournit un PDFExporter et un CSVExporter. En basant notre FileExporterService sur une configuration, on pourra plus tard ajouter dans un autre module un WordExporter par exemple.

public MyFileExporterModule {

  public static FileExporterService buildFileExporterService(Map contributions)
  {
    return new FileExporterServiceImpl(contributions);
  }

  public static void contributeFileExporterService(MappedConfiguration configuration)
  {
    configuration.add("csv", new CSVFileExporter());
    configuration.add("pdf", new PDFFileExporter());
  }

}

On a créé un module publiant un service FileExporterService prenant une map de FileExporter en paramètre. On a pris soin d’ajouter à la map de configuration nos deux exports existants : le CSVFileExporter et le PDFFileExporter.

On peut maintenant ajouter un module fournissant un nouveau type d’export.

public static void contributeFileExporterService(MappedConfiguration configuration)
  {
    configuration.add("doc", new DocFileExporter());
  }

Cette fois, c’est un objet de type MappedConfiguration qui doit être pris en paramètre pour être reconnu par Tapestry IoC.

Il existe trois types de configuration supportés par Tapestry :

  • Configuration<T> liste non ordonnée d’objets, dans le builder on utilisera une Collection<T>
  • OrderedConfiguration<T> liste ordonnée, dans le builder on aura une List<T>
  • MappedConfiguration<S,T> dans le builder on aura une Map, les clés ne sont pas sensibles à la casse

Bien forcé de constater que ni Spring, ni Guice ne proposent quelque chose de similaire.

Lancer le registre

Il est temps d’utiliser nos modules dans une application. Il suffit de lancer le registre Tapestry en listant un, ou plusieurs modules à charger. On peut ensuite accéder à nos services.

public MyApplication {

  public static void main(String [] args) {
    RegistryBuilder builder = new RegistryBuilder();
    builder.add(MyAppModule.class);

    Registry registry = builder.build();
    registry.performRegistryStartup();

    MyService service = registry.getService(MyService.class);
    service.doSomething();

      //for operations done from this thread
    registry.cleanupThread();
      //call this to allow services clean shutdown
    registry.shutdown();
  }
}

La construction du registre n’est pas très compliquée, mais on peut déjà reprocher la lourdeur du système et l’absence de Helper pour faciliter la construction du registre. Devoir par exemple appeler successivement builder.build() puis registry.performRegistryStartup()
paraît étrange comparé à la concurrence.
Ce léger défaut est dû au fait que Tapestry IoC est plus fait pour le framework Tapestry 5 que pour une utilisation extérieure, historiquement en tout cas.

Pour conclure

Voilà un framework d’injection vraiment léger et facile à prendre en main. Du côté injection de dépendances, on apprécie la méthode par constructeur inspirée de Guice. Reste le problème des annotations dans les sources métiers qui sont trop intrusives à mon goût, même avec des Markers. Pourquoi ne pas permettre d’injecter les paramètres à fournir au constructeur directement dans le ServiceBinder du module ? Par contre, le chargement automatique des modules et la configuration distribuée, sont de réels plus auxquels on prend goût très rapidement.

Dans le prochain article, nous tenterons de mettre en avant les particularités de Tapestry IoC et ce que ce framework pourrait changer pour nous autres développeurs.

Pour aller plus loin

Publié par

Commentaire

1 réponses pour " Commencer l’injection de dépendances avec Tapestry IoC "

  1. Publié par , Il y a 11 ans

    Très bon article, merci.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.