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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |