mercredi 17 février 2016

Mythes et légendes de la POO 2/2

Voici donc le second article de cette série qui vise à démystifier certains concepts classiques de la POO. Les choses sont infiniment plus simples qu'on veut bien le faire croire, il faut que ça se sache.


AOP

L'AOP est une des deux techniques fréquemment mise en oeuvre en sous main par la plupart des frameworks pour réaliser leurs tours de magie. 

Par là, je veux dire que quelqu'un qui chercherait à comprendre comment font ces outils pour rendre les divers services qu'il nous offrent, n'aurait aucune chance d'y arriver sans connaissance de ces techniques. 

En effet, la simple utilisation des techniques de programmation "classique" ne permet pas de faire ce qu'ils font.

Attention spoiler ! je dévoile des secrets et je vais me mettre tout la profession à dos (nan je déconne).

Alors AOP ça veut dire Aspect Oriented Programming. C'est parfois considéré comme une extension de la POO même si je trouve ça un peu prétentieux. 

Le plus compliqué dans l'AOP c'est le jargon difficilement compréhensible qui entoure la discipline, et la syntaxe horrible de son langage d'expression (nous verrons ce que c'est). ça sent encore le geek poilu cette histoire. Nous allons soigneusement éviter d'utiliser ce vocabulaire tout pourri.

L'idée de base est très simple et comme d'habitude, pour comprendre la solution, il faut commencer par comprendre problème qu'on vise. 

Le problème à résoudre

Le problème : un investisseur veut démarrer une startup, il lui faut un logiciel qui réalise son idée, Pour faire le logiciel, il lui faut des développeurs. Et les développeurs, ça coûte cher en taux horaire. Mais comme il ne lui faut que 5 écrans, il se dit que ça va aller vite et que ne sera pas cher. Puis quand on lui donne le devis, il défaillit : quoi, comment ça peut coûter aussi cher, je me fais arnaquer ou quoi, expliquez moi ! Mon fils il m'a dessiné mes écrans HTML en 2 jours sur son PC et vous me demandez 2 mois de travail à 3 personnes ?

Et là,on lui explique qu'il faut gérer tout un tas de problématiques techniques bien compliquées et auxquelles il n'a bien sur pas pensé, car comme la plupart des gens qui croient s'y connaître en informatique, il ne voit en fait que la surface des choses (je passe les détails de toutes les problématiques à gérer, l'inventaire serait trop long). Et là, il se rend compte qu'en fait la plus grande partie du temps des développeurs est consacrée à gérer autre chose que les fonctionnalités rendues à l'utilisateur final. D'où le coût.

Si les développeurs passent du temps sur ces problèmes techniques, c'est que leur prise en compte est transverse à l'ensemble des développements : tous les écrans (ou presque) nécessitent une gestion de la sécurité pour contrôler l'accès aux seules personnes authentifiées et avec le niveau de droit requis, tous les écrans (ou presque) impliquent de contrôler l'ouverture et la fermeture d'une connexion vers la base de données, etc.


Tous les écrans impliquent une gestion d'erreur rigoureuse qui permet de loguer les problèmes pour avoir une chance de les détecter et de les résoudre. En effet, quand l'internaute hurle chez lui car l'application vient de planter, et qu'il a perdu sa saisie, on ne l'entend pas depuis Bengalore en Inde.

Dans l'espace (d'internet) personne ne vous entend crier.


Tous ces points ennuyeux et sans réelle plus-value sont ceux qui demandent le plus de technicité et réclament la main d'oeuvre la plus qualifiée, donc la plus chère.

