Métaprogrammation : Amusons-nous avec l’objet « Proxy »

Métaprogrammation : Amusons nous avec l'objet "Proxy"

L’objet Proxy est souvent méconnu de la plupart des développeurs JavaScript. Celui-ci permet pourtant de résoudre facilement certains problèmes et de faire ce que l’on appelle de la métaprogrammation.

Qu’est-ce que la métaprogrammation ?

Avant de présenter l’objet Proxy et son utilité, voyons qu’est-ce que la métaprogrammation, comme d’habitude demandons à Wikipédia :

La métaprogrammation, nommée par analogie avec les métadonnées et les métaclasses, désigne l’écriture de programmes qui manipulent des données décrivant elles-mêmes des programmes. Dans le cas particulier où le programme manipule ses propres instructions pendant son exécution, on parle de programme automodifiant.

Super, mais on n’a rien compris. Bon, je vais tenter de vous expliquer. Tout d’abord, savez-vous ce que signifie le préfixe « méta » ? Celui-ci exprime l’idée d’autoréférence, ainsi une métadonnée est une donnée décrivant une autre donnée, un métalangage est un langage décrivant un autre langage, etc.

C’est ce que nous dit la définition, la métaprogrammation est en gros un programme qui manipule un autre programme. Un exemple très simple est le cas d’un compilateur ou d’un interpréteur qui est un programme qui va manipuler votre code ou c’est encore le cas des debuggers.

Mais cela peut-être également, et c’est ce qui nous intéresse ici, la capacité à votre code d’agir sur lui-même et de modifier certains comportements prévus par le langage.

L’objet Proxy

Vous l’aurez deviné, l’objet Proxy va nous permettre de faire de la métaprogrammation en JavaScript. 

Comment ça marche ?

L’objet Proxy permet d’encapsuler un autre objet et intercepter certaines opérations comme l’accès à une propriété, l’affectation d’une valeur à une propriété, l’appel d’une fonction, etc.

Pour créer un objet Proxy rien de plus simple :

Avec :

  • target : Il s’agit de l’objet à envelopper. Cela peut être n’importe quel objet : une fonction, un tableau ou même un autre proxy;
  • handler : C’est l’objet qui définit la configuration du proxy. C’est ici que l’on mettra nos fonctions qui intercepteront les opérations sur notre objet comme nous le verrons juste après.

Prenons un exemple tout simple :

On remarque que l’ajout d’une propriété sur l’objet proxy est répercutée sur l’objet qu’il encapsule.

Bon ok c’est bien joli tout ça, mais ça ne sert pas à grand-chose pour le moment. On va donc voir comment configurer notre proxy avec le paramètre handler pour intercepter les opérations sur notre objet.

It’s a trap !

Le paramètre handler permet de définir ce que la norme appelle des trappes (ou traps en anglais) pour intercepter les opérations sur l’objet qu’encapsule le proxy. Ces trappes sont au nombre de treize et se déclenchent lors de certaines actions :

Bon, j’espère que vous êtes toujours là ! N’ayez pas peur on va surtout s’intéresser aux trappes get et set et croyez-moi il y a de quoi faire.

la trappe get

La trappe get est appelée lorsque l’on souhaite accéder à une propriété d’un objet. La signature de la fonction est la suivante :

Avec :

  • target: L’objet que le proxy enveloppe;
  • property : Le nom de la propriété à laquelle on souhaite accéder;
  • receiver : Le proxy ou un objet qui en hérite.

Cette méthode peut renvoyer n’importe quelle valeur.

On va commencer par un exemple tout simple pour mieux comprendre le fonctionnement :

Dès que l’on accède à la propriété value de notre objet proxyObj, la trappe get se déclenche, et nous retournons la valeur de la propriété value via l’instruction return target[property].

Accès à la propriété d'un objet via un proxy
Accès à la propriété d’un objet via un proxy

Avant de voir des utilisations concrètes de la trappe get, voyons à quoi sert le paramètre receiver.

Reprenons notre exemple et ajoutons un getter val pour récupérer la propriété value:

Créons ensuite un second objet qui « hérite » de notre proxy via la chaîne de prototype. Je vous invite à aller lire l’article sur l’héritage multiple qui fait un rappel sur l’héritage en JavaScript.

Récupérons la propriété value via le getter val :

