décembre
2007
Comment simplifier son code grace aux fluent interfaces. J’ai pas mal parcouru le net sur le sujet, et je vous livre un résumé de mes pérégrinations…
L’objectif est de se simplifier la vie, le code et la maintenance en allegeant le code au maximum. Imaginons un objet Commande qui a pas mal d’attributs, et contient une liste de lignes de commandes.
au lieu d’écrire le classique
commande.setType(AR); // accusé reception demandé
commande.setTruc1(...); // euh, oui, j'ai pas d'imagination...
commande.setTruc2(...);
commande.setTruc3(...);
commande.getLignes().add(new LigneCommande());
on voudrait pouvoir ecrire (exemple simple)
.dateEnvoi(..)
.truc1(...)
.truc2(...)
.with(new LigneCommande());
Il sagit de chain programming, déjà utilisé dans certaines api java par exemple dans l’api nio (regardez les methodes des buffers, elles renvoient toutes l’objet).
Plus leger, plus simple, potentiellement lisible par un profane en programmation. On peut pousser le concept beaucoup plus loin que sur cet exemple un peu naïf en creant un veritable petit « langage » dédié a cet objet ou a un ensemble d’objets. Imaginez par exemple manipuler l’ horrible api Date/Calendar de java en tappant
ou bien
Un exemple plus complexe de l’api JMock, ou finalement on approche le concept de Domain Specific Language.
mock.expects(once()).method("m").with( or(stringContains("hello"), stringContains("howdy")));
Un autre exemple est le projet Quaere, un genre de linq pour java.
Ca vous donne des idées ? (y’a aussi jrules, l’api criteria de hibernate etc… )
La façon la plus evidente pour faire cela est d’ajouter ces methodes a l’objet sur lequel on veut travailler.
En plus des geter/seters, on peut y ajouter les methodes supplementaires pour avoir cette syntaxe.
this.setTruc1(truc);
return this;
}
Simple, mais bon, le nombre de methodes va exploser, on va melanger les methodes, en gros c’est le bordel ! On s’éloigne de l’objectif, et on ne peut pas ajouter simplement le return this; au seter, car ce n’est plus conforme aux normes du java bean
La méthode proposée par martinfowler.com consiste a avoir un sous objet qui lui dispose de ces methodes.
On crée cette classe.
private Commande commande;
public CommandeFluentInterface(Commande commande) {
this.commande = commande;
}
public CommandeFluentInterface truc1(String truc1) {
commande.setTruc1();
return this;
}
public CommandeFluentInterface truc2(String truc2) {
commande.setTruc2();
return this;
}
public Commande finish() {
return commande;
}
}
public class Commande {
private CommandeFluentInterface commandeFluentInterface;
public Commande() {
commandeFluentInterface = new commandeFluentInterface(this);
}
public CommandeFluentInterface with() {
return commandeFluentInterface ;
}
...
}
on arrive a un code du type
.dateEnvoi(..)
.truc1(...)
.truc2(...)
.with(new LigneCommande())
.finish();
Pas mal, mais cette méthode a selon moi de gros defauts. Déjà on modifie du code existant pour avoir des « facilités » syntaxiques, ce qui pour un gros projet n’est pas forcement possible. Du code stable et terminé, on n’y touche plus. On mélange code de type Model avec du code de Factory/builder. Enfin on ajoute un attribut a tous les objets que l’on veut traiter de cette facon, ce qui peut ne pas etre acceptable (si par exemple cette objet est beaucoup instanciée dans l’application, ca peut jouer sur les perfs etc…)
Oh, mais j’ai parlé de Builder, c’est un design pattern connu ca ! on a qu’a essayer avec. En gros, CommandeFluentInterface devient un CommandeBuilder externe.
private Commande commande;
public CommandeBuilder(Commande commande) {
this.commande = commande;
}
public CommandeBuilder truc1(String truc1) {
commande.setTruc1()
return this;
};
public CommandeBuilder truc2(String truc2) {
commande.setTruc2()
return this;
};
public Commande finish() {
return commande;
}
...
}
du coup, on va coder
.dateEnvoi(..)
.truc1(...)
.truc2(...)
.with(new LigneCommande())
.finish();
Ca commence a etre pas mal. joli, simple, code de construction decouplé de l’objet lui-même, donc on peut ajouter ce genre de choses a un projet existant, sans modifier le code deja présent.
Mais en mélangeant tout ca, avec les satic imports de java 5, on fini par avoir un truc vraiment tres bien (ca vient de ce blog)
public static CommandeBuilder initCommandeAR() {
return new CommandeBuilder(new Commande());
}
public static CommandeBuilder initCommandeAR() {
CommandeBuilder commandeBuilder = new CommandeBuilder(new Commande());
commandeBuilder.withAR();
return commandeBuilder();
}
public static final class CommandeBuilder {
private Commande commande;
public CommandeBuilder(Commande commande) {
this.commande = commande;
}
public CommandeBuilder truc1(String truc1) {
commande.setTruc1();
return this;
};
public CommandeBuilder truc2(String truc2) { commande.setTruc2() );
return this;
};
public Commande finish() {
return commande;
}
...
}
}
et la on peut faire
...
Commande commmande = initCommandeAR()
.dateEnvoi(..)
.truc1(...)
.truc2(...)
.with(new LigneCommande())
.finish();
Commande commmande2 = initCommande()
.dateEnvoi(..).finish();
evidement, l’exemple choisi (la commande si vous suivez) est simpliste, mais cela permet de s’attquer a une api complete
http://stephan.reposita.org/archives/2007/10/17/creating-a-fluent-interface-for-google-collections/
voila voila. Vous en pensez quoi vous ? c’est totalement inutile ? vous preferez une solution a une autre ? En tous cas, c’est un sujet a la mode, probablement parce que les DSL sont interessants, mais que leur construire un outillage approprié n’est pas facile (support dans un ide etc…) : du coup les implementer dans un langage existant est tres utile.
D’ailleurs de nombreuses propositions pour java 7, si elles sont retenues vont permettre beaucoup de choses dans le domaine
Closures, les méthodes d’extension, chained methods
multiples sources qui sont la base de ce blog (beaucoup de copié collé dans ce post )
martinfowler Bliki
static imports fluent interfaces/
fluent-interfaceand-reflection
creating-a-fluent-interface-for-google-collections
HowTo DesignA FluentInterface
http://andersnoras.com/blogs/anoras/default.aspx
Pour moi, un sujet lié est le thème du dernier livre de Kent Beck (voir article sur http://social.hortis.ch/2007/11/08/implementation-patterns-by-kent-beck/)
J’adore cette approche DSL afin de créer un vocabulaire et une syntaxe autour du domaine traité. Par ailleurs les builders, même s’il augmente le nombre de classes, participe à l’isolation des classes du model.
Dernier point, le Test driven development fonctionnel est un bon moyen de mettre en place un DSL : http://clecuret.blogspot.com/2007/11/myxpcorkboard-article-2-modlisation-par.html
je l’avais pas vu passer celle la ! va falloir se faire un DSL pour requeter les propositions pour java 7 !
Ca y est j’ai retrouvé le nom : les self-types.
Et là c’est plus facile pour retrouver le lien : http://www.jroller.com/scolebourne/entry/java_7_self_types
Ce sera surement plus clair
a++
Non je ne parlais pas des chained méthods mais d’une autre proposition (bien qu’assez similaire).
Avec les chained methods, c’est toutes les méthodes retournant void qu’il est possible de chainé.
Avec la proposition dont je parlais, il faut spécifier this en type de retour par exemple ceci :
class A {
public this add(int x) {
// …
return this;
}
}
Et toutes les classes fille de A hériteront d’une méthode add() retournant un objet du type de la classe fille et non pas du type de A.
Actuellement c’est impossible à faire il faut redéfinir la méthode dans la classe fille…
Il faut vraiment que je retrouve le lien ce serait plus clair…
a++
le lien sur les chained methods est a la fin de mon post
(a 8 lignes de la fin precisement)
+1
Il y a toutefois un défaut lorsqu’on veut l’utiliser cela avec des classes filles, car les méthodes retourneront un type correspondant au parent à moins de les modifier…
Cela serait résolu par les « chained methods ».
A noter également l’existence d’une proposition qui consisterais a utiliser this comme type de retour, afin que la méthode retourne implicitement la référence this dans le bon type (désolé je ne retrouve plus le lien).
Sinon on pourrait utiliser directement les méthodes sans les get/set comme dans l’exemple de la classe ProcessBuilder, que je trouve bien plus lisible en fait :
// getter :
File dir = builder.directory();
// setter :
builder.directory(new File(« … »));
Le problème comme tu le dis c’est que cela ne respecte pas les conventions des Java Beans… à moins de proposer un BeanInfo (mais s’il faut le faire pour chaque classe ce serait un peu lourd :().
a++
oui, elle est d’ailleur me semble-t-il ultra inspirée de l’api criteria d’hibernate
Très intéressant!
On trouve déjà ce genre de construction dans Java EE 5 (dans JPA, avec les objets javax.persistence.Query).