Eugene's Blog

I can't believe it's blog!

AOP aspect of JavaScript with Dojo

Finally: my open source JavaScript project DCL is built on ideas described in this article. Available for node.js and modern browsers, it implements OOP with mixins and AOP at "class" and object level. Read documentation and background articles on www.dcljs.org, clone its code on github.com/uhop/dcl, and follow @dcl_js.

If we look at the history of computer programming languages, we can see that practically all new programming methodologies were about one thing: taming complexity. The anarchy of earlier days of procedural programming (example: Fortran) gave way to structured programming (Pascal), which was refined with modular programming (Modula), and was reformulated when object-oriented programing went mainstream (C++, and much later Java). And it stopped there. The focus shifted to different branches of computer programming, namely to functional programming, and, to a lesser degree, logical programming. The only major development in this branch was the rise of aspect-oriented programming (AOP) paradigm. Let’s take a look at AOP in our favorite language: JavaScript, and how Dojo helps the language with dojox.lang.aspect package.

Let’s start with basics taking AspectJ as our model AOP language (after all it was designed by the author of AOP). AOP tries to separate and encapsulate concerns, which are common between different modules — so called crosscutting concerns. The goal is simple — it helps to increase the modularity, which is always a Good Thing™.

Typically AOP needs following things to be present in the language:

  • joinpoint — a point in the control flow.
    • Examples:
      • calling a method,
      • executing a method’s body or an exception handler,
      • referencing an object’s attribute, and so on.
  • pointcut — a query that is used to define a set of affected joinpoints. Essentially this is a logical expression that can pick out joinpoints and make sure that their context is right.
    • Examples: it can verify that the affected object is of right type, that we are in particular control flow branch, and so on.
  • advice — an additional behavior (a code) that will be applied at joinpoints.
    • Available advice types:
      • “before” — runs before a joinpoint,
      • “after” — runs after a joinpoint was executed,
      • “after returning” — runs only after a normal execution of a joinpoint,
      • “after throwing” — runs only if during execution of a joinpoint an unhandled exception was thrown.
      • “around” — runs instead of a joinpoint, may call the original joinpoint.
  • aspect — an entity that encapsulates related pointcuts, and advices together, and can add some attributes to advised classes.
  • weaver — a program that “weaves” aspects with original programs.

What kind of useful functionality can we implement with aspects? This list includes: constraint enforcement, security checks, transaction wrappers, resource management, various logging, profiling, debugging in general, and much much more.

What can we implement and leverage in JavaScript?

Let’s take a look at the weaver first. JavaScript is a dynamic language, so we can weave aspects at the runtime. It would be a major hassle to do it statically with a preprocessor of sort, especially if we take into consideration the fact that a dynamic program can mutate over time. So it is settled: our weaver will be dynamic as well. One major implication of this decision: we cannot go over all objects of our program. It is not possible, and, in any case, not practical. We should apply aspects locally rather than globally.

What kind of joinpoints are available? Again, we don’t have too much choice here: the only joinpoint available across all implementations of JavaScript is a method call. Given into account the prototypal nature of JavaScript’s object system, we can advise “class” methods, and “instance” methods alike.

It means that pointcuts mutate into a runtime check and in many cases this check is not needed:

  • Because we apply aspects locally, in some cases we don’t need to check dynamically if we operate on right objects of right type. And we don’t need to check if we intercept the right method.
  • If we need to do that, we’ll check it dynamically when our advice is executed.
  • We cannot check the control flow, unless it is simulated somehow. In any case this check should be done dynamically.

We can implement all types of advices and package them as aspects.

Now is a good time to go over implementation details of various advices. Without further ado this is how we can implement the “before” advice:

1
2
3
4
5
var old = object[method];
object[method] = function(){
    before.apply(this, arguments);
    return old.apply(this, arguments);
};

As you can see the before() function is called in the context of the object with all parameters passed to the original method. The “old” method is called after.

The possible implementation of the “after returning” advice:

1
2
3
4
5
6
var old = object[method];
object[method] = function(){
    var ret = old.apply(this, arguments);
    afterReturning.call(this, ret);
    return ret;
};

