Posts Under code Category
Une histoire qui commence mal
OK, je tranche le malheureux pattern Visiteur a la vie dure; on ne l’aime pas trop, il est mal compris, et le pauvre est sous utilisé. Alors bon même s’il a ses défauts, pourquoi lui en vouloir autant, alors qu’il apporte justement ses avantages au code orienté objet.
Et oui vous avez bien lu orienté objet. Jusqu’à aujourd’hui j’ai vu du code qui ressemble à ça?
- On a soit des objets très complexes, avec des comportements qu’il n’est pas forcément intéressant de mettre dans l’objet même. Le code ci-dessous montre un objet ou les méthodes qui permettent de récupérer les livres d’un certain genre ne sont pas forcément appropriées dans cette partie du code. Pourquoi parce qu’il est envisageable (selon le bon sens) que d’autres genres serait apprécié. Et s’il faut ajouter d’autres méthodes encore.
public class FatObject { private Iterable<Book> books; public Iterable<Book> selectOnlySciFi() { ... } public Iterable<Book> selectOnlyThriller() { ... } public Iterable<Book> selectOnlyDetectiveStory() { ... } public Iterable<Book> selectOnlyRomance() { ... } public Iterable<Book> selectOnlyManga() { ... } } - Ou alors on a des objets anémiques (cf Martin Fowler) et le comportement est bien en dehors des objets traités, mais, et c’est la ça pèche, le comportement est délocalisé dans des helpers. Bref en gros c’est de la programmation procédurale, ce sont des structures qui sont manipulées par des fonctions, c’est du C avec des espaces de nommage (les classes *Helper.java). La programmation objet en prends un coup, pas étonnant que les principes objets ne marchent pas dans ce contexte, mais je diverge. Bref on a du code qui ressemble à ce qui suit. Un objet anémique qui ne fait rien. Au mieux il aura probablement les méthodes equals et hashCode et peut-être un toString.
public class AnemicObject { private Iterable<Book> books; public void setBooks(Iterable<Book> books) { this.books = books; } public Iterable<Book> getBooks() { return books; } @Override public boolean equals(Object o) { ... } @Override public int hashCode() { ... } }Et le démoniaque helper :
public class Helper { public void iDoSomethingWith(AnemicObject anemicObject) { ... } public Price iExtractTotalPriceFrom(AnemicObject anemicObject) { ... } public Iterable<Book> getSciFiBooks(AnemicObject anemicObject) { ... } public Iterable<Book> getDetectiveStoryBooks(AnemicObject anemicObject) { ... } }
Comme vous le voyez les deux exemples ci-dessus ne sont pas vraiment élégants, même si je préfère la première voie. A long terme ce n’est probablement pas une bonne idée. J’aimerais d’ailleurs avoir l’avis des gens du DDD?
Et c’est là que notre ami le visiteur va nous aider.
Pourquoi le visiteur nous aide, qu’apporte-t-il ?
Bonne question, ce pattern est souvent incompris, et pour cause, il ne porte pas un nom qui lui facilite la vie.
Et oui pour le coup un visiteur n’est pas fait pour visiter. Page 387 de la traduction française du livre Design Patterns (par le GoF), nous pouvons lire :
Le visiteur fait la représentation d’une opération applicable aux éléments d’une structure d’objet. Il permet de définir une nouvelle opération, sans qu’il soit nécessaire de modifier la classe des éléments sur lesquels il agit.
Effectivement aussi, ce livre donne comme un exemple un arbre. Et le visiteur prends toute sa puissance sur un arbre ou sur une structure composite. Mais ce n’est le seul cas ou celui-ci est utile, dans tous les cas il s’agit bien de permettre l’ajout / la suppression / la modification de comportements d’une manière objet sans retoucher à ce qui existe déjà. Je le répète le fait que le visiteur marche super bien sur un arbre est un bonus, mais le problème adressé, l’intention du visiteur n’est pas de visiter, mais de définir une nouvelle opération sans changer l’existant sur lequel il agit.
Il faut mesurer l’intérêt du visiteur suivant deux axes.
- S’il y a beaucoup d’objet du domaine qui peuvent avoir le même comportement, ou si la grappe de nœud d’un arbre est importante, un ou des visiteurs sera une bonne solution de conception pour mutualiser du code.
- S’il n’y a pas énormément d’objet du domaine, voir qu’un seul, mais que les comportements relatifs sont à la fois divers et volatiles. Alors le visiteur est un candidat pour ajouter des comportements sans faire de satané helper et sans avoir à modifier les éléments du domaine.
- Si vous avez des opérations différentes et un arbre ou des objets composite, le visiteur est le pattern pour vous, c’est la qu’il prendra toute son essence.
- Si finalement vous n’avez pas beaucoup de comportement, qu’ils ne risque pas beaucoup de bouger et que vous n’avez pas des objets variés pour mutualiser ce code, alors le visiteur n’est probablement pas pour vous.
Egalement aussi le visiteur étant un objet permet de conserver un état, ce que ne permettent pas les objets même du domaine ou les helpers (sauf si on utilise des objets contextes passé de fonction en fonction, ce n’est pas exceptionnel).
Exemple sans prétention de visiteurs
D’abord la grappe d’objet “complète” :
public class CoolBookCollection {
private Collection<Book> books;
private String owner;
private CollectionStatus status;
private void accept(DomainOperation operation) {
operation.operateOn(this);
}
public Collection<Book> books() { return books; }
public static enum CollectionStatus {
TIDY, MESSY, OK
}
// ...
}
public class Book {
private Price price;
private String title;
private String Author;
public Price price() { return price; }
public String title() { return title; }
public String author() { return Author; }
}
public class Price {
public Price() { }
public Price(Price priceA, Price priceB) { }
public Price add(Price price) { return new Price(this, price); }
}
Et la partie relatives aux visiteurs, d’abord l’interface (ou j’ai choisi volontairement de ne pas mettre les mot Visitor et visit) :
public interface DomainOperation {
void operateOn(CoolBookCollection coolBookCollection);
}
public class CountAllBooks implements DomainOperation {
private int count;
public void operateOn(CoolBookCollection coolBookCollection) {
count = coolBookCollection.books().size();
}
public int bookCount() {
return count;
}
}
public class ObtainCollectionPriceByGenre implements DomainOperation {
private final String genre;
private Price totalPrice = new Price();
public ObtainCollectionPriceByGenre(String genre) {
this.genre = genre;
}
public void operateOn(CoolBookCollection coolBookCollection) {
for (Book book : coolBookCollection.books()) {
totalPrice.add(book.price());
}
}
public Price totalPrice() { return totalPrice; }
}
Et voilà on des comportements différents liés à un objet en particulier, pas besoin de retoucher notre élément. Et on a une manière élégante de sortir nos comportements. Bien entendu, ce genre de chose est à faire avec du bon sens, en fonction du contexte et de l’opération à effectuer.
Quand on a davatage d’objets du domaine à visiter, attention!
Attention quand même, comme précisé plus haut, le visiteur n’est pas non plus sans défaut. Sur une structure d’objet profonde ou large, votre pattern visiteur va créer une dépendance cyclique entre lui et les objets sur lesquels il est sensé s’appliquer.
public interface DomainOperation {
void operateOn(CoolBookCollection coolBookCollection);
}
Si mon visiteur doit par exemple travailler sur plusieurs sous type de l’objet (on pourrait typiquement avoir ce genre de problème avec les structures composites) :
public interface DomainOperation {
void operateOn(BookCollection bookCollection);
void operateOn(CoolBookCollection coolBookCollection);
void operateOn(CheesyBookCollection cheesyBookCollection);
void operateOn(InTheCaveBookCollection inTheCaveBookCollection);
}
On voit vite le problème ou le visiteur est forcé d’implémenter des opérations pour des objets qui ne l’intéresse pas forcément. Le problème est contournable en utilisant intelligemment les interfaces, mais cette solution palliative a également des limites; on ne va faire implémenter 45 interfaces à nos objets.
Pour cela il y a une solution un peu plus complexe qui est également un pattern, c’est le Visiteur Acyclique. Je n’approfondie pas trop, mais l’idée est d’avoir pour chaque sous type du domaine une interface de visiteur qui permet de vérifier que l’instance du visiteur est acceptable. Evidemment vous pourrez adapter le comportement, et vous n’êtes non plus obligé d’implémenter toutes les méthodes, c’est le but de ce pattern acyclique.
Et typiquement le code du accept pour chaque sous-type de collection aurait une tête du genre :
public void accept(DomainOperation operation) {
if(operation instanceOf BookCollectionOperation) {
((BookCollectionOperation) operation).operateOn(this);
}
}
Et voilà on a cassé les dépendance, et on est pas obligé d’implémenter toute les interfaces de chaque type de collection.
Le double dispatch, à ne pas confondre avec un visiteur
Le lecteur avertit aura vite deviné que ça ressemble au pattern stratégie, et il aura raison, ce sont des patterns comportementaux. Mais là ou le visiteur se distingue, et notamment dans des langages comme Java, .Net, C++ c’est qu’il utilise la technique du double dispatch.
Alors le double dispatch (double répartition) c’est quoi exactement, c’est un moyen pour le logiciel de résoudre au runtime les méthodes à exécuter.
Je vais citer les exemples wikipédia et transformer leurs exemples en Java.
On a donc deux catégories d’objets, des astéroïdes et des vaisseaux spatiaux.
public class SpaceShip {
}
public class GiantSpaceShip extends SpaceShip {
}
public class Asteroid {
void collideWith(SpaceShip spaceShip) {
System.out.println("Asteroid hit a SpaceShip");
}
void collideWith(GiantSpaceShip giantSpaceShip) {
System.out.println("Asteroid hit a GiantSpaceShip");
}
}
public class ExplodingAsteroid extends Asteroid {
void collideWith(SpaceShip spaceShip) {
System.out.println("ExplodingAsteroid hit a Spaceship");
}
void collideWith(GiantSpaceShip giantSpaceShip) {
System.out.println("ExplodingAsteroid hit a GiantSpaceShip");
}
}
Ok, maintenant dans le code on a ça
Asteroid theAsteroid = new ExplodingAsteroid(); SpaceShip theSpaceShip = new GiantSpaceShip(); GiantSpaceShip theGiantSpaceShip = new GiantSpaceShip(); theAsteroid.collideWith(theSpaceShip); theAsteroid.collideWith(theGiantSpaceShip);
Comme en java c’est la méthode de l’instance qui est appelée, pas de problème pour nos astéroïdes. Mais là ou ça coince c’est au niveau des vaisseaux spatiaux. Les deux appels vont afficher sur la sortie sandard:
[pre]ExplodingAsteroid hit a SpaceShip
ExplodingAsteroid hit a GiantSpaceShip[/pre]
En effet le type réel du vaisseau spatial n’est pas connu, sauf si on fait de la reflection avec un instanceof, mais il y a plus élégant, c’est le double dispatch.
Si maintenant nos vaisseaux spatiaux ont tous les deux cette méthode définie :
public class SpaceShip {
void collideWith(Asteroid asteroid) {
asteroid.collideWith(this);
}
}
public class GiantSpaceShip extends SpaceShip {
void collideWith(Asteroid asteroid) {
asteroid.collideWith(this);
}
}
Maintenant notre code utilisera l’API de cette façon :
Asteroid theAsteroid = new ExplodingAsteroid(); SpaceShip theSpaceShip = new GiantSpaceShip(); GiantSpaceShip theGiantSpaceShip = new GiantSpaceShip(); theSpaceShip.collideWith(theAsteroid); theGiantSpaceShip.collideWith(theAsteroid);
Et on aura le code correcte utilisé.
Cette technique est utilisée par le visiteur, mais nous ne somme pas obligé d’avoir des visiteurs pour l’utiliser (la preuve par l’exemple grâce à wikipédia). C’est utilisé régulièrement dans la JDK, typiquement pour la sérialisation (même si c’est caché). Coté performance si on a le choix, le double dispatch sera toujours plus rapide qu’un instanceof. Coté design c’est pratique quand on a des branches d’objets qui travaillent ensemble.
Certains langages proposent nativement un support pour ces problèmes de résolution de type d’opérande, comme Nice.
A regarder aussi, c’est le multi dispatch ou les multi-méthodes, il y a notamment une implémentation de Rémy Forax de l’université de Marne-la-Vallée, cette implémentation a le mérite d’être standard Java, c’est à dire qu’elle n’étends pas le langage lui-même.
Pour y jeter un œil : http://www-igm.univ-mlv.fr/~forax/works/jmmf/index.html
Récapitulatif sur le visiteur
Le visiteur est bien un ami, mais comme tous les potes, il ne sait pas tous faire non plus.
Un visiteur sait parcourir des arbres, il se débrouille super bien avec, mais il est aussi utile quand il n’y a pas d’arbre.
Un visiteur sert avant tout à extraire des comportements lié à un structure d’objet qui bouge peu. La structure peut être plate, ou en profondeur (cela dit je privilégierait la composition à la lace de l’héritage).
Le visiteur utilise la technique du double dispatch, ne pas confondre les deux.
Le visiteur permet de respecter le SRP (Single Responsibility Principle).
Le visiteur aide à maintenir le CCP (Common Closure Principle), c’est une histoire de cohésion entre les classes qui sont regroupées dans un même package.
The classes in a package should be closed together against the same kind of changes. A change that affects a package affects all the classes in that package.
Bon voilà, le débat reste ouvert, si vous pensez que j’ai tort, que j’oublie un point important, ou pour autre chose, il y a les commentaires.
Références
http://www.objectmentor.com/omSolutions/oops_what.html
http://www.objectmentor.com/resources/articles/visitor.pdf
http://www.objectmentor.com/resources/articles/acv.pdf
http://www.artima.com/cppsource/top_cpp_aha_moments.html
http://butunclebob.com/ArticleS.UncleBob.VisitorVersusInstanceOf
http://www.javaperformancetuning.com/articles/ddispatch.shtml
Prologue
A l’école nos professeurs nous apprenaient ce qu’était la programmation orientée objet; en particulier l’encapsulation. En effet avoir un accès public aux variables internes d’un objet n’est pas particulièrement recommandé, pourtant nous avons connaissons tous la convention JavaBean :
class Bean {
/** constructeur sans argument, optionnel si c'est le seul constructeur de la classe */
public Bean() { }
public void setBeanName(String name) {
beanName = name;
}
public String getBeanName() {
return beanName;
}
}
Manque de bol, cette convention qui a pourtant son utilité -voire sa nécéssité- peut dans certains contextes briser l’encapsulation, et plus dangereux pour votre code, elle permet à vos objets d’être mutable, c’est à dire de pouvoir modifier l’état d’un objet après sa création. Bien que dans certains cas le design ou le rôle de la classe demande cette caractéristique, dans beaucoup d’autres situations la mutabilité peut poser problème.
D’ailleurs historiquement les JavaBeans ont été pensé pour être utilisé par des applications graphiques afin d’être construit itérativement et finalement pour être facilement dé/sérialisés [1]. Mais ces objets exposent publiquement leurs états, du coup :
- Il y a de l’adhérence à des propriétés internes d’un objet, s’il y a beaucoup de code qui utilise ces propriétés internes, l’évolutivité et la maintenance de ce code peut très vite devenir difficile et donc couteuse.
- Ce n’est plus vraiment de la programmation orientée objet. C’est en quelque sorte des variables globales, ça fait plus de 30 ans qu’on sait que les variables globales c’est mal! Demandez à Barbara Liskov [2].
- Avec cette possibilité de muter les objets, il peut y avoir des problèmes au runtime, et croyez moi avec l’arrivée de la parallélisation en plus dans vos applications il va y avoir des surprises.
Bon revenons au design, et aux problèmes rencontrés.
Illustration des problèmes de design du code
hashCode et equals
Donc pour commencer, on va juste faire quelques tests sur un objet sans les méthodes hashCode() et equals(). Prenons les test suivants, je créé 4 instances de beans, obj1 et obj3 puis obj2 et obj4 ont les mêmes propriétés.
Ce test montre les problèmes quand on oublie les méthodes equals et hashCode.
public class MutabilityCanBeBadTest {
private AJavaBean obj1;
private AJavaBean obj2;
private AJavaBean obj3;
private AJavaBean obj4;
@Before
public void initTheBeans() {
obj1 = new AJavaBean();
obj1.setName("paraboot");
obj1.setSellingDate(new GregorianCalendar(2010, 03, 30).getTime());
obj2 = new AJavaBean();
obj2.setName("ethnies");
obj2.setSellingDate(new GregorianCalendar(2010, 10, 30).getTime());
obj3 = new AJavaBean();
obj3.setName("paraboot");
obj3.setSellingDate(new GregorianCalendar(2010, 03, 30).getTime());
obj4 = new AJavaBean();
obj4.setName("ethnies");
obj4.setSellingDate(new GregorianCalendar(2010, 10, 30).getTime());
}
@Test
public void objectsShouldBeEquals() throws Exception {
assertEquals(obj2, obj4); // fail
assertEquals(obj1, obj3); // fail
}
@Test
public void hashCodeShouldBeEquals() throws Exception {
assertEquals(obj1.hashCode(), obj3.hashCode()); // fail
assertEquals(obj2.hashCode(), obj4.hashCode()); // fail
}
@Test
public void addAndRemoveToHashBasedCollection() throws Exception {
Set<AJavaBean> set = new HashSet<AJavaBean>();
assertTrue(set.add(obj1));
assertTrue(set.add(obj2));
assertFalse(set.add(obj3)); // fail
assertFalse(set.add(obj4)); // fail
assertEquals(2, set.size()); // fail
assertTrue(set.remove(obj1));
assertTrue(set.remove(obj2));
assertFalse(set.remove(obj3)); // fail
assertFalse(set.remove(obj4)); // fail
}
}
Si l’implémentation de AJavaBean oublie donc le hashCode et le equals, la plus part des assertions ne marchent plus.
public class AJavaBean {
private String name;
private Date sellingDate;
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public Date getSellingDate() {
return sellingDate;
}
public void setSellingDate(final Date uid) {
this.sellingDate = uid;
}
@Override
public String toString() {
return "AJavaBean [name=" + name + ", sellingDate=" + sellingDate + "]";
}
}
Que s’est-il passé? S’il n’y a pas de hashCode et de equals, ce sont les méthodes de la super classe qui sont utilisées, dans le code listé plus haut ce sont les méthodes de Object qui seront utilisées pour tester l’égalité et le hashCode.
- Donc pour l’égalité Object.equals(Object) vérifie uniquement si l’instance est la même. Ce qui explique que les tests d’égalité échouent plus haut.
- Pour le hashCode, c’est la JVM qui le génère, bref autant dire que le hashcode est différent pour chaque instance. Ceci explique que les instances obj1 et obj2 sont ajoutées au HashSet, si le hashcode avait été le même alors les opérations d’ajout et de suppression auraient renvoyé false (n’oublions pas qu’il s’agit d’un HashSet).
Et donc pour le code mutable
Ok, bon maintenant qu’on a vu ça, notre bean implémente les méthodes equals et hashCode de manière idoine, c’est à dire dans notre cas que le code se base sur les attributs name et sellingDate. Pas de mystère, on peut utiliser l’outils de génération de l’IDE.
Eclipse génère ça:
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((sellingDate == null) ? 0 : sellingDate.hashCode());
return result;
}
@Override</div>
public boolean equals(final Object obj) {
if (this == obj) { return true; }
if (obj == null) { return false; }
if (getClass() != obj.getClass()) { return false; }
AJavaBean other = (AJavaBean) obj;
if (name == null) {
if (other.name != null) { return false; }
} else if (!name.equals(other.name)) { return false; }
if (sellingDate == null) {
if (other.sellingDate != null) { return false; }
} else if (!sellingDate.equals(other.sellingDate)) { return false; }
return true;
}
Bon à priori on se dit que notre code est safe puisqu’on a nos méthodes equals et hashcode, mais on se fourvoie ; notre objet est mutable!
Exemple :
@Test
public void playWithMutabilityWithABeanInHashBasedCollection() throws Exception {
Set<AJavaBean> set = new HashSet<AJavaBean>();
assertTrue(set.add(obj1));
assertTrue(set.add(obj2));
obj2.setSellingDate(new GregorianCalendar(2010, 05, 30).getTime()); // valeur précédente : 2010-10-30
assertTrue(set.remove(obj2)); // owned
assertEquals(2, set.size()); // owned
assertFalse(set.add(obj2)); // owned
}
Alors on sait que les méthodes equals et hashCode utilisent les deux propriétés name et sellingDate, donc quand on ajoute un objet dans le HashSet le hashCode correspondra au calcul fait partir des valeurs des ces attributs. Mais voilà le hashcode de l’objet n’est calculé qu’une fois, au moment de l’interaction dans la Map (ajout, suppression, contains, etc…).
Donc ce qu’il se passe c’est qu’on a fait muter l’état de notre objet, du coup le hashcode est différent, mais la collection conserve la référence de l’objet qu’elle contiens et ne recalcule pas son hashcode! C’est aussi avec avec ce genre de code que vous pouvez avoir des fuites mémoires. Et on est même pas dans un contexte multithreadé, alors imaginez si la collection est partagée entre plusieurs thread!
Attention aux collections ou aux dates du JDK
Par ignorance puis par laxisme, j’avoue que j’ai écris du code qui ressemble à ça (et j’ai honte de le dire) :
public class AnotherSupposedImmutableClass {
private final String name;
private final Date aDate;
private final Map<String, Integer> aMap;
public AnotherSupposedImmutableClass(final String name, final Date aDate, final Map<String, Integer> aMap) {
super();
this.name = name;
this.aDate = aDate;
this.aMap = aMap;
}
public String getName() {
return name;
}
public Date getADate() {
return aDate;
}
public Map<String, Integer> getAMap() {
return aMap;
}
}
Et forcement il y a des hics! A priori notre classe n’est pas mutable. Mais cela ne vous aura pas échappé, les propriétés aDate et aMap sont mutable!
@Test
public void playWithInternalMutability() throws Exception {
Map<String, Integer> map = new HashMap<String, Integer>();
AnotherSupposedImmutableClass supposedImmutableClass = new AnotherSupposedImmutableClass(
"name",
new GregorianCalendar(2010, 05, 30).getTime(),
map
);
supposedImmutableBean.getADate().setTime(123456789l); // oups
supposedImmutableBean.getAMap().put("trente quatre", Integer.valueOf(34)); // oups
supposedImmutableBean.getAMap().clear(); // oups, again
}
Et là, vous vous retrouverez les mêmes surprises que celles vu plus haut, ou évidement pire si vous êtes dans une application multithreadée. A ce sujet j’ai vu des ConcurrentModificationException parceque levé par du code à priori immutable, une optimisation d’un vieux code multithreadé avait déplacé une section qui modifiait une Map.
Je vous conseille vivement d’utiliser des objets immutables pour vos property, les librairies Joda-Time [3] et Google-Collections [4] fournissent des objets immutables.
Le pattern Builder de Joshua Bloch
Pour Joshua Bloch, c’est un peu une référence en Java, je pense qu’on peut lui faire confiance. Il est l’auteur du fameux livre Effective Java [5].
Alors pourquoi le pattern Builder de Joshua Bloch et non le pattern Builder du GoF ? En fait ce design vient d’une constatation au sujet de la construction d’objet complexes et pour s’affranchir des inconvénients des accesseurs.
En gros un objet du genre agrégat pourrait être construit avec un constructeur avec un paquet d’argument ou itérativement avec une foule de setter. Mais, un les gros constructeur ce n’est pas très pratique, puis deux les setters ça peux vite être lourd et ça rends votre objet mutable (ce qui n’est donc pas souhaité dans tous les cas).
Cette déclinaison du builder permet de construire un objet itérativement sans forcer la mutabilité.
Exemple les collections google :
public abstract class ImmutableMap<K, V> implements Map<K, V>, Serializable {
// ...
public static <K, V> Builder<K, V> builder() {
return new Builder<K, V>();
}
// ...
public static class Builder<K, V> {
final List<Entry<K, V>> entries = Lists.newArrayList();
public Builder() {}
public Builder<K, V> put(K key, V value) {
entries.add(entryOf(key, value));
return this;
}
// ...
public ImmutableMap<K, V> build() {
return fromEntryList(entries);
}
}
private static <K, V> ImmutableMap<K, V> fromEntryList(List<Entry<K, V>> entries) {
// ...
}
// ...
}
Ou encore avec une classe de notre domaine :
public class ACoolImmutableClass {
private final String name;
private final DateTime sometime;
private final List<String> listOfStuff;
// many other fields
public String getName() {
return name;
}
public DateTime getSometime() {
return sometime;
}
public static class Builder {
private String name;
private DateTime sometime;
private List<String> listOfStuff = new ArrayList<String>();
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder at(DateTime moment) {
this.sometime = moment;
return this;
}
public Builder addThisThing(String thing) {
this.listOfStuff.add(thing);
return this;
}
public ACoolImmutableClass build() {
return new ACoolImmutableClass(this);
}
}
private ACoolImmutableClass(Builder builder) {
this.name = builder.name;
this.sometime = builder.sometime;
this.listOfStuff = ImmutableList.copyOf(builder.listOfStuff);
}
}
A noter que cette classe utilise des objets immutables pour ces attributs (DateTime, et ImmutableList).
Un des avantages, c’est qu’il est possible de valider les propriétés avant la création effective de l’objet. Avec les setters c’est faisable mais ça peut être délicat dans certaines situations.
Il y a un plugin Eclipse, qui permet de générer ces Builder, celà dit il est loin d’être super user friendly.
http://code.google.com/p/bpep/
Quoiqu’il en soit en aucun cas ce pattern n’est un remplacement du pattern Builder du GoF, il s’agit plus d’un pattern à appliquer dans un contexte ou il faut des objets immutable. Et encore ce n’est pas la seule solution, JodaTime typiquement n’utilise pas de builders.
Comment gérer la modification de l’objet
Si un comportement qui fait partit du domaine de l’objet et doit modifier l’état, alors il faut peut-être créer une nouvelle instance. La bibliothèque Joda-Time fait typiquement ça lorsqu’il y a modification d’un champs.
DateTime instance1 = new DateTime("2009-04-01");
DateTime instance2 = instance1.withYear(2010);
Je ne m’étends pas sur le sujet, mais ce genre de choses dépends de votre contexte, du rôle et du besoin. Un objet devrait être par défaut immutable, sauf si vraiment votre domaine identifie un cas ou l’état doit bouger et alors vous aurez des méthodes documentées qui appliqueront cette modification.
Conclusion
Mieux vaut des objets bien pensés et immutables que d’introduire la possibilité de changer l’état d’un objet et avoir des surprises. Et puis aussi :
- Il y a un risque fort d’avoir des problèmes au runtime, d’autant plus 10 ans après lorsqu’il y a une évolution à apporter et que plus personne ne sait qu’à tel endroit dans le code il y a le truc qui fout tout en l’air. Et les problèmes au runtime ca peut vite couter cher à analyser.
- Si vos objets ne peuvent pas être modifié alors vous n’aurez pas à vous soucier des problèmes de concurrences, c’est manifestement un gain de temps au développement et en maintenance. (Et donc un gain d’argent sur le long terme.)
- Bon ces objets sont bien cool, mais voilà il y a encore plein de framework (à tord ou à raison) qui se basent sur la convention JavaBean, je pense notamment aux objets marshallés en XML et consort.
- Ce code basé sur les builders est propre, mais il faut passer un petit peut plus de temps pour le faire. Il y a bien un plugin pour Eclipse, mais quid des autres IDE.
Quoi qu’il en soit, ces solutions sont toujours à appliquer avec du recul et toujours en fonction du contexte de votre domaine.
D’ailleurs cette entrée parle des problèmes rencontrés avec les collections du JDK, mais le problème pourrait se manifester différemment si une collection ou un de vos objets fonctionne autrement.
Encore une fois les remarques sont les bienvenues, ça fait plus de 40 ans que l’Homme fait du logiciel, et mafois on se plante encore assez souvent.
Références
- http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html
- http://www.infoq.com/presentations/liskov-power-of-abstraction
- http://dutheil.brice.online.fr/blog/index.php/2010/02/09/a-propos-de-joda-time/
- http://dutheil.brice.online.fr/blog/index.php/2010/02/16/les-collections-par-google-comment-sy-retrouver/
- http://www.amazon.fr/Effective-Java-Joshua-Bloch/dp/0321356683/ref=sr_1_1?ie=UTF8&s=english-books&qid=1269958692&sr=8-1
- http://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html
Le voilà il est sorti. Ce framework de mock digne épigone de easymock, a su plaire et propose plus que son ancêtre. Mais quoi de neuf qui vaut la peine d’être mentionné?!
Extensions des annotations
Le code ci-dessous présente le support étendu des annotations.
- @InjectMock, qui permet donc d’injecter les bouchons et les espions dans l’instance de ce champs. Il s’agit le plus souvent de classe testée. Attention la classe instanciée ne doit pas être nulle.
- @Mock a été étendu pour fournir des paramètres, équivalent aux possibilités offertes par withSettings, par exemple :
Mockito.mock(Class<?>, Mockito.withSettings().name("a mock for bob")) - @Spy qui comme son nom l’indique permet de créer un espion à partir d’une instance déjà créé.
- @Captor qui permet d’instancier un ArgumentCaptor.
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertSame;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class BusinessTestStuff {
@InjectMocks
private BusinessStuff stuffToTest = new BusinessStuff();
@Mock(name = "businessMethods")
private List<String> methodList;
@Mock(name = "technicalMethods")
private List<String> methodList2;
@Spy
private Map<Class<?>, Object> classInstances = new HashMap<Class<?>, Object>();
@Captor
private ArgumentCaptor<String> methodCaptor;
@Test
public void shouldDoSomethingWithBusinessAndTechnicalMethod() {
// given
given(methodList.get(15)).willReturn("businessMethod");
given(methodList2.get(15)).willReturn("technicalMethod");
// when
stuffToTest.doSomething(15);
// then
// ne lève pas d'exception
}
@Test
public void shouldCaptArgument() {
// given
// when
stuffToTest.addBusinessStuff("businessMethod");
// then
verify(methodList).add(methodCaptor.capture());
assertSame("businessMethod", methodCaptor.getValue());
}
@Test
public void shouldSpyMyMap() {
// given
// when
classInstances.put(String.class, "instance");
// then
verify(classInstances).put(eq(String.class), any(String.class));
}
}
Pourquoi ces annotations? Eh bien afin de rendre les tests plus clairs. Faire du beau code dans le code de production ne suffit pas, il faut faire attention à la clareté et l’expressivité de ces tests. Il ne faut pas oublier qu’on écrit seulement une fois du code et qu’on le relit bien plus. Parlez à vos équipe de maintenance ou d ‘évolutions combien de temps il passent à comprendre ce que vous avez voulu coder.
L’idée c’est d’écrire du code le plus propre possible, mais d’avoir aussi les tests les plus propres possibles, ceci aidera à comprendre à votre lecteur quelle(s) responsabilité(s) et quelle(s) comportement(s) son attendu.
Pour tester du code legacy
Ah voilà qui va en intéresser plus d’un. Pour votre ancien code ou vous devez traverser une grappe d’objet pour mocker le dernier, c’était plutôt fastidieux.
Mockito.when(mockedLegacyCode.getTop()).thenReturn(mockedTop);
Mockito.when(mockedTop.getMiddle()).thenReturn(mockedBottomRight);
Mockito.when(mockedBottomRight.getId()).thenReturn("BR");
Pour information, depuis longtemps déjà Mockito supporte la création de mock avec des réponses prédéfinies. Jusque là par défaut, les mock retournait les valeur par défaut, 0 pour un entier, null pour une référence, etc… Typiquement on évitais les null avec la réponse, qui renvoyait donc des mocks :
Mockito.mock(LegacyCode.class, Mockito.RETURNS_MOCKS);
Cependant la limite de cette réponse était qu’on ne pouvait pas créer des comportements pour des objets profonds dans la grappe d’objet. Et c’est là que la version 1.8.3 fournit un petit ajout assez sympa, on pourra maintenant écrire des choses comme :
CrapyLegacy mock = Mockito.mock(CrapyLegacy.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(mock.getTop().getMiddle().getBottomRight().getId()).thenReturn("deep id");
assertEquals("deep id", mock.getTop().getMiddle().getBottomRight().getId());
De la même manière avec les annotations étendues, on déclarera le mock de cette façon :
@Mock(answer = RETURNS_DEEP_STUBS) private CrapyLegacy mock;
Attention cependant, ceci est une facilité pour tester le code legacy, utiliser cette facilité aujourd’hui pour du nouveau est un signe grave que vous êtes en train de développer du code Legacy!
En effet vous jouez avec la loi de Demeter, et quand on joue à la loi de Demeter c’est très souvent parce que le code créé n’est pas orienté objet : il s’agit de code procédural! Le code procédural peut coûter cher voire très cher à maintenir et à faire évoluer [1]. C’est un des élément de la dette technique et financière qui coutera plus cher d’année en année!
Un mot sur le TDD
Mockito s’enrichit, fournit des facilités, permet d’éclaircir le code, pour véritablement être dans l’esprit TDD. L’esprit TDD ce n’est pas de faire de la couverture de code, ce n’est pas d’écrire du code puis de le tester après, ce n’est pas de tester la mécanique interne d’une classe. C’est juste commencer par écrire un test simple pour exprimer une responsabilité et un comportement qu’on attend sur un objet, puis d’enrichir le code et le test associé.
Si vous voulez faire du reverse engineering ou essayer des framework inconnus, commencez par écrire un test qui correspond à ce que vous attendez, puis refactorer petit à petit le code testé et le test. Le code s’enrichira petit à petit, si ça devient difficile peut-être que voues êtes face à un problème de découpage de responsabilités, ça peut induire de créer de nouveaux objets et donc de spliter le test également. Ce faisant vous réduisez la complexité du code et du test.
Si vous le nom de vos classes ou de vos méthodes ne sont pas satisfaisant c’est que les responsabilités ne sont pas clairement définies/identifiées, un refactoring est à prévoir.
Au début c’est un peu difficile, mais avec un peu d’exercice on devient meilleur! N’est-ce pas le but de l’agilité, de s’améliorer!
MAJ : Source [1] : http://www3.interscience.wiley.com/journal/114082374/abstract?CRETRY=1&SRETRY=0
Depuis quelques jours déjà le framework de collection par google est sorti en version 1.0. Ce framework a vu le jour chez Google donc, et s’impose finalement comme le prochain framework pour travailler avec les collections. En effet les classes utilitaires du JDK sont plutôt limitées et les classes commons-collections de Apache ne sont pas générifiés.
Les classes fournies par Google, ont été tunées pour être performante en rapidité et en utilisation mémoire. Si possible ce sont les collections standard du JDK, les collections du JDK sont mutables. Éventuellement l’utilisation des classes standard du JDK pourrait permettre à la JVM de faire les optimisation sur ces objets qu’il connait. Également aussi l’API orientée builder – un peu comme Joda-Time – facilite l’utilisation de google-collections.
Pour commencer, vous pouvez jeter un œil aux classes suivantes :
com.google.common.collect.Collections2 com.google.common.collect.Lists com.google.common.collect.Maps com.google.common.collect.Sets com.google.common.collect.ObjectArrays com.google.common.collect.Multisets com.google.common.collect.Multimaps com.google.common.collect.Iterators com.google.common.collect.Iterables
Ces classes utilitaires permettent déjà d’instancier les collections avec quelques commodités, par exemple dans le code ci-dessous les classes retournées sont les classes mutables du JDK :
LinkedHashSet<String> linkedHashSet = Sets.newLinkedHashSet();
ArrayList<AGenericObject<Class<Observer>>> arrayList = Lists.newArrayList();
Lists.newArrayList("bob", "marie", "barack", "bruce");
à la place de :
List<AGenericObject<Class<Observer>>> list = new ArrayList<AGenericObject<Class<Observer>>>();
Voilà rapidement pour les utilitaires des collections fournies par le JDK, mais Google fournit également des implémentations immutables des collections :
ImmutableSet<Integer> immutableSet = ImmutableSet.of(1, 2, 3, 4, 5);
ImmutableList<String> immutableList = ImmutableList.of("a,b,c,d,e,f,g".split(","));
Pour les maps, il y a aussi une API plutôt expressive et facilement utilisable. Par exemple pour créer facilement une multimap:
Multimap<Color, Fruit> colorIndex = HashMultimap.create();
for (Fruit fruit : fruits) {
colorIndex.put(fruit.getColor(), fruit);
}
Collection<Fruit> redFruits = colorIndex.get(Color.RED);
Si on veut jouer avec des map bi-directionnelles.
ImmutableBiMap<Integer, String> biMap = ImmutableBiMap.of(0, "Zero", 1, "One", 2, "Two", 3, "Three");
biMap.inverse().get("Zero"); // => 0
L’outil MapMaker pour créer des maps customisées :
Map<Params, Result> resultCache = new MapMaker().expiration(5 * 60,TimeUnit.SECONDS)
.makeComputingMap(new Function<Params, Result>() {
public Result apply(Params param) {
return computeHeavyAlgorythm();
}
}).makeMap();
Il est aussi possible de ne pas utiliser l’expiration mais de choisir plutôt des WeakReference ou des SoftReference pour les clés et/ou les valeurs.
Comment utiliser les Multiset. A noter, le Multiset ci-dessous est mutable! Pur un MultiSet immutable il faut le créer avec ImmutableMultiset.
Multiset<String> histogram = HashMultiset.create();
histogram.add("Hello");
histogram.add("World", 3);
histogram.add("Hello");
histogram.add("!");
int count;
count = histogram.count("Hello"); // 2
count = histogram.count("World"); // 3
count = histogram.count("Brice"); // 0
Et pour les itérateurs :
UnmodifiableIterator<Object> tokenizerIt = Iterators.forEnumeration(new StringTokenizer("a|b|c|d|e", "|")); // Eh oui ! StringTokenizer implément Enumeration<Object>
UnmodifiableIterator<String> splitIt = Iterators.forArray("e|ed|f|g|h|i".split("|"));
Iterator<Object> concatenatedIt = Iterators.concat(tokenizerIt, splitIt);
Iterators.frequency(concatenatedIt, "e"); // 2
concatenatedIt.hasNext(); // false
Ok maintenant que nous avons vu comment créer des collections, on peut regarder comment vraiment jouer avec. Ordonner une collection par exemple; il faut utiliser la classe Ordering (étends l’interface Comparator de java)
Function<Fruit, Color> getColorFunction = new Function() {
public Color apply(Fruit from) {
return from.getColor();
}
};
Function<Fruit, String> getNameFunction = new Function() {
public String apply(Fruit from) {
return from.getName();
}
};
Ordering<Fruit> colorOrdering = Ordering.natural().onResultOf(getColorFunction);
Ordering<Fruit> nameOrdering = Ordering.natural().onResultOf(getNameFunction);
// ordonner par couleur puis par nom
Ordering<Fruit> colorAndNameOrdering = colorOrdering.compound(nameOrdering);
List<Fruit> sortedFruitList = Ordering.natural().sortedCopy(fruits);
Set<Fruit> sortedFruits = ImmutableSortedSet.orderedBy(colorAndNameOrdering).addAll(fruits).build();
Filtrer des éléments est devenu super facile à utiliser. Il nous faut les classes Predicate et Predicates.
List<String> names = Lists.asList("Clément", "Jean-Max", "Caroline", "Céline", "Brice");
Iterable<String> filtered = Iterables.filter(
names,
Predicates.or(
Predicates.or(Predicates.equalTo("Clément"), Predicates.equalTo("Brice")),
returnALengthPredicate(5)
)
);
Il est possible de faire des transformations
Lists.transform(lotoNumbers, new Function<String, Integer> {
public Integer apply(final String from) {
return Integer.valueOf(from);
}
});
Que peut-on faire d’autre? Par exemple avec les maps et les sets, on peut observer les différences, faire des unions, ou faire des intersections.
MapDifference<String, Integer> differenceMap = Maps.difference(mapA, mapB); differenceMap.areEqual(); Map<String, ValueDifference<Integer>> entriesDiffering = differenceMap.entriesDiffering(); Map<String, Integer> entriesOnlyOnLeft = differenceMap.entriesOnlyOnLeft(); Map<String, Integer> entriesOnlyOnRight = differenceMap.entriesOnlyOnRight(); Map<String, Integer> entriesInCommon = differenceMap.entriesInCommon();
On peut également faire de l’indexation sur des listes de map :
List<String> badGuys = Arrays.asList("Inky", "Scratchy", "Blinky", "Pinky", "Pinky", "Clyde");
Function<String, Integer> stringLengthFunction = ...;
Multimap<String, Integer> index = Multimaps.index(badGuys, stringLengthFunction); // { 4=[Inky], 5=[Pinky, Pinky, Clyde], 6=[Blinky], 7=[Scratchy] }
Au cas ou pour éviter de chercher voici quelques méthodes utilitaires dans Iterables, d’ailleurs c’est là qu’on retrouve le fameux isEmpty. (Attention la librairie google ne vérifie pas la nullité, et leur argument est de ne pas encourager de retourner null mais plutôt des collections vide, bref ce que dit Joshua Blosh dans son fameux livre Effective Java, §Item 43)
Iterables.getOnlyElement(ImmutableSet.of("1")); // 0
Iterables.getOnlyElement(ImmutableSet.of("1", "2")); // IllegalArgumentException
Iterables.isEmpty(ImmutableMultiset.of()); // true
Iterables.isEmpty(null); // NullPointerException
Iterable<String> moreFruits = Iterables.concat(ImmutableMultiset.of("apple", "banana", "kiwi"), Lists.newArrayList("ananas", "orange")); // "apple", "banana", "kiwi", "ananas", "orange"
String kiwi = Iterables.getLast(ImmutableMultiset.of("apple", "banana", "kiwi")); // "kiwi"
Iterable<List<String>> fruitBasket = Iterables.partition(moreFruits, 2); // { "apple", "banana" }, { "kiwi", "ananas" }, { "orange" }
Pour passer d’un Iterable à un tableau :
Iterables.toArray(Lists.newArrayList(new DateTime(), new DateTime().plusDays(1)), DateTime.class);
Voilà il y a pas mal de petits trucs bien sympa, ceci dit il peut manquer des choses qui nous semblent essentielles. Mais cette bibliothèque apporte enfin des choses qui nous simplifient la vie. Les commons-collection ont bien marqués nos habitudes, mais pour s’y retrouver et utiliser cette bibliothèque à bon escient il est certain qu’il va falloir faire un petit effort.
Le contexte L’histoire commence par un problème en production sur une version à priori stable et sans anomalie connue. Seulement voilà une fois en prod l’application devient de plus en plus lente. Pourquoi? Que se passe-t-il? Avec l’activation des logs du GC dans les options de la JVM, l’équipe s’aperçoit donc très vite que l’application arrive à bout de la…






Follow