vendredi 18 décembre 2009

Le mécanisme de réflexion – partie 2

Une extension du design pattern Strategy grâce au mécanisme de réflexion en .NET

Le contexte
Dans la société où je travaille, nous avons développé récemment un module d’acquisition de documents EDI. Ce module doit rematérialiser le document (générer une image à partir du contenu du fichier), puis l’injecter dans une chaîne de traitement métier pour analyse et export vers des systèmes informatiques cibles, genre ERP ou GED. Les documents traités sont des factures.
Un des principaux problèmes liés à l’EDI est la multiplicité des formats existant : EDIFACT, ANSI X12 pour les plus connus, mais surtout les formats non standards mais déjà abondamment utilisés dans des cadres de partenariats de dématérialisation, notamment dans le monde de la grande distribution. Le module doit donc pouvoir s’adapter facilement à un nouveau format EDI que nous découvririons lors de la mise en œuvre chez un client.

Dans les grandes lignes, le module est composé des trois fonctions suivantes :
1. Lecture du fichier EDI source
2. Rematérialisation du document
3. Injection dans la chaîne de traitement métier.

Solution 1
Nous optons pour le design pattern Strategy pour implémenter la lecture du fichier EDI source. La célèbre interface « Strategy »  publiera une méthode qui pourra être implémentée autant de fois que nous aurons formats à gérer. Chaque implémentation concrète de l'interface crée une représentation objet du document EDI qui sera ensuite utilisée pour la rematérialisation et l’injection dans la chaîne de traitement métier.
Donc c’est parti, on développe, on teste, on compile, on livre et on met en production !
Quelques temps plus tard, le client annonce un nouveau format à gérer. C’est facile, le pattern Strategy permet d’ajouter de quoi lire ce nouveau format sans prendre de risque avec l’existant. Donc pas de problème, on développe donc une nouvelle implémentation de l’interface « Strategy », on modifie le programme principal, puis on recompile et on livre une nouvelle version du module d’EDI et le tour est joué !
Malheureusement, le client ne l’entend pas de cette manière ! Nouvelle version du module d’EDI veut dire nouvelle exécution des plans de tests d’intégration avec son SI. Il doit mobiliser son informatique interne, ses chefs de projets fonctionnels. Les délais de mise en production vont se chiffrer en semaines, voire en mois. Qui va payer ? Vous avez beau arguer que vous avez utilisé le design pattern approprié et qu’il n’y a pas de risques, il n’y a rien à faire. La situation avec le client s’est indéniablement dégradée !

Solution 2
L’idéal serait que la prise en charge d’un nouveau format se résume à déposer un nouvel assemblage et mettre à jour un paramètre décrivant dans quelles circonstances utiliser ce nouvel assemblage. Ceci est possible grâce à la réflexion. Si chaque implémentation de l’interface Strategy est isolée dans un assemblage, il suffit de récupérer dans les paramètres l’assemblage à charger (il peut être fonction de l’émetteur du document, par exemple) et la réflexion se chargera du reste.
L’interface Strategy s’appelle ici IEdiInvoiceParser. Elle propose une méthode Parse() qui prend un flux de données en entrée et renvoie un objet métier Invoice en sortie. En voici le code en C# :



Le code ci-dessous montre comment créer une instance d’une classe à partir du nom de l’assemblage et de la classe.



 

Noter la puissance de l’instruction CreateInstanceAndUnWrap() qui charge dans le domaine courant l’assemblage désigné, puis crée une instance du type passé en paramètre et enfin renvoie une référence vers cette instance. Naturellement, pour que le cast fonctionne, il faut que ce type implémente l’interface IEdiInvoiceParser définie ci-dessus !
Même si l’opération tient en une seule ligne, il peut être intéressant de l’isoler dans une classe. Dans la pratique, cette classe pourra également récupérer les informations assemblyName (nom complet de l’assemblage à charger) et className (nom de la classe préfixé par le namespace) à partir d’autres informations comme la source du fichier EDI, par exemple.