Et là ça coince, on récupère la valeur 1 au lieu de 2.

Le souci se situe dans notre trappe get, lors du renvoi de la valeur target[property]. Le paramètre property est le nom de notre getter, de ce fait l’instruction target[property] exécute le code de celui-ci. Vu que le paramètre target est notre objet obj cela retourne donc la valeur 1 et non 2, car la valeur this au sein du getter est bien obj et non inheritsObj. Je vous invite à aller lire mon article sur this si vous avez du mal à comprendre.

Comment passer la bonne valeur de this au getter val ? Et bien c’est là que rentre en jeu le paramètre reveiver. En effet, nous avons vu que ce paramètre était soit le proxy soit un objet qui en hérite, c’est bien notre cas ici, inheritsObj hérite du proxy proxyObj .

Malheureusement, nous ne pouvons pas directement récupérer la propriété de cette façon reveiver[property], car cela rappellerait la trappe get et créerait une « boucle infinie » d’appels.

Pour remédier à ce problème, JavaScript propose un objet Reflect qui fournit des méthodes permettant d’interagir avec les objets et faire appel aux fonctions internes du langage. Pour chaque trappe de l’objet Proxy, il existe une méthode correspondante pour l’objet Reflect

Prenons celle qui nous intéresse :

Cette méthode permet de récupérer la valeur de la propriété d’un objet (c’est en fait un accesseur de propriété, comme . ou [] mais sous la forme d’une fonction).

Les paramètres de cette méthode sont les suivants :

  • target : L’objet sur lequel on souhaite récupérer la propriété;
  • property : Le nom de la propriété;
  • receiver : La valeur de this qui sera passée dans le cas de l’utilisation d’un getter;

Corrigeons notre exemple :

Et voilà ! On obtient bien le résultat attendu.

Bon maintenant qu’on a toutes les informations qu’il nous fallait pour comprendre comment fonctionne l’objet Proxy on va passer aux choses sérieuses et voir des cas d’utilisations.

Cas d’utilisation

On va voir plusieurs cas d’utilisation de la trappe get, avec des exemples variés afin de voir les possibilités qui s’offrent à nous.

Renvoyer une erreur si une propriété n’existe pas

Lorsque l’on souhaite accéder à une propriété inexistante d’un objet, la valeur undefined est renvoyée :

Il est possible via l’utilisation d’un proxy de renvoyer une erreur si la propriété n’existe pas :

Calculer le temps d’exécution des méthodes

On a souvent besoin de calculer le temps d’exécution de nos méthodes. L’utilisation d’un proxy permet de réaliser cette opération de façon très simple et élégante :

Espionner nos méthodes

Lorsque l’on fait des tests unitaires, on a souvent besoin de récupérer des informations sur l’exécution des méthodes de nos objets, que ce soit le nombre de fois où celles-ci ont été appelées ou les arguments passés en paramètres. On utilise pour cela ce que l’on appelle un spy. C’est d’ailleurs ce que propose la librairie sinon.

Voyons voir comment, à l’aide d’un proxy, réaliser un spy qui va compter le nombre de fois où nos méthodes ont été appelées :

Encore une fois, quand on a compris le concept de proxy, c’est relativement simple. L’avantage de l’utilisation d’un proxy est que l’on a aucune modification à faire sur notre objet.

Un conteneur d’injection de dépendances

Pour ce dernier exemple de la trappe get, on va voir comment à l’aide d’un proxy, créer simplement un conteneur d’injection de dépendances. J’ai fait un article sur l’injection de dépendances, je vous conseille d’aller y jeter un coup d’œil.

Prenons un exemple de trois services  :

On doit à chaque fois gérer les dépendances de chaque service. Voyons maintenant comment gérer automatiquement ces dépendances à l’aide d’un conteneur de dépendances qui utilise un proxy.

Voici la classe du conteneur :

Bon, il y a besoin d’une petite explication, tout se passe dans la méthode paramParser celle-ci utilise un proxy qui permet de récupérer le nom des paramètres d’une fonction lorsque le destructuring de paramètre est utilisé. Pour chaque paramètre, elle vérifie, via la méthode resolve, si un service portant ce nom existe, si c’est le cas elle le renvoie. Cela permet de créer de manière dynamique l’objet qui sera passé au constructeur de chaque service et ainsi faire de l’injection automatique.

