Friday, 23 March 2007
Présentation de Guice
Mes excuses pour ceux qui attendent le troisième volet de mon tutorial sur comment créer un composant JSF, mais je viens de découvrir une nouvelle librairie tellement intéressante que je ne pouvais pas attendre pour en parler. Guice (prononcé comme juice, "jus" en anglais) est une nouvelle librairie de Google pour faciliter l'inversion de contrôle et l'injection des dépendances. Si vous connaissez le framework Spring ou l'annotation @javax.annotation.Resource, vous savez déjà combien ce design pattern peut rendre votre code plus souple, en le rendant plus indépendant des autres composants du programme. Je ne vais pas expliquer le design pattern ici car j'estime qu'il est déjà bien connu, et sinon les explications que vous pourrez trouver ailleurs sur la toile l'expliqueront mieux que je ne pourrais le faire ici.
Mais je pense que la plupart d'entre vous connaîtront déjà l'inversion de contrôle, l'ayant pratiqué avec le framework Spring. Alors la première question que vous allez demander c'est pourquoi apprendre à utiliser Guice si on connaît déjà Spring? Deux raisons principales sont à citer :
- La performance de l'inversion de contrôle avec Guice va typiquement entre 10 et 100 fois plus rapide que Spring. Ça veut dire qu'on peut envisager de l'utiliser dans les projets plus petits, les librairies utilitaires, etc., là où Spring pourrait être considéré comme trop lourd.
- Basé sur les annotations (introduites dans le langage Java dans sa version 5), Guice permet de définir les dépendances plus naturellement en langage Java uniquement, sans toucher à l'XML.
Bien sûr, Spring fait beaucoup plus que l'injection des dépendances, alors Guice ne peut pas et ne vise pas à remplacer ce framework. Mais, si votre besoin se limite à l'injection des dépendances, je crois qu'il y a des fortes raisons de préférer la nouvelle librairie ultraperformante de Google à Spring IOC. (D'ailleurs, Guice peut s'intégrer très bien avec le reste du framework Spring.)
Alors, comment ça marche? Prenons pour illustration un outil récent que j'ai codé qui écrit une table d'une base de données dans un fichier et peut le relire dans la table après. Il veut donc reposer sur des intérfaces afin de séparer le programme principal des différents types de fichier qu'on pourrait utiliser (.csv, .ixf, .xml, etc.). On a donc deux intérfaces, Table2File et File2Table. Dans notre programme principal on veut éviter de spécifier l'implémentation en dur dans le code, pour pouvoir le changer plus rapidement par la suite. Avec Guice, on code donc ceci dans notre classe principal :
public class DBCopy {
private Table2File t2f;
@Inject
public void setTable2File(Table2File t2f) {
this.t2f = t2f;
}
...
Et ainsi Guice saura qu'il faudra injecter ce valeur. Ensuite, pour faire le lien entre l'intérface et son implémentation, on crée non pas un fichier XML, mais une classe Java qui implémente Module. En voici un exemple (pour une implémentation qui emploie des fichiers de tableur comme transport) :
public class SpreadsheetModule extends AbstractModule {
protected void configure() {
bind.(Table2File.class).to(Table2FileTableurImpl.class);
}
}
Vous voyez, que c'est très court, facile à lire, et propre. Plus souple aussi, notre exemple ne le montre pas, mais on peut injecter plus que les simple setters, mais aussi des constructeurs, injecter des implémentations différentes selon les annotations sur le champ injecté, etc.—et ça reste facile à lire.
Alors, maintenant on a déclaré les champs qu'on veut injecter, et on a définit leurs implémentations dans une module. La dernière étape c'est de dire à Guice d'utiliser cette module pour injecter les dépendances. En voici le code :
...
public static void main(String[] args) {
//imaginons que arg[0] c'est "fr.craven.dbcopy.impl.SpreadsheetModule"
String moduleName = args[0];
Module module = (Module) Class.forName(moduleName).newInstance();
Injector injector = Guice.createInjector(module);
DBCopy copieur = new DBCopy();
injector.injectMembers(copieur);
copieur.start();
}
...
Là je passe le nom du module en paramètre, dans un vrai programme on le mettra dans un fichier properties, un paramètre d'initialisation, ou quelque chose comme ça. On pourrait aussi le mettre en dur dans le code, bien sûr, mais si le but de l'inversion de contrôle c'est d'éviter de lier notre code à une implémentation précise, je ne vois guère l'intérêt.
Idéalement, on peut structurer notre programme pour que ce seul appel à injector.injectMembers() injectera toutes les dépendances de notre programme, car Guice sait cascader dans les classes membres et injecter à son tour dans ces classes là. Par exemple, mon implémentation de Table2File qui utilise les fichiers de tableur utilise elle aussi des interfaces, SpreadsheetWriter et SpreadsheetReader, qui sont implémentés par des versions csv, Excel, et (bientôt) ODF. Si je les ajoute dans mon module (bind(SpreadsheetWriter.class).to(CsvWriterImpl.class);), Guice saura les injecter aussi.
Voilà pour cette petite introduction à Guice, je vous conseille de visiter son site pour en savoir plus et de l'essayer. D'après les ingénieurs de Google, cette librairie est déjà très mûr et est utilisé en interne chez Google dans des très grands projets (dont Struts 2). Guice est disponible sous licence Apache. Comme toujours, si mes explications ne sont pas claires ou vous avez des questions, n'hésitez pas à laisser vos commentaires.




