This is a decorator, which is used to weave AOP advices while building new “classes”.
Description
dcl.advise() is a decorator function, which takes the advice object with properties before, around, and/or after and
combines an existing method with supplied advices.
Alternatively it can take dcl.Prop object, and advise its get, set, or value properties. For more info on property descriptors see Object.defineProperties().
dcl.advise()
12345678910111213141516171819202122232425262728
varA=dcl({method:function(msg){console.log("MSG: "+msg);}});varB=dcl(A,{method:dcl.advise({before:function(msg){console.log("Method was called with msg = "+msg);},after:function(args,result){console.log("Method has finished.");},around:function(sup){returnfunction(msg){// let's ignore our parametersup.call(this,"Canned response no matter what.");};}})});varb=newB();b.method("Hey!");// Method was called with msg = Hey!// MSG: Canned response no matter what.// Method has finished.
varA=dcl({constructor:function(msg){this.msg=msg;},m:dcl.prop({get:function(){return"MSG: "+this.msg;}})});varB=dcl(A,{m:dcl.prop({get:dcl.advise({before:function(){console.log("msg = "+this.msg);},after:function(args,result){console.log("Getter has finished.");},around:function(sup){returnfunction(msg){// let's ignore our parameterreturnsup.call(this)+" vey!";};}})})});varb=newB("Oy");console.log(b.m);// msg = Oy// Getter has finished.// MSG: Oy vey!
Advices are functions with following properties.
Before
This is a regular function. It is called with the same context and the same arguments as an advised method.
Its return value is ignored.
It is not recommended to modify parameters inside before advice. Use around advice for that.
After
This is a regular function. It is called with the same context as an advised method. It takes up to four parameters:
args - an arguments object (a pseudo-array) used to call an advised method.
result - a returned value or a thrown exception object.
makeReturn(value) - a procedure, which can be called to supply a new returned value.
makeThrow(value) - a procedure, which can be called to emulate an exception. In this case value is assumed to be a valid exception value, e.g., an Error object.
Both makeReturn() and makeThrow() can be called several times. The last value is used as the result.
The returned value of an after advice is ignored.
It is not recommended to modify parameters or a returned value inside after advice. Use makeReturn(), or makeThrow() for that. Or consider using around advice.
It is recommended to derive all exception objects from the standard Error object, so erroneous and normal
result values would be easily distinguished.
Around
Essentially it is the same as dcl.superCall(). It uses the same double function pattern,
and its behavior is the same.
Order of advices
Advices are always applied in the following order regardless of their declaration order:
All before advices go first in the reverse chronological order (the last one goes first).
All around advices go next in the reverse chronological order (the last one goes first). The next around advice
is called only if its previous around advice yielded control explicitly by calling its super method.
All after advices go last in the chronological order (the first one goes first).
Both before and after chains are called regardless how around advices are handled. For example, this is a totally valid situation:
varA=dcl({m:function(x){returnx;}});vara=newA();console.log(a.m(5));// 5console.log(a.m(6));// 6varB=dcl(A,{m:dcl.advise({after:function(args,result,makeReturn,makeThrow){if(result%2){makeReturn(1);return;}makeThrow(newError("evil even number!"));}})});varb=newB();try{console.log(b.m(5));console.log(b.m(6));}catch(e){console.log(e.message);}// PRINTS:// 1// evil even number!
Notes
Shortcuts
If you want to weave just one advice, you may want to use a shortcut:
dcl.before()
12345678910111213
varB1=dcl(A,{method:dcl.before(function(msg){console.log("Method was called with msg = "+msg);})});// is equivalent tovarB2=dcl(A,{method:dcl.advise({before:function(msg){console.log("Method was called with msg = "+msg);}})});
dcl.after()
12345678910111213
varB3=dcl(A,{method:dcl.after(function(){console.log("Method has finished.");})});// is equivalent tovarB4=dcl(A,{method:dcl.advise({after:function(){console.log("Method has finished.");}})});
dcl.around()
12345678910111213141516171819202122232425262728
varB5=dcl(A,{method:dcl.around(function(sup){returnfunction(msg){// let's ignore our parametersub.call(this,"Canned response no matter what.");};})});// is equivalent tovarB6=dcl(A,{method:dcl.superCall(function(sup){returnfunction(msg){// let's ignore our parametersub.call(this,"Canned response no matter what.");};})});// is equivalent tovarB7=dcl(A,{method:dcl.advise({around:function(sup){returnfunction(msg){// let's ignore our parametersub.call(this,"Canned response no matter what.");};}})});
The described order is followed to the letter. Specifically I want to stress “regardless of their declaration order”.
It doesn’t matter in what order advices were declared.
Which is not true for simple faux-AOP wrapper-based implementations. To wit:
// UBER-SIMPLE FAUX-AOPfunctionwrapBefore(object,name,advice){varsup=object[name];object[name]=function(){advice.apply(this,arguments);returnsub.apply(this,arguments);};}functionwrapAfter(object,name,advice){varsup=object[name];object[name]=function(){try{varresult=sub.apply(this,arguments);advice.call(this,arguments,result);}catch(e){advice.call(this,arguments,e);}returnresult;};}functionwrapAround(object,name,advice){varsup=object[name];object[name]=function(){// for simplicity I assume that the advice takes// its super as its first parameter, and all arguments// as the second parameteradvice.call(this,sup,arguments);};}// EXAMPLEvarx={a:function(){console.log("HA!");}};wrapBefore(x,"a",function(){console.log("before");});wrapAround(x,"a",function(sup){console.log("around: before calling");sup.apply(this,arguments);console.log("around: after calling");});wrapAfter(x,"a",function(){console.log("after");});// RESULT:// around: before calling// before// HA!// around: after calling// after// SHOULD BE:// before// around: before calling// HA!// around: after calling// after
In general the relative order of different wrapper-based advices will depend on their definition order, while in true AOP it is rigid. In our faux-AOP example, if our around advice doesn’t call its super for any reason, before and/or after advices, depending on their order of wrapping, are never called, which is incorrect, and breaks the invariant.
While the order difference looks harmless, it prevents from using some important AOP techniques. For example, it prevents setting up a hook function (an after advice) that runs after all “normal” methods. See multi-stage construction for an interesting use case.