Le « Monkey Patching », ou l’AOP du pauvre

Le monkey-patch, c’est le fait de pouvoir écraser des attributs, méthodes, etc… au moment de l’exécution. On s’en sert en général à remplacer ou étendre une méthode.

En JavaScript, le monkey-patching est extrêmement simple, puisqu’on peut nativement remplacer n’importe quelle méthode d’un objet par une fonction dynamiquement.

Remplacer une méthode

C’est trivial :

object.method = function () {
  // this === object
  // …
};

Modifier le comportement d’une méthode

Supposons que j’ai la classe « Entity » possédant une méthode « save()« . On a un problème, cette méthode a tendance à enregistrer des données complètement corrompues mais on n’arrive pas à savoir dans quelles conditions ça arrive. Le plus simple serait de pouvoir se « brancher » sur cette méthode pour logger certaines informations afin d’en savoir plus.

Le monkey-patching est une façon simple d’y arriver :

// wrap to avoid global context pollution
(function () {
  // store original method
  var _save = Entity.prototype.save;
  // override method
  Entity.prototype.save = function () {
    log_stack_trace(new Error().stack); // grab a stack trace to know where I've been called from
    log_entity(this); // log entity to check data
    // call original method
    _save.apply(this, arguments);
  };
})();

Quelques petits choses utiles à savoir quand on monkey-patch :

  • Function#apply pour appeler la méthode d’origine.
  • arguments représente la liste des arguments passés à notre fonction, pas besoin de jongler avec le prototype de la fonction d’origine pour avoir les données qu’il faut.
  • Penser à imbriquer le tout dans une fonction anonyme pour éviter de dépendre du contexte global. Dans notre exemple si on ne l’avait pas fait, il aurait suffit que « _save » soit surchargée ailleurs pour tout casser :)
  • On peut affecter Classe.prototype.methode pour impacter toutes les instances d’un coup (même celles déjà instanciées), ou objet.methode pour n’impacter qu’un objet spécifique.

Note sur arguments

Un petit détail peu connu, que j’ai d’ailleurs découvert récemment sur Twitter (mais je n’arrive pas à remettre la main dessus) : quand on modifie un argument de la méthode, ça impacte aussi arguments.

Exemple :

trick(42);
function trick (x) {
  console.log(x); // 42
  console.log(arguments); // {'0': 42}
  x = 33;
  console.log(arguments); // {'0': 33}
}

C’est important à savoir, car quand on appelle methode_originale.apply(this, arguments) il faut être sûr de ce qu’on fait.

Quid de l’héritage de méthode

Lorsqu’on fait de l’héritage (même si l’héritage et JavaScript… enfin bon ce n’est pas le sujet) et qu’on souhaite surcharger une méthode, on va justement typiquement faire du monkey-patching.

On aurait pu traiter notre exemple précédent par héritage :

// Standard inheritance
function LoggedEntity () {
  Entity.apply(this, arguments);
}
LoggedEntity.prototype = new Entity;
 
// Override "save"
LoggedEntity.prototype.save = function () {
  // log// call parent method
  Entity.prototype.save.apply(this, arguments);
}

Le rapport avec l’AOP ?

Disclaimer : Je vais me faire assassiner par les puristes, donc je confirme d’avance que c’est une simplification des concepts pour illustrer le propos, pas un cours théorique, trèèèèès loin de là ! n’hésitez pas à corriger dans les commentaires si quelque chose vous semble vraiment absurde.

La Programmation Orientée Aspect consiste grosso-modo à étendre des méthodes plutôt que de fonctionner par héritage. Évidemment c’est un énorme raccourci, et l’AOP (POA en français donc) est accompagné de son vocabulaire specifique bien chiant, mais on peut à peu près résumer :

  • Le « greffon » (« advice ») est le bout de code qu’on veut insérer dans la méthode originale
  • Le « point de coupe » (« pointcut ») est la méthode qu’on surcharge
  • Le « point de jonction » (« join point ») est l’endroit où on va injecter notre code :

À la place :

object.method = function () {
  // new code
}

Avant :

var _method = object.method;
object.method = function () {
  // insert advice here
  // can modify "arguments"
 
  return _method.apply(this, arguments);
}

Après :

var _method = object.method;
object.method = function () {
  var result = _method.apply(this, arguments);
 
  // insert advice here
  // can modify result
  return result;
}

Autour de :

var _method = object.method;
object.method = function () {
  // insert first part of advice
  // can modify "arguments"
 
  var result = _method.apply(this, arguments);
 
  // insert last part of advice here
  // can modify result
  return result;
}

Conclusion

Dans une application Node, on peut ainsi étendre ou corriger un module tiers sans avoir à le copier-coller, et ainsi continuer à bénéficier des mises à jour. Je conseille d’isoler les patchs dans des fichiers à part dans l’application de manière à pouvoir les identifier simplement.

En bref, un concept simple et très utile (débugger un module sans éditer son code source est vraiment utile), mais à utiliser avec modération pour continuer à s’y retrouver :)

8 réflexions au sujet de « Le « Monkey Patching », ou l’AOP du pauvre »

    1. naholyr Auteur de l’article

      Thanks pour la référence !

      Par contre j’ai toujours un peu de mal avec le nom « wtfjs » quand il s’agit justement d’un « documented behaviour », m’enfin ^^

      Répondre
    1. naholyr Auteur de l’article

      Ce n’est pas vraiment un « problème », mais c’est surtout que c’est un concept qui n’existe pas vraiment en tant que tel 😉 on l’implémente (justement à coup de monkey-patch) mais ça ne fait pas vraiment partie de l’esprit du langage.

      Répondre
    1. naholyr Auteur de l’article

      Le monkey-patching ? Oh oui je l’ai utilisé moults fois 😉 de là parler d’AOP c’est pompeux et pas très à propos dans mon cas, mais j’ai rencontré quelques situations où le monkey-patching m’a été très utile :

      • Logger des entrées dans les méthodes d’un objet (avec une Error pour la stack trace) m’a servi plusieurs fois quand je n’y comprenais plus rien 😛
      • Corriger un bug dans un module sans le copier-coller (ça m’est arrivé deux fois)
      Répondre

Laisser un commentaire