Accueil > .Net, C#, Développement, Humeur > [C#] Réflexion sur la reflection

[C#] Réflexion sur la reflection

La reflection, parfois, ça peut être super utile.
Mais avant d’utiliser de la reflection, il convient de bien poser sa réflexion.

En effet, si cela peut être une solution de facilité à un instant donné, cela peut également être une source de difficulté non négligeable par la suite.
En réalité, j’avais bien une métaphore en tête, mais elle n’est pas à proprement parlé « politiquement correcte ». Donc, je vais éviter.

En attendant, dans ce billet, je vais donner deux exemples (que j’ai vu en entreprise, mais « annonymisés ») de ce qui justifie ce même billet.
Je parlerais ensuite de petites questions utiles pour réfléchir avant de faire et enfin de ce est généralement admit avec un peu de lecture supplémentaire.

 

 

Quand un développeur reprend un code existant, la première pensée est souvent « j’aurais pas fais comme ça » pour les plus polis d’entre nous.
Mais quand un code est réellement mal foutu ou étaient bien, y a 20 ans, on a souvent envie d’aller voir le(s) développeur(s) qui a(ont) mis en place le truc.
Un peu comme ça :
Shining - Jack Nicholson

Ou de vouloir remettre au goût du jour les bûchers en place publique (pour l’exemple), voir même de forcer les coupables à se petit suicider.

Alors, qu’est ce que j’entends pas la reflection sans réflexion ?

 

Contrôle des types/méthodes

 

Déjà, le fait de charger des types/méthodes ou autre à la volée.
Cela implique que l’on a aucun réel contrôle sur ce que le développeur peut faire.

Exemple :

using System;

namespace TestBLL
{
    internal class InternalDemo
    {
        internal InternalDemo() { }

        internal String MyInternalMethod()
        {
            return "Je ne suis pas censé être accessible.";
        }
    }
}

Si l’on met le tout dans une bibliothèque de classe, c’est sans doute pour qu’un développeur ne puisse pas avoir accès à cette classe.
Typiquement car elle est censée fonctionner avec d’autres qui, elles, peuvent être exposées, par exemple dans le pattern Façade.

Erreur, il est possible d’y accéder :

            // Chargement de l'assembly.
            Assembly assembly = Assembly.Load("TestBLL"); // Ma bibliothèque de classe.

            // Récupération du type voulu.
            Type type = assembly                                       // Dans mon assembly.
                .GetTypes()                                            // Tous les types.
                .Where(t => t.FullName.Equals("TestBLL.InternalDemo")) // Le nom complet de la classe.
                .FirstOrDefault();                                     // Un seul est attendu.

            // Récupération du constructeur par défaut.
            ConstructorInfo ctor = type.GetConstructor(
                BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public,
                null, new Type[0], null);

            // Création d'un objet de type InternalDemo.
            // Il n'est pas censé être accessible, donc 
            // Activator.CreateInstance(typeof(InternalDemo));
            // Ne peut pas fonctionner, car le type n'est pas accessible.
            Object objInternal = ctor.Invoke(new Object[0]);
            // Cependant, un objInternal.GetType() donnera bien un objet de type InternalDemo.

            // Récupération de la méthode interne.
            MethodInfo method = type.GetMethod("MyInternalMethod",
                BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);

            // Exécution de la méthode interne.
            String objMethod = method.Invoke(objInternal, null) as String;

La reflection va donc permettre d’utiliser des types/méthodes/autres… alors qu’ils ne sont pas censés être accessibles.
Ce qui veut dire que ma bibliothèque de classes bien architecturée pourra être utilisée d’une manière totalement non prévue !
Et donc occasionner des comportements qui peuvent être totalement imprévisibles.

L’exemple type est toujours encore le pattern Façade : si l’on change l’implémentation sous-jacente, il est possible que le type InternalDemo disparaisse au profit d’un autre. Ce qui va occasionner de gros plantage à l’exécution du code ci-dessus (et donc potentiellement beaucoup trop tard si le code n’a pas été convenablement testé).

Pour obtenir le namespace complet, il y a plusieurs moyens.
Le premier, c’est de décompiler la DLL (du genre avec ILSpy).
Par contre, si c’est obfusqué, c’est juste un brin plus complexe : il suffit de récupérer les types de l’assembly, au runtime (voir le pavé de code suivant).

 

Les collections de types

 

A plusieurs reprises, j’ai vu des collections contenant des types, avec pour clef une interface, une chaîne de caractères ou autre.
Grosso modo, tous les types dont le nom suit un certain motif ou alors présent dans un certain namespace sont chargés dans cette collection.
Dès que l’on a besoin d’un type, il est renvoyé par une méthode spécifique qui lit dans la collection.

Le premier inconvénient est à la lecture du code.
Il est totalement impossible de savoir si un type est ou non utilisé en faisant une recherche.
J’entends pas là une recherche sous forme de chaîne de caractères (ctrl+F) ou par référence (shift+F12).

Un exemple de ce qu’il est possible de faire ?

            // Chargement de l'assembly.
            Assembly assembly = Assembly.Load("TestBLL"); // Ma bibliothèque de classe.

            // Chargement de la collection (au lancement de l'application puis mise en cache ou singleton...).
            IEnumerable<Type> types = assembly               // Dans mon assembly.
                .GetTypes()                                  // Tous les types.
                .Where(t => t.Namespace.Equals("TestBLL"))   // Dans le namespace souhaité.
                .Where(t => t.Name.EndsWith("Impl"));        // Dont le nom du type fini par Impl.

            Dictionary<Guid, Type> collection = new Dictionary<Guid, Type>();

            // On boucle sur tous les types qui sont éligibles.
            foreach (Type item in types)
            {
                // On récupère les interfaces implémentées par le type en question.
                Type[] typeInterfaces = item.GetInterfaces();

                // Et on ajoute chaque type par interface qu'il implémente.
                foreach (Type itf in typeInterfaces)
                {
                    collection.Add(itf.GUID, item);
                }
            }

            // Récupération du type concret (dans une méthode qui le type de l'interface en générique...).
            ITypeOne itfTypeOne = Activator.CreateInstance(collection[typeof(ITypeOne).GUID]) as ITypeOne;

            // Utilisation
            String retour = itfTypeOne.MyMethod();

Evidemment, c’est bien plus drôle quand il n’y a aucune gestion d’erreur.
Ça serait trop facile de comprendre d’où vient le problème quand on a ajouté un type sans réellement comprendre les implications dramatiques qui pourraient mener vers la fin du monde tel qu’on le conçoit. Enfin, un truc dans le genre…

 

Là où je veux en venir ?

 

Ce n’est pas parce que l’on peut faire quelque chose que l’on doit le faire !!!

Les bouts de code que j’ai montré sont destinés à illustrer mon propos.
Mais le faire effectivement sur une application, cela revient à se tirer une balle dans le pied pour plus tard (ou dans le pied d’un autre, ce qui est pire).
Ni plus, ni moins.

Et oui, j’ai déjà rencontré du code similaire. Plusieurs fois.
C’est juste TRÈS galère à débugger, à faire évoluer, ou plus simplement à comprendre !
D’où le fait que je sois quelque peu agacé de tomber sur une utilisation abusive de la reflection.

Alors, par pitié pour ceux qui vont venir derrière : réfléchir avant d’utiliser la reflection pour n’importe quoi.
Le premier que je prends à faire ça, je le maudis lui, sa famille et ses éventuels animaux domestiques !🙂

 

Le bon usage de la reflection ?

 

Du coup, à mon sens, la première question à se poser est : « Est-ce que je peux faire sans reflection ? »
Si oui, il ne faut surtout pas persévérer.
Si non : « Est-ce que je peux passer par de la généricité ? »
J’avoue, si la réponse est « oui », alors on aurait déjà du s’arrêter avec la première question.
Si non : « Est-ce que ça sera simple ? »
Si non, alors ne pas continuer non plus. Le code complexe peut la majeure partie du temps être découpé en des tâches plus unitaires et plus simples. Donc, il faut encore réfléchir.
« Est-ce que ce sera maintenable et/ou évolutif ? »
Là, en général, la réponse est souvent « non » pour ce qui touche à la reflection.
Parce que mal utilisée.

La reflection prend sa place naturelle pour tous les mapping (Object Relational Mapping). Mais là, il est utile de ne pas réinventer la roue et de voir ce qui existe pour se faciliter la tâche.

Le Pattern Factory est aussi éligible à la reflection, surtout quand la configuration est stockée dans un fichier (App.Config ou Web.Config), en spécifiant directement le namespace complet de la classe.

 

Un peu de lecture complémentaire

 

Catégories :.Net, C#, Développement, Humeur
  1. Aucun commentaire pour l’instant.
  1. No trackbacks yet.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :