Wednesday 20 February 2019

Modifiable Proxy

There's something in ES6 proxies that I see as a downside when used for intercepting method calls if we compare them to mechanisms like the beautiful invokeMethod provided by Groovy or directly monkeypatching the object replacing its funtions by new ones that run the additional code and then call the original (we can do this in JavaScript or Python). The downside is when you already have an object referenced from different places. Creating a proxy around it won't magically point those references to the new object, so basically you have to decide that you are going to use a proxy to that object before passing it to any consumer.

Most times this is not a big deal. We work with shortlived objects and we won't be deciding over their lifetime if we want to proxy them or not. Well, this is not so true. For example the DI mechanism in Angular is by default injecting as services a single instance, so they will be alive for way too long. Thinking a bit about it I've come up with an alternative. If there are chances that we could want to proxy the object in the future, why not proxying it now with a "do nothing" proxy, and modify this proxy in the future to "do something".

Exploring this option I read somewhere about modifying the handler object associated to the proxy. The idea seemed a bit odd to me cause I was expecting that the Proxy constructor would use the handler object in a similar way as an options object. I thought the different traps in the handler object would get assigned to some internal structures in the proxy, and basically, the handler itself would be of no more use, but I was pretty wrong. If you keep one reference to the handler provided to your proxy and replace one of the traps in it, the proxy will use the new trap. Let's see an example.


class Person{
 constructor(name){
  this.name = name;
 }
 
 sayHi(){
  return "Hello I'm " + this.name;
 } 
}
 
let p1 = new Person("Francois");
console.log(p1.sayHi());

let loggingHandler = {
  get: function(target, property, receiver) {
 let it = target[property];
 if (typeof it != "function"){
  return it;
 }
 else{
  return function(){
   console.log(property + " started");
   return it.call(this, ...arguments);
  }
 }
  }
  
  // resetGetTrap: function(){
   // this.get = function(target, property, receiver) {
  // return target[property];
   // }
  // }
};

console.log("----------------------");
console.log("Modifying handler test");
let proxiedP1 = new Proxy(p1, loggingHandler);
console.log("- Operations with proxiedP1");
console.log(proxiedP1.sayHi());

//let's change the "get" trap

loggingHandler.get = function(target, property, receiver) {
 let it = target[property];
 if (typeof it != "function"){
  return it;
 }
 else{
  return function(){
   console.log(property + " STARTED!!!");
   return it.call(this, ...arguments);
  }
 }
  }
  

console.log("after modifying handler");
console.log(proxiedP1.sayHi());

// - Operations with proxiedP1
// sayHi started
// Hello I'm Francois
// after modifying handler
// sayHi STARTED!!!
// Hello I'm Francois

As you can see in the output, replacing the trap in the handler affects the existing proxy, pretty nice... The thing is that having to store a reference to the handler is a bit of a pain, the nice way would be that the proxy itself provided a mechanism to change the trap. Well, that turns to be rather simple. I've written a factory function (modifiableProxyFactory) that creates a proxy with a get trap that when intercepting a "setGetTrap" access will interpret it as a command to change the existing get trap logic. For that it captures the real trap code in a closure. Let's see:

function modifiableProxyFactory(target){
 //initialize the proxy with a "transparent" trap
 let coreGetTrapFunc = (target, property, receiver) => target[property];
 let handler = {
  //the "get trap" checks if we want to set the trap, otherwise it invokes the existing trap
  get: function(target, property, receiver){
   if (property === "setGetTrap"){
    console.log("setting a new get trap!");
    return function(getTrap){
     coreGetTrapFunc = getTrap;
    };
   }
   else{
    return coreGetTrapFunc(target, property, receiver);
   }
  }
 };
 return new Proxy(target, handler);
}

let p2 = new Person("Emmanuel");
console.log(p2.sayHi());

let proxiedP2 = modifiableProxyFactory(p2);
console.log("\n- After creating 'empty' proxiedP2");
console.log(proxiedP2.sayHi() + "\n");


//------------------------------------
proxiedP2.setGetTrap(function(target, property, receiver){
 let it = target[property];
 if (typeof it != "function"){
  return it;
 }
 else{
  return function(){
   console.log(property + " called");
   return it.call(this, ...arguments);
  }
 }
});

console.log("\n- After reassigning get trap");
console.log(proxiedP2.sayHi() + "\n");

//-- output:

// Hello I'm Emmanuel

// - After creating 'empty' proxiedP2
// Hello I'm Emmanuel

// setting a new get trap!

// - After reassigning get trap
// sayHi called
// Hello I'm Emmanuel


I've uploaded it as a gist here

No comments:

Post a Comment