The afterReturning() method is concerned with the return value. It is not going to be called if the original method throws an exception.

The possible implementation of the “after throwing” advice:

1
2
3
4
5
6
7
8
9
var old = object[method];
object[method] = function(){
    try{
        return old.apply(this, arguments);
    }catch(excp){
        afterThrowing.call(this, excp);
        throw excp;
    }
};

The afterThrowing() handles possible exceptions. It is not called if the original method returned normally. After processing the exception is re-thrown again.

The possible implementation of the “after” advice:

1
2
3
4
5
6
7
8
var old = object[method];
object[method] = function(){
    try{
        return old.apply(this, arguments);
    }finally{
        after.call(this);
    }
};

It doesn’t accept any argument because it is called after the normal return, and when the exception is thrown. If you need those values, use afterReturning() and afterThrowing() respectively.

The “around” advice is very simple conceptually. The naïve implementation goes like this:

1
2
3
4
5
6
7
8
9
10
11
12
var old = object[method];
object[method] = function(){
    // do something

    // now call the original implementation
    // note #1: we can substitute any arguments
    // note #2: we can even bypass calling the original method
    var ret = old.apply(this, arguments);

    // do something else, e.g., process the returned value
    // and return something
};

Essentially it looks like any other advice but its algorithm is not set in stone, it can be anything. The only requirement for this advice is to emulate the original method — it takes the same parameters, and returns the expected value (if any).

People versed in OOP probably recognized familiar implementation patterns when methods of a derived class augment their base class counterparts by adding a preprocessing, postprocessing, or calling the inherited methods conditionally. Something like that (pseudo-code):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class D : public Base{
public:
    void enter(){
        // the "before" advice pattern
        print("Entering the mutex");
        Base::enter();
    },

    void leave(){
        // the "afterReturning" advice pattern
        Base::leave();
        print("Leaving the mutex");
    },

    int calculate(int n){
        // the "around" advice pattern
        if(n < 0){ return 0; }
        return Base::calculate(n / 2) * 2 + (n % 2);
    }
};

Using AOP we can implement patterns like that across several classes instead of creating “technical” classes to augment the existing functionality.

Another interesting point is to see how joinpoints are similar to events. Dojo allows programmers to use dojo.connect() to attach a custom code to existing methods. It looks something like that:

1
2
3
dojo.connect(object, method, function(){
    console.log("we are called:", this, arguments);
});

Technically it is the “afterReturning” advice, but it is being passed the initial arguments (unless they were modified by the original method, or by previously run event handlers) just like the “before” advice in the context of the object. The returned value is not available, and this function will not be called in the case of an exception.

While it doesn’t make much sense in some cases, we should realize that the primary role of dojo.connect() is to process DOM events. DOM events can be thought as a function call of sort, but the function has an empty body — there is no default processing. In this case all “before”, “after”, or “around” don’t make any sense and can be used only for a crude prioritizing of event handlers: “before” handlers go first running backwards in the LIFO order, then “around” handlers will call each other in the LIFO order, then “after” handlers running in the FIFO order. Sounds complicated? If we are really concerned with priorities, we can do better, e.g., using priority queues.

To sum it up: events != AOP. Events cannot simulate all aspects of AOP, and AOP cannot be used in lieu of events.

So AOP in JavaScript sounds simple! Well, in reality there are several sticky places:

  • Implementing every single advice as a function is expensive.
    • Usually iterations are more efficient than recursive calls.
    • The size of call stack is limited.
    • If we have chained calls, it’ll be impossible to rearrange them. So if we want to remove one advice in the middle, we are out of luck.
    • The “around” advice will “eat” all previously attached “before” and “after” advices changing the order of their execution. We cannot guarantee anymore that “before” advices run before all “around” advices, and so on.
  • Usually in order to decouple an “around” advice from the original function, the proceed() function is used. Calling it will result in calling the next-in-chain around advice method or the original method.
  • Our aspect is an object but in some cases we want it to be a static object, in other cases we want it to be a dynamic object created taking into account the state of the object we operate upon.

These and some other problems were resolved in dojox.lang.aspect (currently in the trunk, will be released in Dojo 1.2). Let’s take a look at it.

All examples below assume that the proper package is loaded and the following namespace alias is defined:

1
2
dojo.require("dojox.lang.aspect");
var aop = dojox.lang.aspect;

First let’s define how our aspect looks like. It is an object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Aspect = {
    before: function(){
        // all arguments of the original method are passed
        // the return value will be ignored
    },

    around: function(){
        // this method emulates the original method
        // all arguments of the original method are passed

        // if we want to call the original method
        // (or the next around advice) we use proceed()
        var ret = aop.proceed(a, b, c);

        // the returned value is processed as usual
    },

    afterReturning: function(ret){
        // this method takes a return value as the only parameter
        // the return value will be ignored
    },

    afterThrowing: function(excp){
        // this method takes an exception value as the only parameter
        // the return value will be ignored
        // the exception will be propagated
    },

    after: function(){
        // this method doesn't have any parameters
        // it will be called right afterReturning() or afterThrowing()
        // the return value will be ignored
    },

    destroy: function(){
        // this optional method makes sense only for dynamic aspects
        // it will be called before the dynamic aspect is discarded
        // it should implement any necessary cleanup actions, if any
    }
};

Simple enough? Notes:

  • All aspect methods are called in the context of their aspect object. The advised object and other pertinent information is available too with lightweight function calls. See aop.getContext() and aop.getContextStack() below for details.
  • You don’t need to define all advices, only relevant methods should be defined. For example, it is perfectly valid to define the before() method only, and skip all other methods. An object with no advices defined is a legal aspect object too.

In order to attach an aspect to an object we call following function that implements a weaver:

1
aop.advise(object, method, aspect);

The “object” is the advised object. The “method” can be a string (a method’s name), a regular expression (to match several methods), or an array of such strings and regular expressions. The “aspect” can be:

  • an object.
    • It is used as a static aspect.
    • This object will be shared across all matching joinpoints.
  • a function — a constructor of an aspect object, or a function that returns an aspect object:
    • The function/constructor will be called with a single parameter — the context (see aop.getContext() below). It should create an aspect object.
    • The dynamic aspect object will be created every time before processing a joinpoint.
    • The dynamic aspect object can expose an optional destroy() method to be properly disposed when not needed. This method will be called when the processing of a joinpoint is finished.
  • an array of such objects and functions.

Examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// all setXXX() and an update() method change the object
aop.advise(object, [/^set/, "update"], {
    after: function(){
        console.log("the object was changed");
    }
});

// trace all possible exits and print them
var traceReturn = {
    afterReturning: function(ret){
        var joinPoint = aop.getContext().joinPoint;
        console.log(joinPoint.targetName + " returns " + ret);
    },
    afterThrowing: function(excp){
        var joinPoint = aop.getContext().joinPoint;
        console.log(joinPoint.targetName + " throws " + excp);
    }
};

// trace calls if an object has an attribute "loggable" set to true
var logCall = function(context){
    var aspect = {};
    if(context.instance.loggable){
        aspect.before = function(){
            var joinPoint = aop.getContext().joinPoint;
            console.log("calling " + joinPoint.targetName +
                " with arguments " + arguments);
        };
    }
    return aspect;
};
aop.advise(object, /^get/, [traceReturn, logCall]);

(We will discuss aop.getContext() and the context structure below.) As you can see our weaver is simple, yet powerful. If we want to un-advise, we can do that at any point providing we remembered the handle returned by aop.advise():

1
2
3
var handle = aop.advise(object, [/^set/, "update"], logCall);
// now we can revert our "weaving"
aop.unadvise(handle);

Internally the very first aop.advise() will create a special stub, which will run advices in the predefined fashion. All other aop.advise() calls will add necessary data to this stub. aop.unadvise() removes advices, and if it detects that there are no more advises it will remove the stub completely boosting the performance to the original level. Both aop.advise() and aop.unadvise() play nice with dojo.connect() and dojo.disconnect(). You can use all four of them together if you like. In this case aop.unadvise() is smart enough to leave a dojo.connect() stub when all AOP advices were removed, but dojo.connect() handlers are still present.

