Intro to Aspect-Oriented Programming
Posted on 09 Jul 2014
In any non-trivial application, functions quickly become weighed down with supporting functionality like logging, tracing, and metrics gathering. These blocks of code tend to echo throughout codebases with low variance and are referred to as cross-cutting concerns. Aspect-Oriented Programming (AOP) helps achieve the separation of concerns by pulling these blocks into modules called aspects.
AOP in Practice
Introducing the Phone class1
At the risk of presenting a relatively contrived example, I have constructed a
simple Phone class with a single method dial
.
function Phone() {};
Phone.prototype.dial = function (friend) {
var start = new Date().getTime();
log('dial(): {}', friend.name);
if (friend && friend.picksUp) {
var end = new Date().getTime();
log('Call lasted: {} milliseconds', end - start);
return friend.message;
} else {
log('Error: No answer!');
throw 'No answer!';
}
};
The dial
function works as follows:
- Log a tracing message containing the method name and friend’s name
- If the friend picks up:
- Log the call duration
- Return the friend’s message
- Otherwise:
- Create a log message indicating the friend failed to pick up
- Throw a ‘No answer!’ exception
Remember that AOP aims to separate concerns from business logic. Now look back at the Phone class. How much of it is actually business logic?
-
Log a debug message with the method name and friend's name - If the friend picks up:
-
Log the call duration - Return the friend’s message
-
- Otherwise:
-
Create a log message indicating the friend failed to pick up - Throw a ‘No answer!’ exception
-
It turns out that nearly half our function code is devoted to concerns! Let’s see how to separate these concerns into aspects with the meld AOP library.
Adding Callee Metadata
To start off, let’s pull out the initial tracing statement into a piece of
advice. Advice refers to a function that acts upon another function. Here,
we create a before
advice, an advice that runs before the execution of
other functions.
meld.before(Phone.prototype, 'dial', function () {
log(
'{}: Calling {}... ',
meld.joinpoint().method,
meld.joinpoint().args[0].name
);
});
We use the meld.before
method to apply the log advice before Phone.dial
runs. In AOP terminology, we apply the log advice at the before
join point of the Phone.dial
pointcut. The join point is the point in
execution where the advice is applied, while the pointcut defines which
functions the advice is applied on.
Notice that join points provide relevant information about the associated
pointcut. In this aspect, we access meld.joinpoint().method
and
meld.joinpoint().args[0].name
to access the method name and arguments.
Join points also provide other useful meta features, as we will see in the next
section.
Tracing is an extremely common use case for AOP. It allows developers to easily observe the passage of values through an application without modifying any class code.
Timing the Call
Next, we extract the timing function:
meld.around(Phone.prototype, 'dial', function (joinPoint) {
var start = new Date().getTime();
var message = joinPoint.proceed();
var end = new Date().getTime();
log('Call lasted: {} milliseconds!', (end - start));
return joinPoint.args[0].name + ': ' + message;
});
As the name implies, around
advice is executed around the function being
called. That is, it contains code that is run both before and after the
function call. This special join point has a .proceed
function that tells
meld to go ahead and run the function this aspect is associated with. If
joinPoint.proceed
is not used, phone.dial
will never be executed.
// Calling fn2 from fn1, where fn2 has around advice
---------- ----------- ---------
| | --fn2()--> | | | |
| fn1() | | around | --jp.proceed()--> | fn2() |
| | | advice | <----return-------| |
| | <-return-- | | | |
---------- ----------- ---------
around
also allows us to change the return value of the function. The return
value of joinPoint.proceed
is the return value of the function call.
around
’s advice can modify it freely before returning it to the caller, or
decide to return a completely different value altogether. The around
advice
essentially serves as a proxy
between the caller and the callee.
around
is the most powerful and robust join point in AOP due to its control
over the callee function.
Error Handling
Finally, let’s look at logging exceptions. The afterThrowing
aspect is
executed after an exception is thrown.
meld.afterThrowing(Phone.prototype, 'dial', function (error) {
log('Error: {}', error);
});
Here, the join point passes the thrown exception to the afterThrowing
advice.
afterThrowing
is particularly useful for collecting metrics about where/why
an application is encountering errors. You can think of a web server
aggregating data on which URLs many visitors are encountering “404 Not Found”
errors at.
In more complex applications, this join point can also be used to implement retry logic for code that might experience transient errors:
meld.afterThrowing(SomeObject, 'someMethod', function (error) {
if (isTransient(error)) { // Check whether error is recoverable
setTimeout(function () {
// Retry after a delay
SomeObject.someMethod();
}, delay);
}
});
The afterThrowing
aspect can be handy for logging and handling exceptions.
Putting It All Together
The finished code can be seen on JSFiddle or GitHub. As you can see, using AOP allowed us to condense our function down to 5 lines of business logic and 3 reusable aspects.
While here we have stuck all the code in one file, developers with larger codebases may choose to split the aspects into a separate file.
Parting Thoughts
Aspect-Oriented Programming can be useful for refactoring applications. In addition to the types of advice discussed, AOP libraries also provide:
after
advice: Run after function call, regardless of success.afterReturning
advice: Run after successful function call.
In the writing of this tutorial, I chose to use Javascript due to its popularity and convenience, but AOP is available in many other languages. Java, for example, has Spring AOP and AspectJ. Both libraries, in addition to matching the functionality of meld.js, also support greater pointcut flexibility and reuse. There are Spring AOP snippets in my snippets repository on GitHub for those interested.
Aspect-Oriented Programming provides powerful techniques to decouple concerns from business logic, resulting in cleaner code.
UPDATE: The kind folks at POSTD have translated this post into Japanese! You can find it here.
Further Reading
Footnotes
-
Technically, Javascript has “prototypes”, not “classes”. ↩