Dinamically Proxying objects and Wrapping functions.

It’s been some time ago since I needed some mechanism to make debugging in CAAT and other javascript projects I’m working on easier. I’ve an strong Java background and I’m used to dynamically proxying most of my core objects, mainly for safety reasons but as well for the ease of development and to do some aspect-oriented programming.
So I’ve come up with a javascript proxy solution which will allow me upon a call to evaluate whatever object’s method , hook that call at least in three stages:

  • Pre method call, which allows to wrap, change or modify method call parameters.
  • Post method call, which allows to modify method call’s return value.
  • On exception throw call, which allows to return values on exception preventing exception toss.
  • Of course, these are just some examples of why I would be willing to use proxies. But one more compelling reason is that of log code activity without sprouting alert/console.log sentences all around my beautifully crafted code.

    While in Java we’ve incredibly powerful introspection mechanisms as well as an InvocationHanlder ready to be used, the javascript implementation will be a little bit less ambitious, I mean, the code security concerns of Java’s dynamic proxy will be left out the implementation.
    The use of this proxy to wrap a whole object be as follows:

    // Define a module/namespace whatever you call it.
    Meetup= Meetup || {};
    
    // Define an 'object class'
    (function() {
     Meetup.C1= function(p) {
       this.a= p;
       return this;
     }
    
     Meetup.C1.prototype= {
       a: 5,
       b: function() {
         return this.a*this.a;
       },
       c: function(p1,p2) {
          return this.a+p1/p2;
       },
       err: function() {
           throw 'mal';
       }
     };
     })();
    
    var c0= new Meetup.C1(10);
    
    // Instantiate and wrap the object into a proxy:
    // This cp0 object will behave exactly as c0 object does.
    var cp0= proxy(
            c0,
            function(ctx) {
                console.log('pre method on object: ',
                        ctx.object.toString(),
                        ctx.method,
                        ctx.arguments );
            },
            function(ctx) {
                console.log('post method on object: ',
                        ctx.object.toString(),
                        ctx.method,
                        ctx.arguments );
    
            },
            function(ctx) {
                console.log('exception on object: ',
                        ctx.object.toString(),
                        ctx.method,
                        ctx.arguments,
                        ctx.exception);
    
                return -1;
            });
    
            

    With this code, by calling cp0.b(); the following method calls will be performed:

  • Call anonymous function pre-method.
  • Call proxied object’s ‘b’ method
  • If the proxied object’s method call went rightm call anonymous function post-method, otherwise (on execption toss) call anonymous function on-method-exception.
  • This, with some little extra work, could be considered aspect oriented function calls. These hook functions receive as their parameters one object of the form:


    {
      object: the-proxied-object,
      method: evaluated-proxied-object-method-name,
      arguments: the-evaluated-method-arguments,
      exception: if-exception-thrown-on-error-hook--the-exception-thrown
    }

    It is up to the developer what to do with these information for each hook function, but in the example, they’ve been set up as activity logging functions. An example of the result of the execution of cp0.c(1,2) will be:


    pre method on object: Meetup.C1 {a: 10} c [ 1, 2 ]
    post method on object: Meetup.C1 {a: 10} c [ 1, 2 ]
    10.5

    (which should be read as pre-method call on object XX, method ‘b’, with method arguments ‘[]’ (none in this case).)

    When proxying a simple function, the code downgrades to a function wrapping. In this case I’m keeping the same call scheeme. An example will be as follows:

    function ninja(){
      console.log("ninja running" );
    };
    
    var pninja= proxy(
            ninja,
            function(context) {
                console.log('pre method on function: ',
                        context.fn,
                        context.arguments );
            },
            function(context) {
                console.log('post method on function: ',
                        context.fn,
                        context.arguments );
            },
            function(context) {
                console.log('exception on function: ',
                        context.fn,
                        context.arguments );
                return -1;
            });
            

    As you can see, when wrapping functions, the context supplied to hook functions is of the form:

    {
      fn: the-wrapped-function,
      arguments: the-wrapperd-function-arguments
    }

    The proxy function itself is the following:

    function proxy(object, preMethod, postMethod, errorMethod) {
    
        // proxy a function
        if ( typeof object=='function' ) {
    
            if ( object.__isProxy ) {
                return object;
            }
    
            return (function(fn) {
                var proxyfn= function() {
                    if ( preMethod ) {
                        preMethod({
                                fn: fn,
                                arguments:  Array.prototype.slice.call(arguments)} );
                    }
                    var retValue= null;
                    try {
                        // apply original function call with itself as context
                        retValue= fn.apply(fn, Array.prototype.slice.call(arguments));
                        // everything went right on function call, then call
                        // post-method hook if present
                        if ( postMethod ) {
                            postMethod({
                                    fn: fn,
                                    arguments:  Array.prototype.slice.call(arguments)} );
                        }
                    } catch(e) {
                        // an exeception was thrown, call exception-method hook if
                        // present and return its result as execution result.
                        if( errorMethod ) {
                            retValue= errorMethod({
                                fn: fn,
                                arguments:  Array.prototype.slice.call(arguments),
                                exception:  e} );
                        } else {
                            // since there's no error hook, just throw the exception
                            throw e;
                        }
                    }
    
                    // return original returned value to the caller.
                    return retValue;
                }
                proxyfn.__isProxy= true;
                return proxyfn;
    
            })(object);
        }
    
        /**
         * If not a function then only non privitive objects can be proxied.
         * If it is a previously created proxy, return the proxy itself.
         */
        if ( !typeof object=="object" ||
                object.constructor==Array ||
                object.constructor==String ||
                object.__isProxy ) {
    
            return object;
        }
    
        // Our proxy object class.
        var cproxy= function() {};
        // A new proxy instance.
        var proxy= new cproxy();
        // hold the proxied object as member. Needed to assign proper
        // context on proxy method call.
        proxy.__object= object;
        proxy.__isProxy= true;
    
        // For every element in the object to be proxied
        for( var method in object ) {
            // only function members
            if ( typeof object[method]=="function" ) {
                // add to the proxy object a method of equal signature to the
                // method present at the object to be proxied.
                // cache references of object, function and function name.
                proxy[method]= (function(proxy,fn,method) {
                    return function() {
                        // call pre-method hook if present.
                        if ( preMethod ) {
                            preMethod({
                                    object:     proxy.__object,
                                    method:     method,
                                    arguments:  Array.prototype.slice.call(arguments)} );
                        }
                        var retValue= null;
                        try {
                            // apply original object call with proxied object as
                            // function context.
                            retValue= fn.apply( proxy.__object, arguments );
                            // everything went right on function call, the call
                            // post-method hook if present
                            if ( postMethod ) {
                                postMethod({
                                        object:     proxy.__object,
                                        method:     method,
                                        arguments:  Array.prototype.slice.call(arguments)} );
                            }
                        } catch(e) {
                            // an exeception was thrown, call exception-method hook if
                            // present and return its result as execution result.
                            if( errorMethod ) {
                                retValue= errorMethod({
                                    object:     proxy.__object,
                                    method:     method,
                                    arguments:  Array.prototype.slice.call(arguments),
                                    exception:  e} );
                            } else {
                                // since there's no error hook, just throw the exception
                                throw e;
                            }
                        }
    
                        // return original returned value to the caller.
                        return retValue;
                    }
                })(proxy,object[method],method);
            }
        }
    
        // return our newly created and populated of functions proxy object.
        return proxy;
    }
            

    Despite being a powerful beast, the proxy model has some drawbacks. In example, attributes (object’s non function elements) can’t be proxied, since they can’t be evaluated by function call (Will have to take a look at __define_getter and setters functions tough). Also, newly dynamically added methods to an object won’t be proxied by an already defined proxy object, but you always have the opportunity of creating a new proxy for that object.
    Also, not every object type can be proxied. Concretely, String and Array primitive object types can’t, so the proxy function is checking at first instance whether the supplied object is elligible to be proxied. If not, the unchanged object will be returned.

    One more thing to tell about the proxy function is its own definition. It is defined as a global function because it makes no sense adding it to Object’s prototype since as I said, not every object type is elligible to be proxied.

    This technique has shown very useful to me. While developing, I’m just passing object’s proxies back and forth throught the code which seamlessly behave as regular objects. Given that one single object can have more that one proxy, I’ve the abbility to hook different extra behaviors to the original object by passing different proxies for different situations. In the end, when I’m finished with the development/debug phase, I simply disable the proxy functionality by supplying with a proxy function which just returns the passed object. Maybe this technique is only suitable for very object centric developments, but I’ve found it very valuable.

    Maybe I’m wrong, let me know what you think.

    Advertisements

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out /  Change )

    Google+ photo

    You are commenting using your Google+ account. Log Out /  Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out /  Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out /  Change )

    Connecting to %s