L’utilisation est des plus simples et des plus rapides. Il n’y a plus qu’à laisser fonctionner le polymorphisme !




 

Pour conclure...
La solution 2 est meilleure car elle allie vitesse d’exécution (grâce au polymorphisme) et découplage (grâce à la réflexion). 
Une de ses limites est que les assemblages contenant les stratégies de lecture ne peuvent pas être déchargés car ils sont chargés dans le domaine courant de l’application. Cela pourrait donc poser des problèmes de consommation mémoire si on utilise cette technique pour implémenter un système de plugin. Dans ce cas, il sera préférable de charger l’assemblage dans un autre domaine, qui lui pourra être déchargé à la demande.

mardi 8 décembre 2009

Le mécanisme de réflexion – partie 1

En naviguant sur les forums de développement, il est assez fréquent de trouver une question relative à la réflexion, ou bien un intervenant qui propose d’utiliser la réflexion  comme solution à un problème. Les exemples ci-dessous sont donnés dans l’environnement .NET, mais la mécanique est très proche en Java.

Quelles sont les possibilités offertes par ce mécanisme ?

Le mécanisme de réflexion offre un très large spectre de possibilités : chargement dynamique d’assemblages, exploration dynamique des types d’un assemblage, création d’instances de classes, déclenchement de méthodes, compilation de code à la volée, etc.

Quels sont les problèmes liés à son utilisation ?

Le mécanisme de réflexion présente certes des avantages, mais il a aussi des inconvénients qu’il est important de maîtriser :

  • Les performances : le déclenchement d’une méthode par l’intermédiaire de la réflexion (instruction Invoke) est beaucoup plus lent que son appel direct. Il est cependant possible d’optimiser ce chargement en utilisant des Interfaces.
  • La conception : La réflexion offrant un moyen d’accéder à chaque membre de chaque classe de chaque assemblage, elle peut occulter la nécessité d’une conception rigoureuse et l’utilisation de Design Patterns. Par exemple, la réflexion permet de connaître le type d’une classe dérivée depuis du code situé dans sa classe mère (c’est un peu tordu, mais c’est possible). Ce genre d’utilisation viole le principe LSP (Liskov Substitution Principle) et suggère un design médiocre qui sera tôt ou tard difficile à maintenir.
  • La clarté du code : la lisibilité du code source est très nettement dégradée. Ce critère n’est pas seulement subjectif et peut à terme être source de bugs et de régressions lors de la maintenance du logiciel.
  • L’obfuscation : elle empêche le chargement dynamique des classes puisque leur nom a été modifié.

Quand doit-on utiliser la réflexion ?

Compte-tenu de ses inconvénients, l’utilisation de la réflexion devrait être réservée au seul usage suivant : la manipulation par un assemblage A d’autres assemblages qui n’étaient pas connus au moment de la compilation de A. Dans la pratique, on trouvera 3 grandes familles d’applications :
  • L’exploration d’assemblages à l’exécution : les outils ildasm.exe, fxcop.exe ou encore .NET Reflector utilisent massivement la réflexion pour explorer les assemblages. Visual Studio l’utilise également dans son explorateur d’objets ou lors de la complétion de code.
  • Le chargement dynamique d’assemblages (late binding) : un assemblage A peut utiliser la réflexion pour charger explicitement un autre assemblage B si B n’était pas connu lors de la compilation de A. Cela permet par exemple de construire des mécanismes de plugins.
  • Compilation de code à la volée : Dans certains cas, on souhaitera compiler et exécuter dynamiquement du code. Cela peut être utile pour évaluer des expressions mathématiques qui sont fournies à l’exécution. Le temps perdu  lors de la construction et la première compilation de l’assemblage est compensé par la vitesse des exécutions suivantes.

Un prochain article présentera une utilisation du chargement dynamique d’assemblages sans dégradation notable des performances.