Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Des interfaces fluides extensibles avec les Self-Bounded Generics

Certains objets complexes nécessitent d'être construits ou paramétrés en plusieurs étapes, comme les composants d'une interface graphique (définition de leur taille, position, couleur...) ou les requêtes SQL (génération des clauses select, from, order by...),
Ces étapes sont matérialisées par un ensemble de méthodes, que le développeur doit appeler dans un ordre précis pour réaliser le processus de construction, au terme duquel l'objet finalisé lui est retourné. Ces méthodes peuvent appartenir à l'objet lui-même, ou à un objet externe implémentant le design pattern Builder.

Afin de simplifier la vie du développeur et de rendre le code plus lisible, il est intéressant de coder ces méthodes de manière à pouvoir chaîner leurs appels : l'objet possède alors une interface dite "fluide" (fluent en anglais).
Il suffit pour cela que chaque méthode renvoie l'instance courante de l'objet auquel elle appartient :

  1. public class Foo {
  2. public Foo method() {
  3. (...)
  4. return this;
  5. }
  6. }

Cet article expose la principale limitation de cette approche et propose une méthode simple pour la contourner, grâce à la technique des Self-Bounded Generics.

Exemple : BasicQueryBuilder

Tout au long de l'article, je prendrai comme exemple un générateur de requêtes SQL.

En voici une implémentation très basique, qui gère les clauses select, from, et where :

  1. public class BasicQueryBuilder {
  2.  
  3. protected StringBuilder buf = new StringBuilder();
  4.  
  5. public String getQuery() {
  6. return buf.toString();
  7. }
  8.  
  9. public BasicQueryBuilder select(String... fields) {
  10. buf.append("SELECT ");
  11. buf.append(join(", ", fields));
  12. return this;
  13. }
  14.  
  15. public BasicQueryBuilder from(String... tables) {
  16. buf.append("\nFROM ");
  17. buf.append(join(", ", tables));
  18. return this;
  19. }
  20.  
  21. public BasicQueryBuilder where(String... criteria) {
  22. buf.append("\nWHERE ");
  23. buf.append(join(" and ", criteria));
  24. return this;
  25. }
  26.  
  27. protected String join(String separator, Object... objects) {
  28. StringBuilder builder = new StringBuilder();
  29. boolean first = true;
  30. for (Object o : objects) {
  31. if (first) {
  32. first = false;
  33. } else {
  34. builder.append(separator);
  35. }
  36. builder.append(o);
  37. }
  38. return builder.toString();
  39. }
  40.  
  41. }

Elle s'utilise comme suit :

  1. // Trouver les liens vers TheCodersBreakfast.net
  2. BasicQueryBuilder builder = new BasicQueryBuilder();
  3. builder.select("visit.referrer")
  4. .from("visit","blog")
  5. .where("visit.blogId=blog.id", "blog.name='TheCodersBreakfast.net'");
  6. System.out.println(builder.getQuery());

Ce qui donne le résultat souhaité :

SELECT visit.referrer
FROM visit, blog
WHERE visit.blogId=blog.id and blog.name='TheCodersBreakfast.net'

Interfaces fluides et extensibilité : AdvancedQueryBuilder

Les utilisateurs du BasicQueryBuilder réclament maintenant des fonctionnalités supplémentaires, comme la gestion des clauses orderBy, groupBy et having.

Pour une quelconque raison, il nous est interdit de modifier le code source du BasicQueryBuilder ; nous allons donc le sous-classer :

  1. public class AdvancedQueryBuilder extends BasicQueryBuilder {
  2.  
  3. public AdvancedQueryBuilder orderBy(String... orders) {
  4. buf.append(" ORDER BY ");
  5. buf.append(join(", ", orders));
  6. return this;
  7. }
  8.  
  9. public AdvancedQueryBuilder groupBy(String... groups) {
  10. buf.append(" GROUP BY ");
  11. buf.append(join(", ", groups));
  12. return this;
  13. }
  14.  
  15. public AdvancedQueryBuilder having(String... criteria) {
  16. buf.append(" HAVING ");
  17. buf.append(join(" and ", criteria));
  18. return this;
  19. }
  20.  
  21. }