Bien entendu pour que cela fonctionne vous devez impérativement utiliser le destructuring dans vos constructeurs :

Vous pouvez vous amuser à améliorer cet exemple si vous le souhaitez, voici quelques pistes :

  • Gérer l’enregistrement de fonction et de valeur (primitive ou objet), car dans l’exemple on ne gère que des classes;
  • Ajouter une option lors de l’enregistrement d’un service pour savoir si celui-ci doit être un singleton ou non (pour le moment on ne gère que des singletons)
  • Supprimer un service;
  • etc.

la trappe set

La trappe set est appelée lorsque l’on définit ou modifie une propriété d’un objet. La signature de la fonction est la suivante :

Avec :

  • target: L’objet que le proxy enveloppe;
  • property : Le nom de la propriété que l’on souhaite définir ou modifier;
  • value : La valeur que l’on souhaite affecter à la propriété;
  • receiver : Le proxy ou un objet qui en hérite.

Cette méthode doit renvoyer une valeur booléenne, true si l’affectation a réussi, false dans le cas contraire.

Cas d’utilisation

On va tout de suite passer à des cas d’utilisation de cette trappe pour mieux voir ce qu’il est possible de faire avec.

Créer un tableau d’entier

On commence par un exemple tout simple. On souhaite disposer d’un tableau qui ne contient que des entiers. Pour cela, il est possible de créer une nouvelle classe qui hérite de Array :

Malheureusement il est possible d’affecter n’importe quelle valeur via l’accesseur de propriété [].

Voyons comment régler ce problème facilement à l’aide d’un proxy :

On peut également reprendre notre classe IntegerArray et utiliser le proxy comme ceci :

Généralement, les constructeurs n’ont pas d’instruction return. Ils sont chargés d’ajouter les différentes propriétés dans this qui représente notre objet en construction. this est ensuite renvoyé de manière implicite, mais il est possible de renvoyer un autre objet à la place comme ici où nous renvoyons notre proxy.

Valider des propriétés d’un objet

Voyons un exemple un peu plus compliqué que le précédent. Nous souhaitons valider les propriétés d’un objet lors d’une affectation ou d’une modification.

Prenons une classe User :

Aucune vérification n’est faite, il est possible d’affecter n’importe quelle valeur.

Voyons comment régler ce souci à l’aide d’un proxy et des expressions régulières, car je sais que vous adorez ça :

Vous pouvez également utiliser la librairie Joi pour définir des schémas de données plus complexes.

Observer des changements sur les propriétés d’un objet

On est parfois amené à devoir exécuter certaines opérations lorsqu’une propriété d’un objet est modifiée. L’utilisation d’un proxy permet très facilement d’observer les changements de valeur des propriétés d’un objet et d’exécuter des fonctions lorsque cela se produit :

Proxy révocable

Il est également possible de créer un objet Proxy révocable :

Cette fonction retourne le proxy créé ainsi qu’une fonction revoke qui permet de désactiver celui-ci.

Lorsque la méthode revoke est appelée, toutes les trappes lèveront une exception TypeError :

On peut par exemple donner accès à un objet de manière temporaire :

Pour finir…

Nous avons vu comment utiliser l’objet Proxy en JavaScript avec seulement l’utilisation de deux trappes à savoir get et set dans des exemples relativement simples, j’aurais pu vous montrer encore pleins d’autres exemples comme un système de cache, rendre des propriétés privées (en attendant que ce soit pris en charge par le langage : https://github.com/tc39/proposal-class-fields) ou encore garder un historique des modifications sur un objet. Le nombre de trappes disponibles est au nombre de treize je vous laisse donc imaginer toutes les possibilités. Je ferai sûrement un second article sur l’utilisation des autres trappes si cela vous intéresse. N’hésitez pas à partager vos utilisations de l’objet Proxy en commentaire !


Annonces partenaire

Je suis lead developer dans une boîte spécialisée dans l'univers du streaming/gaming, et en parallèle, je m'éclate en tant que freelance. Passionné par l'écosystème JavaScript, je suis un inconditionnel de Node.js depuis 2011. J'adore échanger sur les nouvelles tendances et partager mon expérience avec les autres développeurs. Si vous avez envie de papoter, n'hésitez pas à me retrouver sur Twitter, m'envoyer un petit email ou même laisser un commentaire.

3 commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.