La réflexion au service de la modularité
Un petit tutoriel sur les principes, l’intérêt et la mise en place dans un projet de la réflexion à des fins de modularité. Il ne s’agit pas ici de dire qu’il est conseillé de réfléchir quand on code, mais bien de décrire un mécanisme peu connu qui peut être bien utile lorsqu’il est pris en considération dès les phases amont de conception.
|
|
Vu
482
fois
|
Avant de parler de la réflexion, prenons le temps d’étudier le fonctionnement standard que nous utilisons tous.
Pour appeler une méthode en .Net, il faut référencer la dll dans laquelle se trouve le code que l’on souhaite utiliser, connaitre la classe et la méthode concernées.
// On identifie le namespace correspond à notre DLL using MaDLL; ... // On effectue l’appel de la méthode MaDLL.MaClasse.MaMethode(...); ... |
Une autre approche concerne l’utilisation de DLL natives pour appeler depuis du .Net du code non managé. Le principe reste malgré tout très similaire : on identifie la DLL concernée, et il est nécessaire de connaitre la méthode appelée.
// On identifie la DLL et la méthode souhaitée [DllImport("coredll.dll")]
private static extern bool RegisterHotKey(IntPtr hWnd, ...); ... // On effectue l’appel de la méthode RegisterHotKey(...); ... | |
Dans la majorité des cas, ce fonctionnement somme toute logique va répondre au besoin. Cela implique pourtant une limitation forte, qui est de connaitre à l’avance l’ensemble des librairies et des méthodes que l’on peut être amené à appeler au cours de l’exécution du programme.
Il est impossible dans ce contexte d’ajouter une nouvelle DLL qui serait appelée dynamiquement par un programme existant. C’est pourtant ce que l’on souhaite faire dans un fonctionnement de type modulaire (plug-in) où on définit un noyau applicatif qui doit être en mesure d’appeler et d’utiliser des méthodes et classes de librairies qu’il ne connait pas.
Mais comment notre programme peut-il appeler une méthode qui n'existe pas encore au moment de sa compilation ?
Pour mieux comprendre la suite, commençons par faire une petite introduction du fonctionnement du .Net, avec en particulier la notion de CLR.
Lors de la compilation d’un programme en Basic, C, C++, Prolog, Lisp, Fortran, … le code est converti en instructions machines. Lors de la compilation en .Net (et le principe est semblable pour le Java), le code est converti en instructions MSIL (Microsoft Intermediate Language) qui est converti dynamiquement en instructions machine par le CLR (Common Language Runtime) lors de l’exécution.
Cette conversion effectuée lors de l’exécution qui va nous permettre de générer des instructions machines différentes en fonction du contexte.
Par ailleurs, du fait de la compilation en langage MSIL et non en instructions machines, le Framework .Net nous permet de récupérer au sein de librairies compilées la liste des classes, des méthodes, etc. dynamiquement à l’exécution.
La combinaison de ces deux possibilités nous permet de définir dynamiquement lors de l’exécution le nom de la classe et de la méthode que l’on souhaite appeler, en fonction de résultats obtenus par exemple précédemment dans la même exécution.
C’est ce principe qu’on appelle la réflexion qui va nous permettre d’effectuer depuis un programme des appels à une méthode qui n’était pas connue lors de la compilation de celui-ci.
Il faut néanmoins être conscient que la réflexion implique qu’il n’y a plus aucun contrôle lors de la compilation de l’existence des méthodes appelées, et qu’on s’expose donc à des erreurs de type « MissingMethodException » lors de l’exécution.
Par ailleurs, il est utopique d’imaginer que le programme principal va être en mesure d’appeler pertinemment des méthodes qu’il ne connait pas si celles-ci ne s’inscrivent pas dans un cadre très strict (en particulier au niveau de la signature des méthodes afin de leur transmettre des paramètres cohérents).
L’utilisation de la réflexion se fait à l’aide de la classe System.Reflection.Assembly :
// On charge la librairie souhaitée Assembly maLibrairie = Assembly.LoadFrom("MaLibrairie");
// On identifie la méthode que l’on souhaite appeler MethodInfo method = module Assembly.GetType("Namespace.MaMethode");
// On appelle cette méthode Object retour = method.Invoke(null, new object[0]); |
Le premier argument de la méthode Invoke est null si la méthode est statique (approche conseillée) et le deuxième argument contient la liste des paramètres de cette méthode.
La valeur de retour peut être un objet de type primaire (string, bool, int…) mais cela ne présente que peu d’intérêt si on se limite à ces types. Afin d’étendre les possibilités, le type de retour sera en général de type Interface, référencée aussi bien par le socle que par l’ensemble des modules. Chaque module est libre de son implémentation, mais cette interface donne le cadre nécessaire à la communication.
|
// On récupère un objet qui sera le point d’entrée du module
IModule retour = (IModule)method.Invoke(null, new object[0]); |
|
Si on ne souhaite pas utiliser une méthode statique, il faut créer une instance de l’objet préalablement à l’appel de la méthode :
// On crée une instance de la classe souhaitée
object maClasse = maLibrairie.CreateInstance("MaClasse");
Object retour = method.Invoke(maClasse, new object[0]); | |
L’objet nécessite donc un constructeur par défaut ne prenant pas d’argument, et cela présente donc peu d’intérêt par rapport à l’utilisation d’une méthode statique.
On peut désormais appeler dynamiquement des méthodes dans des librairies non référencées, mais encore faut-il savoir quelles méthodes appeler. La récupération dynamique de la liste des méthodes à appeler va nous permettre de finaliser la mise en place de notre application modulaire.
Le pattern le plus courant pour la mise en place d’un socle modulaire est l’utilisation du pattern Factory avec une classe dédiée qui fait office de point d’entrée du module.
public class Factory { // Ma méthode statique pour la réflexion public static IModule CreerClassePrincipale(string arg) { return new ClassePrincipale(arg); } } | |
Afin de connaître la liste des Factory (librairies, classes et méthodes) à appeler, on passe par un fichier de configuration (qui peut être généré dynamiquement) :
<Modules>
<Id>MonModule</Id>
<Librairie>MaLibrairie.dll</Librairie>
<Factory>Namespace.Factory</Factory>
<Methode>CreerClassePrincipale</Methode>
<Argument>MonParametre</Argument>
...
</Modules> | |
Le fichier de configuration pourra également être enrichi par des informations complémentaires sur les modules telles que l’icône à afficher, la position de l’icône sur le menu graphique principal, des notions de statuts, profils, etc.
Avec l'arrivée à partir du Framework 2.0 du principe des attributs de classe, l'utilisation de la réflexion est simplifiée.
L’utilisation des attributs de classe permet d’affecter des attributs à des classes, et de déterminer dynamiquement quelles sont les classes qui répondent aux critères (en implémentant directement les méthodes de IModule par exemple).
L’utilisation de ces attributs fera par contre l’objet d’un article à part entière…
Bon code !
|