Les visiteurs, une question de nommage, et le double-dispatch

06/05/2010 code, design, pattern 2 comments

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?

  1. 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.

  2. 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.

    Et le démoniaque helper :

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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” :

Et la partie relatives aux visiteurs, d’abord l’interface (ou j’ai choisi volontairement de ne pas mettre les mot Visitor et visit) :

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.

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) :

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 :

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.

Ok, maintenant dans le code on a ça

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:

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 :

Maintenant notre code utilisera l’API de cette façon :

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 œilhttp://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

A propos du design de vos objets, des getters et setters, de equals/hashCode et de la mutabilité

30/03/2010 code, design, pattern, TDD No comments , , , , ,

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 :

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 :

  1. 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.
  2. 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].
  3. 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.

Si l’implémentation de AJavaBean oublie donc le hashCode et le equals, la plus part des assertions ne marchent plus.

En bref :

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:

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 :

Surprise! You just got

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) :

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!

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 :

Ou encore avec une classe de notre domaine :

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.

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 :

  1. 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.
  2. 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.)
  3. 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.
  4. 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

  1. http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html
  2. http://www.infoq.com/presentations/liskov-power-of-abstraction
  3. http://dutheil.brice.online.fr/blog/index.php/2010/02/09/a-propos-de-joda-time/
  4. http://dutheil.brice.online.fr/blog/index.php/2010/02/16/les-collections-par-google-comment-sy-retrouver/
  5. http://www.amazon.fr/Effective-Java-Joshua-Bloch/dp/0321356683/ref=sr_1_1?ie=UTF8&s=english-books&qid=1269958692&sr=8-1
  6. http://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html

Mockito 1.8.3

09/03/2010 code , , ,

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 :
  • @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.

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.

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 :

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 :

De la même manière avec les annotations étendues, on déclarera le mock de cette façon :

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

Les collections par Google, comment s’y retrouver?

16/02/2010 code 1 comment

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 :

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 :

à la place de :

Voilà rapidement pour les utilitaires des collections fournies par le JDK, mais Google fournit également des implémentations immutables des collections :

Pour les maps, il y a aussi une API plutôt expressive et facilement utilisable. Par exemple pour créer facilement une multimap:

Si on veut jouer avec des map bi-directionnelles.

L’outil MapMaker pour créer des maps customisées :

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.

Et pour les itérateurs :

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)

Filtrer des éléments est devenu super facile à utiliser. Il nous faut les classes Predicate et Predicates.

Il est possible de faire des transformations

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.

On peut également faire de l’indexation sur des listes de map :

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)

Pour passer d’un Iterable à un tableau :

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.

Une fuite mémoire, beaucoup de reflection et pas de OutOfMemoryError

12/02/2010 code 2 comments , , ,

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 mémoire disponible, mais pas de OutOfMemoryError (pourtant classique lors d’une fuite mémoire).

 

Lors de l’analyse des GC on remarque immédiatement une famine de mémoire, la JVM est obligée de faire des Full GC très souvent, et un Full GC c’est lent!

Pour avoir une représentation un peu compréhensible, on analyse ces logs avec GCViewer. On a alors un graphe qui ressemble à ça :


On voit comment se passe le consommation de la mémoire dans l’application, on sait que l’application est lente, maintenant pourquoi la consommation mémoire monte autant sans être libéré. Effectivement les raisons peuvent varier surtout qu’il n’y avait pas de OutOfMemoryError!

  • Possibilité 1 : Un problème de concurrence (deadlock, point de contention sur une ressource, …); c’est cette possibilité qui a été retenue pour l’investigation du problème. Les thread dump nous confortaient dans cette optique étant donné qu’on voyait régulièrement le même code revenir. Et les indicateurs sur le CPU montrait qu’il n’était pas énormément utilisé.
  • Possibilité 2 : Une fuite mémoire, choix écarté parce qu’on ne voyait de OOME.

Et bien on avait tort, il s’agissait d’une fuite mémoire. Avec un collègue plus expérimenté nous avons fait du profiling, très vite il a mis le doigt sur le code en tort. Mais quelque chose me choquait, pourquoi pas d’erreur OOME alors qu’il s’agissait manifestement d’une fuite mémoire.

La bonne rencontre

J’ai eu la chance de pouvoir rencontré Heinz Kabutz lors d’une conférence chez Zenika, en discutant avec lui j’ai eu l’occasion d’aborder ce sujet. Il m’a immédiatement demandé si notre application utilisait beaucoup d’introspection. Il m’a dit qu’il soupçonnait que ce genre de cas pouvait se produire, et il m’a ensuite aiguillé sur la manière dont le JDK de Sun utilise des SoftReference pour stocker les éléments issus de la reflection.

Et là, les cases manquantes n’étaient plus, en effet les objets SoftReference sont des références qui sont réclamées par le GC lorsque la JVM a vraiment vraiment besoin de mémoire, juste avant de lever une OutOfMemoryError. En gros, ça se passe typiquement lors des Full GC.