Notez que le type de retour des méthodes est désormais AdvancedQueryBuilder et non plus BasicQueryBuilder.

Nous devrions maintenant pouvoir construire des requêtes plus complexes :

  1. // Sélectionner les blog français influents
  2. AdvancedQueryBuilder builder = new AdvancedQueryBuilder();
  3. builder.select("blog.name","count(visit.id)")
  4. .from("visit", "blog")
  5. .where("blog.language='fr'")
  6. .groupBy("blog.name")
  7. .having("count(visit.id) > 1000");
  8. System.out.println(builder.getQuery());

Mais cela ne compile pas !
Le problème se situe à la ligne 6. En effet, la méthode where() appartient à la classe BasicQueryBuilder et renvoie donc une instance de ce type... sur laquelle on ne peut pas appeler la méthode groupBy(), qui n'est définie que dans AdvancedQueryBuilder !

Une solution qui pourrait venir à l'esprit serait de modifier les signatures des méthodes de la classe BasicQueryBuilder afin qu'elle renvoie des instances de AdvancedQueryBuilder.
D'un point de vue architecture, ce serait évidemment une catastrophe ; en particulier, les méthodes de la classe mère ne pourraient plus être appelées que via sa classe fille...

Les Self-Bounded Generics à la rescousse

Il nous faut donc trouver un système permettant à la classe mère de renvoyer dynamiquement le bon type d'instance, en fonction de son contexte d'appel (depuis la classe elle-même ou depuis l'une de ses sous-classes).

Depuis Java 5, il est possible de paramétrer dynamiquement une instance de classe grâce aux types paramétrés (generics en anglais). Vous utilisez ce système tous les jours avec l'API Collections :

  1. List<Blog> blogs = new ArrayList<Blog>()
  2. blogs.add(new Blog());

Amélioration du BasicQuerybuilder

Dans l'exemple ci-dessus, le fait de paramétrer la classe ArrayList avec le type Blog a modifié la signature de ses méthodes. Ainsi, la méthode add() a accepté une instance de Blog en paramètre, au lieu d'un simple Object. Nous allons utiliser le même système pour définir le type de retour des méthodes de notre générateur de requêtes :

  1. public class BasicQueryBuilder<T> {
  2.  
  3. public T select(String... fields) {
  4. buf.append("SELECT ");
  5. buf.append(join(", ", fields));
  6. return (T) this;
  7. }
  8.  
  9. (idem avec les autres méthodes)
  10. }

Désormais, le type de retour des méthodes sera celui du type paramétré de la classe.
Mais attention, le cast de la ligne 6 impose que ce type soit compatible avec BasicQueryBuilder, c'est-à-dire BasicQueryBuilder lui-même ou l'une de ses sous-classes. Nous devons donc modifier la définition de la classe comme ceci :

  1. public class BasicQueryBuilder<T extends BasicQueryBuilder<?>>
  2. { ... }

Nous voyons ici pourquoi cette technique est appelée "Self-Bounded Generics" : le type de la classe apparaît dans l'expression de son paramètre.

Amélioration de l'AdvancedQuerybuilder

Voyons maintenant les modifications à apporter à l'AdvancedQueryBuilder.

Tout d'abord, il faut modifier sa définition de manière à paramétrer sa classe mère :

  1. public class AdvancedQueryBuilder extends BasicQueryBuilder<AdvancedQueryBuilder>
  2. { ... }

Grâce à cela, les méthodes héritées de BasicQueryBuilder renvoient bien des instances de type AdvancedQueryBuilder, et le code qui posait problème tout à l'heure fonctionne désormais :

  1. // Sélectionner les blog français influents
  2. AdvancedQueryBuilder<?> builder = new AdvancedQueryBuilder<AdvancedQueryBuilder<?>>();
  3. builder.select("blog.name","count(visit.id)")
  4. .from("visit", "blog")
  5. .where("blog.language='fr'")
  6. .groupBy("blog.name")
  7. .having("count(visit.id) > 1000");
  8. System.out.println(builder.getQuery());

Et l'on obtient :

SELECT blog.name, count(visit.id)
FROM visit, blog
WHERE blog.language='fr'
GROUP BY blog.name
HAVING count(visit.id) > 1000

But wait, there's more !

Nous avons maintenant un BasicQueryBuilder et un AdvancedQueryBuilder joliment architecturés et extensibles.
Extensibles, vraiment ? Et si les utilisateurs réclament un SuperAdvancedQueryBuilder avec encore davantage de fonctionnalités ?

Afin d'être totalement extensibles, il est nécessaire de répéter le processus de générification à tous les niveaux de la hiérarchie.
La définition de l'AdvancedQueryBuilder devient logiquement :

  1. public class AdvancedQueryBuilder<T extends AdvancedQueryBuilder<?>> extends BasicQueryBuilder<T>
  2. { ... }

Et il s'instancie comme ceci :

  1. AdvancedQueryBuilder<?> builder = new AdvancedQueryBuilder<AdvancedQueryBuilder<?>>();
  2. (...)

SuperAdvancedQueryBuilder

Et pour satisfaire nos utilisateurs, développons donc un SuperAdvancedQueryBuilder sur le même modèle :

  1. public class SuperAdvancedQueryBuilder<T extends SuperAdvancedQueryBuilder<?>> extends AdvancedQueryBuilder<T> {
  2.  
  3. public T comment(String comment) {
  4. buf.append("\n-- ").append(comment);
  5. return (T) this;
  6. }
  7.  
  8. }

Qui s'utilise comme ceci :

  1. SuperAdvancedQueryBuilder<?> builder = new SuperAdvancedQueryBuilder<SuperAdvancedQueryBuilder<?>>();
  2. builder
  3. .select("blog.name", "count(visit.id)")
  4. .from("visit", "blog")
  5. .where("blog.language='fr'")
  6. .groupBy("blog.name")
  7. .having("count(visit.id) > 1000")
  8. .comment("This is a comment");
  9. System.out.println(builder.getQuery());

Et donne ce résultat :

SELECT blog.name, count(visit.id)
FROM visit, blog
WHERE blog.language='fr'
GROUP BY blog.name
HAVING count(visit.id) > 1000
-- This is a comment

Conclusion

La technique des Self-Bounded Generics demande une certaine maîtrise des types paramétrés, mais permet de développer des interfaces fluides automatiquement extensibles.
La prochaine fois que vous implémentez de telles interfaces, pensez à ce pattern !

Le code source est disponible en annexe de ce billet.


Commentaires

1. Le vendredi 18 décembre 2009, 02:44 par Alexandre Bertails

Dejà, je trouve que l'article est sympa, bien écrit et clair. Mais j'ai envie d'être tatillon ce soir :)

"Depuis Java 5, il est possible de paramétrer dynamiquement une instance de classe grâce aux types paramétrés" -> le transtypage est introduit à la compilation, il n'y a rien de dynamique ici.

"le fait de paramétrer la classe ArrayList avec le type Blog a modifié la signature de ses méthodes" -> non, la classe List est dejà compilée quelque part et donc la méthode n'est pas modifiée. Par contre, le compilateur va introduire du transtypage là où le type paramétré était attendu.

Alexandre.

2. Le vendredi 18 décembre 2009, 10:44 par Snoop Dog

Super article,

Je vais pouvoir supprimer mes overrides dans les classes filles !!!

Merci

3. Le vendredi 18 décembre 2009, 10:55 par Piwaï

Approche intéressante... Notons que l'ajout des génériques rends le code légèrement plus lourd pour l'utilisation simple d'un BasicQueryBuilder, il faut désormais écrire :

BasicQueryBuilder<BasicQueryBuilder> builder = new BasicQueryBuilder<BasicQueryBuilder>();

(pour le coup, ça fait un peu bizarre).

D'autres part, le chaînage de méthodes est très intéressante, mais ici rien n'empêche une mauvaise utilisation due à un appel des méthodes dans le mauvais ordre..

Autrement dit ? Je peux faire ça :

builder.from("visit", "blog")
.select("blog.name", "count(visit.id)")
.groupBy("blog.name")
.comment("This is a comment")
.where("blog.language='fr'")
.having("count(visit.id) > 1000");

=> Ce code va compiler, mais bien entendu génèrera une erreur au Runtime.

Idéalement, le "dsl" ainsi créer devrait empêcher à la compilation de telles erreurs...

Des solutions à proposer (ou des liens/ressources traitant du sujet) ? J'avoue que je viens tout juste de me poser la question...

On pourrait probablement s'en sortir en créant un certain nombre d'interfaces qui limitent les opérations possibles et en retournant le builder emballé dans l'interface plutôt que celui-ci directement.

Du genre (j'écris ça au fil de l'eau, pour le principe) :

public AfterSelectBuilder select(String... fields) {
...
return this;
}

public interface AfterSelectBuilder {

 AfterSelectBuilder from(String... tables);

}

public interface AfterFromBuilder{

 AfterWhereBuilder where(String... criteria);
 AfterGroupByBuilder groupBy(String... groups);

}

etc etc...

Ce qui, à l'utilisation, permet au compilateur de valider le bon enchainement d'appel, et même d'avoir une auto complétion bien pratique...

Une autre option serait de permettre l'appel des méthodes de manière non ordonnée, et de sauver l'effet de chaque appel séparément, pour ensuite tout assembler dans le bon ordre dans getQuery.

Hésitez pas à me faire signe si je dis des conneries ;-) .