Do not use aop.advise() to advise DOM events. Use dojo.connect() for that. Use aop.advise() only with regular JavaScript objects and methods.

When writing an advice you can use following functions: aop.getContext(), aop.getContextStack(), and aop.proceed().

1
context = aop.getContext()

This function returns a context object:

1
2
3
4
5
6
7
8
9
10
11
12
// this object defines an AOP context for advices
context = {
    instance:  ..., // the instance we operate on, the same as "this"
    joinPoint: ..., // the joinpoint object (see below)
    depth:     ...  // current depth of the context stack
};

// this object defines a joinpoint's attributes
joinpoint = {
    target:     ..., // the original method
    targetName: ...  // name of the method
};

So given the context we know what object we operate on, current depth of the context stack (not counting this context), the original method, and its name in the object.

1
contextStack = aop.getContextStack();

This function returns an array of all previous context objects. We can use it to inspect the control flow up to this point. Obviously this stack doesn’t include any calls between unadvised methods, only advised methods are recorded.

aop.proceed() is used only inside “around” advices. It is called when we want to call the next in chain method (either another “around” advice, or the original method). Example:

1
2
3
4
5
{
    around: function(n){
        return aop.proceed(n + 1) - 1;
    }
}

And that’s basically it! Now you know everything you need to use AOP in your JavaScript programs.

But wait, there is more! dojox.lang.aspect implements some handy aspects, which can be used to get you started, and a helper function.

1
2
dojo.require("dojox.lang.aspect.cflow");
var flag = aop.cflow(instance, method);

This function takes two arguments: instance of the object we are checking for (or null, if any object will do), and method, which can be a string, a regular expression, or an arrays of strings and regular expressions (exactly the same as in aop.advise()). It should be called from the advice method. The function returns “true” if we found the match in the context stack, or “false” otherwise. This is a helper function to inspect control flow. See notes for aop.getContextStack().

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
aop.advise(object, /^set/, {
    // ...
});

aop.advise(object, /^get/, {
    before: function(){
        var instance = aop.getContext().instance;
        if(aop.cflow(instance, /^set/)){
            console.log("internal call to getXXX()");
        }else{
            console.log("external call to getXXX()");
        }
    }
});

Now it is time for some canned aspects. The first one is a counter, which counts calls and errors (exceptions):

1
2
dojo.require("dojox.lang.aspect.counter");
var aspect = aop.counter();

Besides advices this object defines two attributes: “calls”, which is a number of calls made to controlled methods, and “errors”, which is a number of exceptions thrown. The method “reset” can be used to reset these counters to 0. Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var counter1 = aop.counter(),
    counter2 = aop.counter();

aop.advise(object, /^get/, counter1);
aop.advise(object, /^set/, counter2);

console.log("calling calculate(42)");

object.calculate(42);

console.log("getXXX() were called", counter1.calls, "times");
console.log(counter1.calls - counter1.errors, "calls were successful");
console.log(counter1.errors, "calls were unsuccessful");

console.log("setXXX() were called", counter2.calls, "times");
console.log(counter2.calls - counter2.errors, "calls were successful");
console.log(counter2.errors, "calls were unsuccessful");

counter1.reset();
counter2.reset();

console.log("calling calculate(7)");

object.calculate(7);

console.log("getXXX() were called", counter1.calls, "times");
console.log(counter1.calls - counter1.errors, "calls were successful");
console.log(counter1.errors, "calls were unsuccessful");

console.log("setXXX() were called", counter2.calls, "times");
console.log(counter2.calls - counter2.errors, "calls were successful");
console.log(counter2.errors, "calls were unsuccessful");

If we want to trace calls with optional grouping, we can use a tracer aspect:

1
2
dojo.require("dojox.lang.aspect.tracer");
var aspect = aop.tracer(grouping);

The only parameter of this function governs the visual representation of the tracing: if it is “true” the console will create nested structures, otherwise it will use simple console.log(). The picture below shows how it looks with grouping in the console:

aop.tracer() output

