Comprendre la réflexion, l’introspection et l’intercession

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 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.

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

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.

Que faire avec ?

Les systèmes de mapping avec le parcours des propriétés

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.

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"

 this[element] permet d’obtenir une propriété de l’objet sous la forme this[« proprerty »] à la place de this.property.

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

}

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é.

Les Factory avec l’instanciation d’objets

L’instantiation d’objet dans un factory permet de montrer rapidement un cas de suppression de condition.

Chargement d’un plugin ou d’un module externe

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#.

Les génériques et l’instanciation de classe

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).

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();
}

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 :

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> ();

}

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.

La lecture d’un fichier template ou de configuration

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.

 

Le routage générique pour une Api Rest avec l’invocation de fonction

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) :

$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);
    
});

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 :

$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();
});

...

Et elle permet d’éviter un unique routage avec une suite de condition extensible comme ci-dessous :

$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)

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.

Compilation / Création de code à la volée (cas super extrême)

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.


Laisser un commentaire