Dernier point, essentiel, les problématiques visées sont génériques (ouvrir une connexion à la base de données, c'est la même chose quelque soit l'écran pour lequel on a besoin de lire des données, il n'y a que la nature des données lues qui change ).

Du coup, ce qui serait bien, ce serait qu'on puisse développer séparément la prise en compte générique de ces problèmes transverses, par des experts, puis qu'on se débrouille pour que ce soit "mixé" avec le code "métier" (les fonctionnalités de l'application) plus simple et implémenté par des développeurs moins qualifiés et moins chers.

Et là, on touche au cœur de l'AOP : 

  • la séparation des problématiques, (separation of concern) : pouvoir faire développer séparément et par des personnels différentes les aspects techniques transverses complexes, appelés aspects, et les développements métiers plus simples et plus importants tant en valeur ajoutée qu'en volume
  • le tissage des aspects : "tissage" (weaving) est le terme consacré en AOP  pour désigner l'opération qui permet de regrouper le code métier et le code transverse (aspects) en un tout unique et cohérent (le programme final)
Au delà de l'aspect économique; il y a surtout un aspect qualitatif ; le développeur pas cher qu'on à tendance à utiliser pour le gros du développement, ben si il est pas cher, c'est pas par hasard : soit il est débutant, soit il est mauvais, et si vous n'avez pas de chance, il est les deux à la fois. Ou pire encore, il peut être bon mais tellement mis sous pression par le respect des délais et du budget qu'il commet des erreurs.

Et, il est bien sur plus facile de faire des erreur sur les aspects les plus techniques et les plus répétitifs. Du coup on a plein de petites erreurs de programmation disséminées un peu partout, non couvertes par les tests unitaires fonctionnels automatisés, avec un impact global sur le niveau de qualité de l'application.

Un exemple typique est une mauvaise gestion des erreurs, qui entraîne la non libération de connexion à la base de données, qui entraîne l'épuisement du pool de connexions du serveur d'application, qui entraîne un dysfonctionnement général de l'application difficilement détectable par les équipes de supervision. J'ai vu ce genre de chose un nombre incalculable de fois, pourtant les techniques pour éviter ceci (AOP ou autre) existent depuis toujours. En assurant une gestion transverse de ces aspects via l'AOP, on évite ces erreurs de programmation, et en outre les développeurs peuvent consacrer toute leur attention au code métier qui apporte la réelle plus-value.

On notera au passage, que tout étant lié, il y a aussi des problématiques de management cachées derrière ces choix : il y aura toujours dans vos équipes des gars qui voudront absolument traiter les problèmes technique (pour des tas de raisons différentes selon les individus), et ils seront frustrés de ne pas pouvoir le faire.

Ca se gère mais il n'est pas inutile d'avoir un lead technique qui surveille le respect de l'architecture.

Les techniques de tissage

Une fois qu'on a compris ça, il reste à comprendre les façons d'y arriver. Il y en a 3, à ma connaissance en tout cas, deux simples à expliquer et une troisième que je vais développer car elle est très intéressante d'un point de vue technique :
  • au "compile time" : au moment de la compilation, on mixe les aspects au code métier dans le code code source fourni au compilateur. Il faut bien sur un compilateur spécial. Le projet open source AspectJ fournit ça
  • au "loading time" : plus rusé, le code compilé n'est pas modifié mais on instrumente les classloaders de la JVM pour qu'ils modifient dynamiquement le bytecode au chargement des classes. Pour ceux qui ne connaissent pas Java, retenez que le programme est modifié au moment où il est chargé en mémoire avant d'être exécuté. On ne perd pas de temps à la compilation mais à l'exécution. Je détaille aussi le principe de manipulation de bytecode plus loin dans l'article.
  • "runtime" : à l'exécution. C'est la technique la plus utilisée. Je vais la détailler plus loin.

Avant de détailler plus avant, il y a une première question qui se pose. Comment l'outil, quel qu'il soit et qu'elle que soit la stratégie de tissage retenu, fait il pour savoir à quel endroit il doit ajouter le code des aspects au code métier écrit par les développeurs ?

Hé bien pas de magie ici, c'est le développeur qui le lui a dis (enfin l'architecte logiciel en charge de la conception globale). Pour ce faire, il s'appuie sur un "langage d'expression" (défini par le projet AspectJ) et qui permet de cibler par des expression régulières spécifiques les endroits où "injecter" le code transverse. Ce langage d'expression a une syntaxe particulièrement barbare mais on s'y fait ;-)

Par exemple, on peut exprimer qu'on veut injecter un code transverse "avant", "après", "autour" de l'exécution de toutes les méthodes présentant une certaine signature, ou étant dans un sous package quelconque de tel package de base. "avant et "après" se passent de commentaire, "autour" permet de mettre en place des blocs de gestion d'erreurs pour du code de terminaison (bloc finaly) ou de gestion d'exception (bloc catch).

Bien, on a presque fini, il nous reste le plus amusant, la technique utilisée pour le weaving au runtime (le tissage des aspects à l'exécution). A partir d'ici, je cesse d'essayer de m'adresser à tout le monde et j'aborde un vocabulaire plus technique car ce serait très fastidieux autrement.

Focus sur le tissage à l'exécution

Attention ; maîtrise de la POO et du JDK requis.

Commençons par le design pattern "Proxy". Un proxy (intermédiaire en Français) est un objet qui reçoit un appel à la place de l'objet qu'il proxifie, et qui retransmet l'appel à l'objet initialement ciblé (il  a donc une composition sur l'objet proxifié). Il en profite pour ajouter  du comportement avant l'appel, après l'appel, ou autour de l'appel (try/catch/finaly).

Le proxy fournit impérativement la même interface que l'objet qu'il proxifie pour pouvoir être invoqué à sa place de façon transparente, par simple application du polymorphisme.

Pour cela, il existe deux techniques. La plus simple consiste à s'appuyer sur la fonctionnalité de "proxy dynamique" fournie en natif par le JDK depuis la version 1.4 de mémoire. Cependant, cette technique implique que la classe à proxyfier (le code métier dans lequel on veut injecter du code transverse) implémente une interface dont le contrat inclus les méthodes à proxyfier. Si tel n'est pas le cas, une seconde technique consiste à générer dynamiquement (par manipulation de bytecode), une classe qui étend par héritage la classe à proxyfier. Cette technique a ses propres contraintes car bien entendu la classe à proxyfier ne peut pas être finale (les méthodes non plus), ni les méthodes statiques (pour des raisons évidentes, de tout façon le statique c'est pas bien).

Etant donné que le proxy a vocation à être invoqué en lieu et place de l'objet proxyfié, il faut également que le code instanciant l'objet à proxyfier soit "polymorphique", c'est à dire qu'il déclare et manipule l'objet au travers de son interface (ou par la classe ancêtre au pire) et que l'instanciation ne soit pas codée en dur mais déléguée à un mécanisme qui permette de faire varier la classe effectivement instanciée, une factory donc (design pattern).

Depuis le temps qu'on vous dis qu'il faut programmer au travers de types abstrait, vous devez commencer à comprendre pourquoi ;-)

Injection de dépendances et framework : la boucle est bouclée

Si vous avez suivi, et compris, tout ce que j'ai expliqué ici, et dans l'article précédent sur l'IOC et l'injection de dépendance, vous devez comprendre qu'un framework d'injection de dépendances est le compagnon idéal de l'AOP.

Et bien sur, c'est la technique mise en oeuvre par Spring pour apporter des services transverses, à l'instar des EJB d'un serveur JEE. La gestion déclarative des transactions par spring-tx (framework de gestion de transaction) est une simple mise en oeuvre de ce mécanisme avec simplement une légère surcouche de configuration, et des aspects pré-implémentés conçus en outre pour pour profiter de la grande versatilité de Spring en matière de gestionnaire de transaction selon l'environnement d'exécution et le type de transaction (distribuée ou non).

Je connais moins intimement spring-security (framework Spring de gestion de la sécurité, ex ACEGI) mais je n'ai aucun doute sur le fait que c'est ce même mécanisme qui est utilisé en cas de gestion des contrôles d'accès au niveau de la couche métier (en cas de gestion de la sécurité au niveau de la couche d'invocation web, il utilise les filtres http de l'API Servlet JEE).

Voilou, rien de bien compliqué au final. Et en prime, tout ceci est parfaitement expliqué dans la documentation de Spring. Encore faut il se donner la peine de la lire (bon c'est pas un roman non plus).

Génération de bytecode

J'invite les personnes qui n'ont pas les idées totalement claires sur la relation entre un code source, un code compilé, une architecture de processeur, et la virtualisation de processeur (tiens au hasard par la JVM) à lire rapidement cette série de deux articles "qu'est qu'un programme"

Produire du bytecode est normalement le travail du compilateur ; il lit le code source, et le transforme en instruction binaires suivants la spécification de la JVM. 

Mais rien n'interdit d'écrire un programme qui fasse la même chose. C'est même plus qu'intéressant étant donné la nature compilée de Java et l'impossibilité qui en découle d'exécuter du code évalué dynamiquement (contrairement par exemple et au hasard à javascript). Et en quoi est ce intéressant au fait ? 

Le premier cas d'utilisation serait celui où il nous faut un logiciel très versatile, au delà des capacités déjà importantes apportées par le paradigme objet (qui impose en gros de pouvoir au moins définir une interface et qui dans l'esprit requiert un typage fort incompatible avec la gestion de cas de figure imprévisibles). Soyons clair, ce n'est pas un cas de figure fréquent dans le développement en entreprise, hormis pour les éditeurs de logiciel qui doivent s'adapter fortement (genre ERP). En outre, il existe la possibilité depuis quelque temps déjà d'intégrer un moteur de scripting dans la JVM ce qui est fait pour répondre à ce genre de cas, avec même un interpréteur Ecmascript fourni en standard (Rhino dans java 6 et Nashorn depuis java 8).

Le second cas d'utilisation est celui qui nous intéresse dans le cadre de cet article sur les tours de passe passe des frameworks. Ces derniers font un usage immodéré de la génération dynamique de bytecode, en particulier Hibernate. Spring également pour la mise en oeuvre de l'AOP dans certains cas. Et bien d'autres.

Donc ne vous flagellez pas si vous n'arrivez pas à comprendre comment font ces frameworks : ils trichent purement et simplement. Si l'AOP en mode "runtime" n'est après tout qu'une utilisation judicieuse des capacités natives des langages objets (héritage, composition, polymorphisme) et une combinaison de design pattern, la génération de bytecode est clairement un procédé malhonnête ;-)

Un dernier mot : la génération de bytecode est quelque chose de non trivial... si vous voulez l'envisager, il est plus que recommandé de vous appuyer sur une des deux librairies existantes (cglib, javassist), librairies que comme par hasard vous trouverez dans les dépendances (tirées par Maven ou Graddle, on est bien d'accord) de vos frameworks préférés. Ceci dis, un développement d'entreprise classique n'a guère de chances de nécessiter le recours à ces techniques.

Conclusion

Ma première motivation pour écrire ces deux articles, sur des sujets qui ne sont en rien nouveaux, est venue, je pense, de l'incapacité que j'ai constatée chez de très nombreux développeurs de s'exprimer clairement sur ces concepts de base. Alors même qu'ils les utilisent à longueur de journée au travers des frameworks qu'ils manipulent.

Il y aurait encore  bien des choses à dire. L'informatique n'échappe pas aux phénomènes de mode et régulièrement des sujets viennent sur le devant de la scène et finissent par arrive au comptoir, ou à la machine à café : tout le monde en parle, sans forcément maîtriser, ce qui est par ailleurs bien compréhensible quand il s'agit de concepts novateurs. Il en résulte beaucoup de bruit et de fausses idées qu'il faut bien finir par débrouiller

Par ailleurs, l'informatique est un business. Faire croire que les choses sont plus compliquées qu'elle ne le sont réellement est un travail, mais ce n'est pas ma vision de choses, ça me perdra ;-)

Aucun commentaire:

Enregistrer un commentaire