Tuesday, 11 December 2012

CoffeeScript aspects

Hojoo! First "real" blog message coming, finally!

There are some cases when client implementation shoudn't be available for everyone. I encountered such a problem when I was creating Backbone views for different users: users may have different roles, and depending on those roles, some actions should be available or not.

Of course I could have just ignored the actions is my Javascript. In this case, the whole implementation had to be hidden so if-else was out of question. Making different views for different role combinations is just plain stupid.

I decided to solve the problem by using using aspect oriented programming. Javascript functions have apply-method so it was quite trivial to make a simple aspect library:

class MyClass
foo: ->
alert "foobar"
bar: ->
alert "barfoo"
@foo()
aspect = (clz, extensions) ->
weaver =
# this does the actual binding
# this is called from extension code
around: (pointcut, advice) ->
prev = null
prev = if clz.prototype then clz.prototype[pointcut] else null
return if not prev
clz.prototype[pointcut] = ->
args = arguments
obj = this
joinPoint =
getTarget: -> obj
retVal: null
getArgs: -> args
getReturnValue: -> @retVal
setReturnValue: (val) -> @retVal = val
# this is called from advice (usually) once
proceed: ->
@setReturnValue prev.apply(obj, @getArgs())
# this line calls the advice code when
# target object method is called
advice.call(weaver, joinPoint)
return joinPoint.getReturnValue()
# bind advices
extensions.apply(weaver)
aspect MyClass, ->
@around 'foo', (jp) ->
alert "BEFORE"
jp.proceed()
alert "AFTER"
a = new MyClass
a.foo()
alert "---"
a.bar()

Usage is trivial: just call @around-method and give the function name (pointcut) which must be wrapped. When function is called, the wrapper code is called first. It can decide whether to execute the original code via joinpoint interface or not. It can also modify function call argumens and return value. Joinpoint supports following functions:

class MyClass
simple: ->
alert "hello!"
another: ->
alert "hi!"
withParams: (a, b) ->
alert a + " - " + b
withReturnValue: (a) ->
return 2 * a
...
aspect MyClass, ->
@around 'simple', (jp) ->
alert "!!"
jp.proceed()
a = new MyClass
a.simple()
....
# multiple aspects are also ok! these aspects could be in separate files
# not that the call order depends on execution order: latest advice is called first
aspect MyClass, ->
@around 'simple', (jp) ->
alert "!!"
jp.proceed()
aspect MyClass, ->
@around 'simple', (jp) ->
jp.proceed()
alert '??'
a = new MyClass
a.simple()
...
# we can add many advices in same aspect
aspect MyClass, ->
@around 'simple', (jp) -> alert '!!'; jp.proceed()
@around 'another', (jp) ->
# we can also "override" whole implementation
alert "overrided"
a = new MyClass
a.simple()
a.another()
...
# we can modify arguments
aspect MyClass, ->
@around 'withParams', (jp) ->
for i in [0..1]
jp.getArgs()[i] = "*#{jp.getArgs()[i]}*"
jp.proceed()
a = new MyClass
a.withParams("foo", "bar")
...
# we can also modify return values
aspect MyClass, ->
@around 'withReturnValue', (jp) ->
jp.proceed()
jp.setReturnValue Math.abs(jp.getReturnValue())
a = new MyClass
alert a.withReturnValue(3)
alert a.withReturnValue(-3)
Now I just have to make a basic Backbone view which is public for everyone. When I fetch the roles during page render, I can include all needed logic in separate files. Those files add the logic by binding their advices to the basic view. I can also apply some security policy for those Javascript "extensions", completely hiding the "secret features" from unwanted users. Pretty handy, I think! :)

No comments:

Post a Comment