vendredi 1 mai 2009

L'abus de composants


Y'en a marre des projets qui accumulent les composants - Open Source ou non - comme des trophées. Les développeurs ne savent plus coder sans utiliser des composants externes, de plus en plus nombreux, générant de plus en plus de traitements. Et pour quoi faire ?

Chaque composant impose sa liste de dépendance. Pour bénéficier d'un service, il faut en rapatrier beaucoup d'autres. Cela devient tellement compliqué qu'il faut concevoir des chaînes de dépendances pour être certain de tout avoir sous la main, au cas où une méthode aurait besoin de quelque chose. Les programmes deviennent ainsi de plus en plus volumineux, sans que les fonctionnalités suivent pour autan.

Une étude rapide du code d'une application démontre souvent que les composants sont utilisés pour pas grand chose. Quelques lignes codées « à la main » peuvent avantageusement remplacer un composant et toute sa clique de dépendances.

À titre d'illustration, évoquons un audit effectué sur une applet java, un code qui doit être téléchargé sur le poste du client. Le code était très volumineux car très riche et complexe, utilisant Swing, de nombreuses fenêtres et boites de dialogue, des graphiques, des analyses mathématiques et de la communication dynamique avec le serveur. En étudiant le code, nous avons constaté qu'une grande part des composants, tous Open Source, était utilisé par une ou deux méthodes. Il était possible de les remplacer par deux lignes de code Java. En suivant nos recommandation, la taille de l'applet a été divisée par deux, sans aucune perte de fonctionnalité. Il y a avait donc 50% de code inutile !

Coté serveur, c'est la même chose. Les applications sont de plus en plus volumineuses, exigeantes en mémoire et en espace disque, alors qu'elles ne font généralement que de la gestion de base de données et se limitent à produire quelques pages XHTML.

Un simple exemple comme « petclinic » de Spring, ne gérant qu'une dizaine de page et une petite base de données, est porté par une archive de 17 Mo ! Cela donne quoi comme image mémoire ? Il faut combien de serveurs pour gérer quelques centaines de requêtes par secondes ?

N'y a t'il pas là un effet CV ? « Plus j'utilise de frameworks Open Source, plus je peux en ajouter dans mon CV et donc, plus je suis un balaise ! »

C'est absurde. A l'époque du C++, il y avait un dogme très important : « je ne paye que pour ce que j'utilise ». En gros, je ne veux pas devoir ajouter un parseur XML spécifique, un composant de logs particulier, un analyseur d'expression régulière, des fichiers de ressources en 53 langues, tout cela pour lire un fichier de paramètres me permettant d'initialiser mon application.

Pour répondre au principe évoqué, les composants doivent être le plus économes possibles en termes de dépendances. Pour cela, il faut parfois revoir l'architecture pour accepter des plug-in optionnels, imposant alors quelques dépendances supplémentaires. Ainsi, l'application consommatrice ne dépend que de ce qui est indispensable. Je n'ai pas besoin que mon fichier de paramétrage puisse être renseigné en XML, alors je n'utilise pas le plug-in correspondant. Une simple lecture de « properties » suffit.

Les frameworks deviennent de plus en plus riches, de plus en plus souples, mais au prix d'une explosion de la complexité et d'une dégradation assurée des performances.

Par exemple, le framework Spring offre une dizaine de scénarios d'usage (avec ou sans AOP, avec ou sans AutoProxy, avec ou sans annotations, etc.). Au final, lorsque l'on étudie un peu le code du framework, on découvre un volume monstrueux de traitements. Pour faire quoi ? Instancier des objets et les relier entres eux. Cela se traduit en 3 lignes de codes sans Spring !

« Mais il est absolument indispensable de pouvoir modifier le framework de persistance utilisé par l'application sans toucher une ligne de code ». Qui fait vraiment cela ? Ne faut-il pas alors re-tester complètement l'application et la débugger ? S'il y a un bug, c'est donc qu'il faut modifier l'application. On re-teste alors avec l'ancien framework de persistance pour garantir la portabilité ? N'importe quoi. Les projets doivent faire des choix, sélectionner des technologies, et s'y tenir. Il faut assumer dans la vie. S'il est nécessaire par la suite de remettre en cause les choix, on refactorise, car on a une batterie de tests unitaires pour éviter une dégradation de l'application.

