Beaucoup de développeurs se sont un jour posé la question :
Comment puis-je afficher ou affecter la valeur d’une variable grâce à son nom stocké dans une variable de type string
Sans le savoir, ils ont par cette simple question posé un premier pas dans le monde de la méta-programmation et plus précisément, dans ce cas de figure, dans le monde de la réflexion. Bien sûr elle ne doit pas être confondue avec la réflexion issue de la pensée et de l’esprit, il s’agit ici de la réflexion telle qu’on pourrait la retrouver lorsqu’un miroir nous renvoie notre image.
La réflexion est un ensemble de mécanismes qui permettent à un programme informatique de s’examiner lui même ou bien de modifier son état ou sa structure au moment de son exécution. Elle s’inscrit au sein du paradigme de méta-programmation – méta qui signifie au dessus, au delà, il s’agit donc de programmation au dessus de la programmation.
Les deux mécanismes qui composent la réflexion sont nommés l’introspection et l’intercession. Derrière ces termes barbares, rien de compliqué : l’introspection concerne la capacité d’un programme à s’examiner, tandis que l’intercession concerne la capacité d’un programme à se modifier.
Un des intérêts majeur de la réflexion est qu’elle permet dans certains cas de réduire considérablement le nombre de lignes de code grâce à la généricité qu’elle induit. Elle permet aussi de programmer certains processus variables non connus par avance qui seraient difficilement programmables sans ce mécanisme. Finalement cette technique a aussi l’atout de supprimer des suites de conditions potentiellement extensibles qui rendrait vite le programme illisible et non maintenable.
Bien sûr, l’utilisation d’un tel mécanisme à un coût en terme de performance et l’exploiter massivement à tort et à travers n’est pas une option envisageable. Il ne s’agit pas non plus d’une solution miracle à tous les problèmes (voir anti-pattern marteau doré), il s’agit plutôt d’un dernier recours en cas d’impossibilité d’automatiser un processus autrement que par son utilisation où bien de vouloir appliquer un algorithme concis pour des raisons de lisibilité ou de maintenabilité.
On distingue la réflexion qui a lieu à l’exécution du templating qui a lieu à la compilation.
Que permettent concrètement ces techniques ?
Elles permettent par le biais de l’introspection de récupérer absolument toutes les informations sur la structure d’un programme et sur son état au moment de l’exécution. Il est donc possible de récupérer les noms, les valeurs, les types et l’ensemble des métadonnées des classes, des structures, des fonctions, des méthodes, et en programmation orientée objet, des propriétés, des attributs, des champs d’une classe, etc.
Sans le savoir, de nombreux développeurs utilisent régulièrement l’introspection de type. Il s’agit, pour citer quelques exemples, du fameux typeof où GetType de C#, du instanceof ou du get_class de Php, du typeof de javascript ou encore du isKindOfClass d’Objective-C.
On reconnaitra la réfléxion par le fait qu’il est possible de récupérer ou bien d’affecter une propriété dont le nom est stocké dans une variable de type chaîne de caractères. La valeur de cette variable peut varier au cours de l’exécution du programme et ainsi permettre à une portion de code d’adopter un comportement générique.
Ci-dessous, une liste des cas les plus utiles.
Récupération et affectation de propriétés à partir d’une chaîne de caractères
Javascript
function Person() { this.name; this.firstname; } var propertyName = "name"; var person = new Person(); person.name = "bobo"; console.log(person[propertyName]); //Affiche "bobo" person[propertyName] = "bibi"; console.log(person[propertyName]); //Affiche "bibi"
PHP
class Person { public $name; public $firstname; } $propertyName = "name"; $person = new Person; $person->name = "bobo"; echo $person->{$propertyName}; //Affiche bobo $person->{$propertyName} = "bibi"; echo $person->{$propertyName}; //Affiche bibi
C#
using System; public class Test { public static void Main() { var propertyName = "Name"; var person = new Person(); person.Name = "bobo"; var propertyInfo = person.GetType().GetProperty(propertyName); Console.WriteLine(propertyInfo.GetValue(person, null)); //Affiche bobo propertyInfo.SetValue(person, "bibi", null); Console.WriteLine(propertyInfo.GetValue(person, null)); //Affiche bibi } } public class Person { public string Name {get;set;} public string Firstname {get;set;} }
Objective-C
NSString *propertyName = @"name"; Person *p = [Person new]; p.name = @"bobo"; NSLog(@"%@", [p valueForKey:propertyName]); // Affiche bobo [p setValue:@"bibi" forKey:propertyName]; NSLog(@"%@", [p valueForKey:propertyName]); // Affiche bibi
Instanciation de classe à partir d’une chaîne de caractères
On peut de la même manière instancier une classe à partir de son nom stocké dans une chaîne de caractères.
L’injection de code à la volée commence à rentrer dans le cas extrême où le programme s’injecte lui même des portions de code en vue de permettre une spécification, une réorganisation ou une évolution. C’est l’intercession ultime. L’injection de code à la volée est sûrement une des techniques les plus lentes de la réflexion et pour ainsi dire trouver des exemples légitimes qui l’utilise est difficile. Cependant, nous ne rentrerons pas dans les détails tant cette technique diffère énormément d’un langage à un autre. Le mapping d’un résultat vers un objet et un bon exemple pour montrer comment la réflexion permet de diminuer le nombre de lignes de code et rendre le programme plus maintenable. this[element] permet d’obtenir une propriété de l’objet sous la forme this[« proprerty »] à la place de this.property.
On obtient ici le même résultat avec et sans la réflexion. On note cependant que la réflexion permet à l’objet d’être étendu au niveau de ses propriétés sans modifier l’implémentation de la fonction de mapping d’où l’intérêt de son utilisation. Un tel processus pourrait être remplacé par du templating (ce qui reste de la méta-programmation) si la personne garde le contrôle sur son code source et sur sa compilation (ce qui n’est plus le cas lors de la définition de librairies ou d’API), et ce serait dans ce cas plus judicieux car plus optimisé. L’instantiation d’objet dans un factory permet de montrer rapidement un cas de suppression de condition. Le chargement d’un plugin externe va nécessiter, dans certains langages, la réflexion afin de pouvoir instancier les classes internes au plugin. Un exemple est présent dans l’article suivant : Implémentation d’un systéme de plugin en C#. Dans les langages statiquement typés qui permettent la définition de méthodes génériques, il est possible d’instancier une classe à la volée sans avoir à connaitre son type par avance. Cela permet de passer une classe en paramètre (paramètre de type) de fonction plutôt qu’une instance de classe (un objet). La fonction Run générique – c’est à dire qu’elle accepte un paramètre de type qui est ici en l’occurrence T – va pouvoir instancier la classe passée en paramètre de type. Dans cet exemple, la classe passée en paramètre de type doit implémenter l’interface IRunnable pour respecter la contrainte where T : IRunnable. Ci-dessous l’utilisation : Il est ainsi possible de passer en paramètre de type de la méthode toute classe qui implémente l’interface IRunnable, le programme sera capable de l’instancier sans ajouter la moindre ligne de code supplémentaire. Ce qui signifie que le code de la classe Process peut être clôturé et fermé tout en conservant un fonctionnement général, ce qui est pratique pour le développement de librairies. Voici un exemple où la réflexion trouve la place la plus légitime. Il s’agit ici de dicter les actions du programme par le biais d’un fichier externe. Cela peut servir dans de nombreux cas. Un exemple intéressant est celui de la création d’un moteur de template. L’appel de fonction est aussi une fonctionnalité permise par la réflexion assez intéressante. En effet dans le cas d’une Api Rest ou le routage s’effectue avec grâce a un url codé par une chaîne de caractères, il peut être utile en terme de généricité de faire appel à l’invocation de méthode par le biais de la réflexion. Bien sûr, il faut bien faire attention à ce que ceci ne constitue par une faille de sécurité, car autoriser l’appel de n’importe quelle fonction grâce à son nom et ses paramètres permettrait à n’importe qui d’exécuter très facilement n’importe quelle partie de code publiquement accessible. Par exemple, il serait possible de proposer une url au format suivant : http://www.monsite.fr/invoke/nomClasse/nomMethode/param1/param2/… Ci-dessous exemple de l’invocation d’une méthode dynamiquement à partir d’une chaîne de caractères en Php (Routage avec framework Slim) : On voit ici que l’intérêt de la réflexion se trouve dans le fait de ne pas devoir explicitement déclarer chaque routage séparément des uns des autres lors de l’ajout d’une fonctionnalité comme ci-dessous : Et elle permet d’éviter un unique routage avec une suite de condition extensible comme ci-dessous : Cette technique pourrait par exemple être utilisée dans l’écriture d’un système orienté aspect AOP [en] où les points de jointure (join point) seraient insérés à la volée afin de permettre l’exécution du code d’aspect selon une condition particulière. Poussé à son paroxysme, la réflexion permet à un programme d’en écrire un autre et de le compiler pendant son exécution. Cette fonction peut être fournie, soit à travers une API, soit directement par l’appel du compilateur. Sincèrement, il faut vraiment se trouver dans un cas particulier pour avoir à l’utiliser, par exemple écrire un évaluateur de code ou un langage de programmation particulier. Ces exemples permettent de comprendre dans quels cas la réflexion peut être utile, mais le champ d’application est bien plus vaste que ceux présentés ci-dessus. C’est au développeur de définir précisément si le besoin de son utilisation se fait ressentir et si elle apporte quelque chose de positif dans le développement supérieur aux aspects négatifs qu’elle provoque tel que la perte de performances. Il est aussi possible d’obtenir dans certains cas un résultat quasi-équivalent sans y faire appel, notamment grâce au templating (génération de code à la compilation) où encore par la définition de paires clé / valeur au sein de dictionnaires qui permettent de définir une sorte de configuration. J’espère que cet article vous aura permis de comprendre un peu mieux la réflexion ou bien d’approfondir vos connaissances préalables. En tout cas si vous l’avez compris, vous verrez assez vite que ces techniques permettent tellement de faire le café que vous ne pourrez plus vous en passer… et c’est bien ça, le piège de la réflexion. Javascript
function Person()
{
this.toString = function()
{
console.log('Im a human man !');
};
};
function Cat()
{
this.toString = function()
{
console.log('Im a cat man !');
};
};
var className = "Cat";
var obj = new window[className]();
obj.toString();
PHP
class Person
{
public function toString()
{
return "Im a human man !";
}
}
class Cat
{
public function toString()
{
return "Im a cat man !";
}
}
$className = "Cat";
$obj = new $className;
echo $obj->toString();
C#
using System;
public class Test
{
public static void Main()
{
string className = "Cat";
Type t = Type.GetType(className); //Récupére le type de la classe de nom className
object obj = Activator.CreateInstance(t); //Créer l'instance à partir du type
Console.WriteLine(obj.ToString()); //Affiche "Im a cat man !"
}
}
class Person
{
public override string ToString()
{
return "Im a human man !";
}
}
class Cat
{
public override string ToString()
{
return "Im a cat man !";
}
}
Java
class TestReflection
{
public static void main (String[] args) throws java.lang.Exception
{
Class<?> personClass = Class.forName("Person");
Person p = (Person)personClass.newInstance();
System.out.println(p);
}
}
class Person {}
Objective-C
id person = [NSClassFromString(@"Person") new];
Invocation d’une méthode à partir d’une chaîne de caractères
Javascript
function Person()
{
this.speak = function()
{
console.log('Hello !');
};
};
var methodName = "speak";
var person = new Person();
person[methodName](); //Affiche "Hello !"
PHP
class Person
{
public function speak()
{
echo 'Hello !';
}
}
$methodName = "speak";
$person = new Person();
$reflectionMethod = new ReflectionMethod('Person', $methodName);
$reflectionMethod->invoke($person); //Affiche "Hello !"
C#
using System;
using System.Reflection;
public class Person
{
public static void Speak()
{
Console.WriteLine("Hello !");
}
}
public class MainClass
{
public static void Main (string[] args)
{
string methodName = "Speak";
Person person = new Person();
MethodInfo mi = person.GetType().GetMethod(methodName);
mi.Invoke(person, null); //Affiche "Hello !"
}
}
Génération de code à la volée
Que faire avec ?
Les systèmes de mapping avec le parcours des propriétés
Javascript
var array = [];
array["name"] = "x";
array["firstname"] = "y";
function Person()
{
this.name;
this.firstname;
this.mapWithoutReflection = function(array)
{
this.name = array["name"];
this.firstname = array["firstname"];
};
this.mapWithReflection = function(array)
{
for (element in array)
{
this[element] = array[element];
}
}
this.toString = function()
{
return "name : " + this.name + ", firstname : " + this.firstname;
};
};
var personA = new Person();
personA.mapWithReflection(array);
console.log(personA.toString()); //Affiche "name : x, firstname : y"
var personB = new Person();
personB.mapWithoutReflection(array);
console.log(personB.toString()); //Affiche "name : x, firstname : y"
C#
class Person
{
public string Name {get;set;}
public string Firstname { get; set;}
public void MapWithReflection(object obj)
{
//Récupération des propriétés de obj
PropertyInfo[] pis = obj.GetType().GetProperties();
//Parcours des propriété de obj
foreach (PropertyInfo pi in pis)
{
//Récupére la propriété de this portant le nom de la propriété parcourue
PropertyInfo mePi = this.GetType ().GetProperty (pi.Name);
//Affectation de la propriété de this avec la valeur de la propriété parcourue
mePi.SetValue(this, pi.GetValue(obj, null), null);
}
}
//dynamic est le seul moyen de passer un objet anonyme en paramètre et de le traiter sans réflexion
//Sinon il aurait fallu le typer comme object
public void MapWithoutReflection(dynamic obj)
{
this.Name = obj.Name;
this.Firstname = obj.Firstname;
}
public override string ToString()
{
return "Name : " + Name + ", Firstname : " + Firstname;
}
}
public static void Main (string[] args)
{
var a = new {Name = "x", Firstname = "y"}; //Déclaration d'un objet anonyme
var pa = new Person ();
pa.MapWithReflection (a);
Console.WriteLine (pa); //Affiche Name : x, Firstname : y
var pb = new Person ();
pb.MapWithoutReflection (a);
Console.WriteLine (pb); //Affiche Name : x, Firstname : y
}
Les Factory avec l’instanciation d’objets
Chargement d’un plugin ou d’un module externe
Les génériques et l’instanciation de classe
C#
public class Process
{
public Process ()
{
}
public void Run<T>() where T : IRunnable
{
//Instanciation de la classe de type T implémentant l'interface IRunnable (contrainte where) par réflexion
T device = Activator.CreateInstance<T>();
device.Run ();
}
}
public interface IRunnable
{
void Run();
}
C#
class MyRunnable : IRunnable
{
#region IRunnable implementation
public void Run ()
{
Console.WriteLine ("My runnable is running of course");
}
#endregion
}
public static void Main (string[] args)
{
Process process = new Process ();
process.Run<MyRunnable> ();
}
La lecture d’un fichier template ou de configuration
Le routage générique pour une Api Rest avec l’invocation de fonction
$app->post('/invoke/:className/:methodName(/:params+/?)', function($className, $methodName, $params = NULL) use($app, $em)
{
//Récupére la méthode de nom $methodName dans la classe de nom $className
$reflectionMethod = new ReflectionMethod($className, $methodName);
//Invocation de la méthode récupéré avec les arguments $params
$responseBody = $reflectionMethod->invokeArgs($repository, $params);
//Retourne le message encodé en JSON (pour l'API Rest)
return json_encode($responseBody);
});
$app->post('/invoke/Person/findAll', function() use($app, $em)
{
Person::findAll();
});
$app->post('/invoke/Person/findById/:id', function($id) use($app, $em)
{
Person::findById($id);
});
$app->post('/invoke/Cat/findAll', function() use($app, $em)
{
Cat::findAll();
});
...
$app->post('/invoke/:className/:methodName(/:params+/?)', function($className, $methodName, $params = NULL) use($app, $em)
{
if ($className === "Person")
{
if ($methodName === "findAll")
{
Person::findAll();
}
else if (methodName === "findById")
{
Person::findById($params)
}...
}
else if ($className === "Cat")
...
});
Un système orienté aspect avec l’injection de code à la volée (cas extrême)
Compilation / Création de code à la volée (cas super extrême)