4. Le vendredi 18 décembre 2009, 10:58 par Piwaï

Pfiouuu, des erreurs se sont glissées dans mon commentaire.

ainsi créer => ainsi créé (la honte!)

et surtout, le type de retour de from() n'était pas bon :

public AfterSelectBuilder select(String... fields) {
...
return this;
}

public interface AfterSelectBuilder {

AfterFromBuilder from(String... tables);

}

public interface AfterFromBuilder{

AfterWhereBuilder where(String... criteria);
AfterGroupByBuilder groupBy(String... groups);

}

5. Le lundi 21 décembre 2009, 23:09 par Olivier Croisier

@Alexandre Tu as parfaitement raison, j'ai effectivement pris quelques raccourcis dans mon explication !

@Piwai Evidemment, il faudrait une véritable machine à états pour garantir la correction de la syntaxe SQL générée. Mais c'était hors du périmètre de l'article.

6. Le mardi 22 décembre 2009, 06:06 par Piwaï

Je ne sais pas si l'on peut parler véritablement de machine à état : je pensais plus à une aide à la compilation, en emballant dans des interfaces, et n'ayant aucun impact sur l'état des objets manipulés au runtime. Il s'agit simplement d'interdire certains enchaînements à la compilation :-) .

Mais tu as raison, c'est certainement hors scope :-) .

7. Le mardi 22 décembre 2009, 23:54 par Epo

@Piwaï
http://www.factorypattern.com/metho...
Dans cette article, il donne un exemple de comment arriver à cela. C'est du C# mais à part les majuscules sur les noms de methodes aucune difficulté de traduction.

8. Le mercredi 10 février 2010, 18:29 par Pascal LAVAUX

J'avoue que j'ai un peu de mal avec l'utilisation des Generics (je dois me faire vieux ;-), l'article est intéressant mais le code est un peu lourd et quand on fait de la maintenance d'un code qui n'est pas le sien on le préfère léger. J'avais déjà fait ce constat lorsque les Template avaient été ajoutés au C++.

9. Le vendredi 21 mai 2010, 17:20 par Lvi

Salut, tombé par hasard sur cet article, très intéressant !

Néanmoins, la solution proposée me paraît très élégante au niveau formel, mais pas tant que ça pour le développeur qui va créer les objets en question :
Je note que rien ne contraint ledit développeur à "self-bounder", il semble légal d'écrire :
new BasicQueryBuilder<AdvancedQueryBuilder<?>>() => sans grand intérêt,
ou new AdvancedQueryBuilder<BasicQueryBuilder<?>>() => erreurs possibles au chainage.
Un nouveau développeur peut mettre du temps à comprendre que seule l'utilisation avec la même classe en paramètre a un intérêt.
De plus, et c'est dommage, la déclaration "self-boundée" est carrément redondante.

J'ai trouvé une solution sans java 5, avec un mélange d'héritage et de délégation, c'est assez verbeux mais une fois codé, cela permet aux utilisateurs d'un AdvancedQueryBuilder de pas se poser de questions :)
(Enfin... tant que la méthode join de BasicQueryBuilder est pas private...)

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.