À force d'ajouter des couches, plus souple les unes que les autres, lorsqu'un objet désire invoquer une méthode d'un autre objet, en réalité il va passer par de nombreuses étapes :

  • L'objet cible est en fait un proxy. Ce dernier capture l'invocation de toutes les méthodes.

  • Le proxy génère des instances pour décrire l'invocation de la méthode invoquée.

  • Le proxy appelle une instance associée qui analyse la méthode à invoquer pour y apporter éventuellement des modifications, pour encapsuler le code d'une gestion de transaction, de la sécurité, etc. Il faut donc comparer le nom de la méthode et ses paramètres avec différentes combinaisons pour savoir de quoi elle traite. Et une boucle de plus. Plus il y a de méthodes dans l'objet, plus le traitement est long.

  • Puis le traitement est lancé à l'aide des paramètres construits par le proxy.

  • Le code utilise alors l'introspection pour trouver la bonne méthode dans la classe, construire un tableau d'objets avec les paramètres et la méthode est invoquée. Une boucle de plus.

  • La JVM analyse l'invocation dynamique pour la transformer en invocation directe vers la méthode cible.

  • Le traitement s'effectue sur l'objet cible ? Non, sur le prochain proxy qui se charge d'une autre couche !

  • Et on recommence pour ajouter la sécurité, la tolérance aux pannes, etc.

  • Au final, après plusieurs cycles, l'objet cible est enfin invoqué.

  • J'ai enfin atteint la cible. J'ai demandé un getNom(), je peux le retourner à l'appelant.

  • Et on remonte tous la chaîne dans l'autre sens. Faut-il modifier l'objet en retour ?

  • Pop, pop, pop, on remonte la chaîne.

  • Et enfin, le nom est retourné à l'appelant.

  • Je veux le prénom maintenant. Pas de problème, on recommence !

Combien de fois faut-il faire cela dans une application ? Plusieurs milliards ? Plusieurs milliards de milliards ? La CPU sert à quoi dans ce cas, à montrer que le framework est « super balaise », qu'il a une plus grosse « introspection » que les autres ? Et cela, sans compter que le serveur d'applications utilise également un machine virtuelle : la JVM.

Heu, je veux juste invoquer getNom() puis getPrenom(). C'est 10 cycles CPU normalement, pas 200 millions de cycles... On marche sur la tête.

Des audits sur la consommation CPU ont été effectué sur des projets d'EAI utilisant abusivement les services Web. Au final, 70% de la CPU ne sert qu'a parser les flux XML. Elle est où la valeur ajoutée ? Pour une machine qui travaille, il en faut sept qui font du bruit, de la chaleur et consomment les ressources de la planète. Et tout le monde est content. Voire pire, les développeurs sont fiers de leurs architectures et de leurs projets. Nous n'avons pas tous les mêmes critères.

Utiliser des composants, oui, mais à discrétion. La valeur ajoutée du composant doit être très forte. Il ne suffit pas de vouloir ajouter une ligne à son CV pour justifier un composant, ou que celui-ci soit « à la mode ». Nous avons eu la période « Strust », puis la période « Spring ». C'est quoi la prochaine, l'AOP ?

À quand une charte de qualité environnement pour les projets informatiques ? Il faut exiger une consommation de ressource minimum dans les appels d'offres. Valoriser les améliorations du code permettant de libérer un des serveur du parc informatique et contribuer à sauver la planète.

Parce qu'y'en a marre de la médiocrité, des développeurs en batteries et de la disparition des petits artisans soucieux du travail bien fait.

Jean-Pierre Troll

jp.troll(at)gmail.com (tout seul sans composants ni amphétamine)

Aucun commentaire:

Enregistrer un commentaire