Every line starts with the object representation, so we can inspect it, if we need to. After that it shows which method was called and with what parameters. We can see that the assertSquare() method was called without parameters. It called getHeight() and getWidth() internally to make sure this object is in fact a square, and it returned normally with no result. The second call to assertSquare() was made on a different object. It called again getHeight() and getWidth(), which returned different values (200 and 300 respectively). After that assertSquare() has thrown an exception “NOT A SQUARE!”.

The next function returns an aspect used for timing calls:

1
2
dojo.require("dojox.lang.aspect.timer");
var aspect = aop.timer(name);

It takes a name of a timer as a parameter. Make sure that you use a unique name. If it is not supplied, the unique name will be generated. In general this aspect is very simple: it starts the timer when the controlled function is called, and stops it, when it is finished, printing the result to the console. If in the course of execution the controlled method calls other methods controlled by the same timer (or calls itself recursively) only the top level call will be timed.

We will see an example of this aspect in action later.

In some cases you need a real profiler. There is an aspect for that too:

1
2
dojo.require("dojox.lang.aspect.profiler");
var aspect = aop.profiler(title);

It takes an optional session title as a parameter. Warning: it works only in Firebug (not in Firebug Lite bundled with Dojo). Example:

1
2
aop.advise(object, "calculate", aop.profiler("Calculations"));
object.calculate(42);

This will print a nice table with profiling data. Note that if your profiled method logged something to the Firebug’s console, it can mess up the display in some cases.

Enough with debugging, let’s take a look at a memoization aspect. Memoization is a fancy term for caching already calculated values, so we don’t need to spend resources to recalculate them. Usually it is used as an optimization technique.

Take a look at the memoizer aspect and its companion — memoizerGuard:

1
2
3
4
5
dojo.require("dojox.lang.aspect.memoizer");
var aspect1 = aop.memoizer(keyMaker);

dojo.require("dojox.lang.aspect.memoizerGuard");
var aspect2 = aop.memoizerGuard(method);

The memoizer aspect can be attached to several methods and it will cache the result for every combination of arguments. But sometimes the result depends on a combination of arguments and the internal state of the object. In this case we can attach the memoizerGuard aspect to mutating methods of the object to invalidate the cache.

The “keyMaker” argument is required if we are to cache a function with multiple arguments, or a single non-trivial argument. It should return a unique number or a unique string, which will be used as a hash key. If your only argument is a number, or a string, in this case it can be used directly — no keyMaker is needed.

The “method” argument can be a string or an array of strings that define what method’s caches should be invalidated after the controlled methods were called.

The best example is the Fibonacci calculator. (I know, in the real life almost nobody writes the Fibonacci calculations, but it makes a good toy example.) Let’s do it with a twist — it’ll calculate Fibonacci numbers of Nth degree (watch for aop.timer() use as well):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var Fibonacci = function(order){
    if(arguments.length == 1){
        this.setOrder(order);
    }else{
        this.order = 1;
    }
};

dojo.extend(Fibonacci, {
    setOrder: function(order){ this.order = order; },
    getOrder: function(){ return this.order; },
    calculate: function(n){
        if(n < 0){ return 0; }
        if(n == 0){ return 1; }
        return this.calculate(n - 1) + this.calculate(n - 1 - this.order);
    }
});

var fib = new Fibonacci;

// we will time the calculations
aop.advise(fib, "calculate", aop.timer("fib"));

fib.calculate(15);
fib.setOrder(0);
fib.calculate(15);

// now lets use memoization
aop.advise(fib, "calculate", aop.memoizer());
aop.advise(fib, /^set/, aop.memoizerGuard("calculate"));

fib.setOrder(1); // set order back to 1 - regular Fibonacci numbers
fib.calculate(15);
fib.setOrder(0);
fib.calculate(15);

On my computer with Firefox 3 the calculation of 1-order (regular) Fibonacci number of 15 (987) took ~48ms without memoization and 0-1ms after memoization. The calculation of 0-order Fibonacci number of 15 (32768) took ~1155ms without memoization and the same 0-1ms after. As you can see this technique can work wonders without much investment of time.

Now go and play with AOP! Can’t wait until Dojo 1.2? Get the code straight from the repository! I advise you to start with the page I created to test the implementation. And don’t forget about this aspect of JavaScript when designing your applications!