Et donc comme l’application est toujours en état de marche, le code qui a besoin de reflection va recréer ces objets. Cette combinaison de Full GC et la recréation constante des références des éléments issus de l’introspection, va très fortement ralentir l’application sans lever cette fameuse OOME. Ou en tout cas en repoussant dans le temps cette OOME.

La preuve

Fort de cette nouvelle connaissance, j’ai été jeter un coup d’œil dans l’objet java.lang.Class pour effectivement y découvrir la mise en cache des éléments comme les méthodes et les champs dans une SoftReference. Ainsi en regardant le code source de OpenJDK:

Bon voilà pour la preuve de ce qui était avancé, mais pour aller plus loin je vais reproduire le scénario.

La preuve par l’exemple

L’idée de l’exemple est d’avoir du code qui va simuler une fuite mémoire et un autre code qui va utiliser plus ou moins intensément l’introspection. On le verra plus tard mais le débit d’allocation d’objet de la fuite mémoire ne doit pas être trop important sinon on verra effectivement très vite l’erreur OutOfMemoryError.

Le processus métier qui utilise de l’introspection

Comme je suis fainéant, je n’ai pas spécialement envie de créer 300 classes, donc je vais les générer en utilisant l’API Compiler du JDK 6. Je me suis un peu inspiré de qui disponible sur le net à ce sujet. En particulier de cette entrée. Je passe brièvement dessus pour simplement dire que c’est la méthode processBusinessLogic qui est intéressante, on charge des classes, et surtout on appelle une méthode par introspection.

Le code avec la fuite mémoire

Bon voilà pour le code qui simule du code métier avec de l’introspection, maintenant c’est au tour de simuler le service qui engendre une fuite mémoire. L’utilisation des thread est accessoire cela dit, mais ça permet de rappeler le fonctionnement d’une véritable application.

C’est à la ligne de 30 du code que je contrôle le débit de la fuite mémoire. En effet si je retire ce Thread.sleep, il y a très vite une OOME. Pour la fuite mémoire, celle-ci consiste juste à alimenter un liste de String. On pourrait par exemple imaginer que dans une application réelle ce code stockerait des objets dans une Map pour chaque session.

Afin de ne pas attendre des heures avec juste quelques String, je vais limiter l’espace mémoire de mon application à 10MB:

 

Je vais également ajouter les paramètres à la JVM pour suivre le GC.

Et le résultat est là, l’application ne plante toujours pas après 6 minutes.

Effectivement les paramètres de la JVM donnent une allure différente d’une application en production, mais ici le but est de reproduire un scénario de fuite mémoire sans OutOfMemoryError. Le GC a donc l’allure suivante :

On voit un premier Full GC vers 1min30 ou les SoftReferences sont nettoyées, et puis vers 2min30 c’est la catastrophe, il n’y a que des Full GC, la JVM va constamment réclamer les références issues de l’introspection, le programme va constamment en recréer, avec la saturation de la mémoire la lenteur de tous les FullGC devient manifeste. Et comme dit plus haut les thread dump ne vont pas révéler de point de contention, ils vont juste montrer que l’application est lente. En particulier les thread dump vont surtout révéler les stacks des modules ou l’application est plus lente!

D’ailleurs sur la sortie standard, on voit au premier Full GC les traces suivantes, et elles arrivent  plus régulièrement une fois que les GC s’enchainent :

 

Autre outil à utiliser, jVisualVM qui est disponible en standard avec le JDK6. On se retrouve avec onglet de monitoring sympa. A noter que les graphes d’activité du CPU ne sont pas disponible en standard sur jVisualVM avec la JDK6.

Ce que je ne voyais pas avec GCViewer c’est que le nombre de threads actives a dramatiquement baissé, ce qui confirme la lenteur exécution, les traitements mettent vraiment plus longtemps, et les autres threads sont alors mises en standby. Si on fait attention à la fenêtre temporelle, ça passe vers 14h48, à ce moment là, la mémoire heap n’est pas encore complètement saturée les GC tenaient jusque là. C’est ensuite que les Full GC prennent le relai pour réclamer de la mémoire, c’est donc à ce moment que les SoftReference sont collectées. Et comme dit plus haut, ces références sont recréées par les traitements métier. Et comme le Full GC s’exerce en permanence après ce moment, les références qui viennent d’être recréés sont collectées à nouveau.  Et voilà la boucle est bouclée.

Conclusion

En conclusion, ce n’est pas parce qu’il n’y a pas de OutOfMemoryError qu’il n’y a pas de fuite mémoire. Plus généralement le réflexe c’est de se demander si notre application utilise beaucoup d’introspection ou plus simplement si l’application utilise beaucoup de références plus faibles comme les WeakReference, SoftReference.

Optimization WordPress Plugins & Solutions by W3 EDGE