内容


深入了解 Dojo 的面向方面编程

Comments

简介

AOP(Aspect Oriented Programming)即:面向方面编程,是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。这里“统一添加的功能”主要是指给一系列函数统一添加某段功能代码,让所有调用这个函数的地方在执行这个函数是都会在开始,或者结束时执行那段功能代码;当然,这种类似嵌入的代码是不会写在函数内部的,即函数的实现是不会有任何改变的。这种机制主要用于权限管理,缓存,错误处理等等方面。Dojo 的 Lang 包里面有一个 AOP 模式的实现,我们接下来会深入了解一下它的功能。

由于本文着重于介绍基于 Web 的 AOP,如果之前没有接触过 AOP 的读者可以先去了解一下基于 Java 的 AOP 实现,目前市面上 AOP 的具体实现也有不少,其中 AspectJ 是一个比较完善的 AOP 项目(http://www.eclipse.org/aspectj),大家可以通过这个开源项目来了解一下 AOP 的工作模式和开发流程,通过它能帮助大家理解我们接下来要介绍的基于 Web 的 AOP 实现。

Dojo 的 AOP 工具包

Dojo 的 AOP 包中核心类就是“dojox.lang.aspect”,它提供了一系列方面编程的接口供大家使用,包括“advise”、“memorization”等等,接下来我们会一一介绍。

我们知道,面向方面的开发离不开对象和函数方法,所以,我们接下来就基于一个类对象,来介绍 Dojo 中的 AOP 接口是如何工作的。

先来看看我们的示例类:

清单 1. Rect 类定义
 var Rect = function(){ 
 this.x = this.y = this.width = this.height = 0; 
 }; 

 dojo.extend(Rect, { 
 // Get 类方法
 getX: function(){ return this.x; }, 
 getY: function(){ return this.y; }, 
 getWidth: function(){ return this.width; }, 
 getHeight: function(){ return this.height; }, 

 // Set 类方法
 setX: function(val){ this.x = val; }, 
 setY: function(val){ this.y = val; }, 
 setWidth: function(val){ this.width = val; }, 
 setHeight: function(val){ this.height = val; }, 

 // 特殊方法
 getPerimeter: function(){ return 2 * (this.width + this.height); }, 
 getArea: function(){ return this.width * this.height; }, 
 getCorner: function(){ 
 return {x: this.x + this.width, y: this.y + this.height}; 
 }, 
 move: function(x, y){ this.x = x; this.y = y; }, 
 makeSquare: function(l){ this.width = this.height = l; }, 
 scale: function(s){ this.width *= s; this.height *= s; }, 
 pointInside: function(x, y){ 
 returnthis.x <= x && x < (this.x + this.width) && 
 this.y <= y && y < (this.y + this.height); 
 }, 
 assertSquare: function(){ 
 if(this.getHeight() != this.getWidth()){ 
 throw new Error("NOT A SQUARE!"); 
 } 
 } 
 });

“Rect”这里有一些属性和方法,我们可以基于这个类,来看看 Dojo 的 AOP 接口是如何作用到这个类上,并获得怎样的效果的。

Advise 接口

我们先来预定义几个对象,用于后面的代码:

清单 2. 预定义的 tracer
 var aop = dojox.lang.aspect, df = dojox.lang.functional; 

 // our simple advices 
 var TraceArguments = { 
 before: function(/*arguments*/){ 
 var joinPoint = aop.getContext().joinPoint, 
 args = Array.prototype.join.call(arguments, ", "); 
 console.log("=> " + joinPoint.targetName + "(" + args + ")"); 
 } 
 }; 
 var TraceReturns = { 
 afterReturning: function(retVal){ 
 var joinPoint = aop.getContext().joinPoint; 
 console.log("<= " + joinPoint.targetName + " returns " + retVal); 
 }, 
 afterThrowing: function(excp){ 
 var joinPoint = aop.getContext().joinPoint; 
 console.log("<= " + joinPoint.targetName + " throws: " + excp); 
 } 
 };

有几个地方需要大家留意一下:

1. “aop = dojox.lang.aspect”我们以后用到的“aop”都是指代“dojox.lang.aspect”。

2. “df = dojox.lang.functional”这里“df”代表 functional 接口集。

3. “TraceArguments”和“TraceReturns”都是对象,今后的 AOP 接口会用到,注意里面的“before”,“afterReturning”,“afterThrowing”等接口。

好了,现在万事俱备,只欠东风,我们来开始我们的 Dojo 的 AOP 之旅吧。

我们先来看看正常的类的用法:

清单 3. 基本类的使用
 var rect1 = new Rect; 
 rect1.move(100, 100); 
 rect1.makeSquare(200); 
 rect1.pointInside(150, 250); 
 console.log("perimeter: " + rect1.getPerimeter()); 
 console.log("area: " + rect1.getArea());

这里“rect1”的方法的返回值相信大家都能算出来,我们主要来看看它加了 AOP 之后的变化。

我们再通过以下的例子来看看 advise 接口的用法。

清单 4. 接口 advise
 var rect2 = new Rect; 
 aop.advise(rect2, /^get/, TraceReturns); 
 aop.advise(rect2, [/^set/, "move", "makeSquare"], TraceArguments); 
 aop.advise(rect2, "pointInside", [TraceReturns, TraceArguments]); 
 rect2.move(100, 100); 
 rect2.makeSquare(200); 
 rect2.pointInside(150, 250); 
 console.log("perimeter: " + rect2.getPerimeter()); 
 console.log("area: " + rect2.getArea());

注意这里的 advise 方法:

1. “aop.advise(rect2, /^get/, TraceReturns)”表示给 rect2 对象的所有 get 开头的方法加上“TraceReturns”的方法。由于“TraceReturns”里面定义了“afterReturning”和“afterThrowing”两个方法,所以如果 get 方法正常返回,则会调用“afterReturning”,如果抛了异常,则会调用“afterThrowing”。注意,这里原 get 方法并没有任何改变,但是 get 方法的行为变了。是不是像是所有的 get 方法被人一刀切开,并在它们的尾部加上了一段功能代码?这就是所谓的面向方面的编程。

2. “aop.advise(rect2, [/^set/, "move", "makeSquare"], TraceArguments)”表示给 rect2 的所有 set 方法以及 move,makeSquare 都加上“TraceArguments”对象的方法。由于“TraceArguments”里面定义了“before”,这表明一旦调用到这些方法,都会先执行“before”函数里面的功能代码。(一刀切又来了)

3. “aop.advise(rect2, "pointInside", [TraceReturns, TraceArguments])”表示给“pointInside”方法同时加上“TraceReturns”和“TraceArguments”,作用就是一种叠加效果。

所以,我们可以看出这里函数的执行方式:

“rect2.move(100, 100)”:“before” == 》 “move”。
“rect2.makeSquare(200)”:“before” == 》 “makeSquare”。
“rect2.pointInside(150, 250)”:“before” == 》 “pointInside” == 》 “afterReturning”
“rect2.getPerimeter()”:“getPerimeter” == 》 “afterReturning”
“rect2.getArea()”:“getArea” == 》 “afterReturning”

这里支持模糊匹配方法“/^set/”,“/^get/”,所以,可以方便您进行针对海量方法的 AOP 编程。

再来看看下面的代码:

清单 5. 接口 advise 进阶
 var h1 = aop.advise(Rect, /^get/, TraceReturns); 
 var h2 = aop.advise(Rect, [/^set/, "move", "makeSquare"], TraceArguments); 
 var h3 = aop.advise(Rect, "pointInside", [TraceReturns, TraceArguments]); 
 rect1.move(100, 100); 
 rect1.makeSquare(200); 
 rect1.pointInside(150, 250); 
 console.log("perimeter: " + rect1.getPerimeter()); 
 console.log("area: " + rect1.getArea()); 
 aop.unadvise(h1); 
 aop.unadvise(h2);

这里的 advise 方法看似与之前的一样,其实不然,大家仔细看,这里的传入参数已经不是对象“rect2”了,而是一个类“Rect”,当我们加入 advise 之后,我们可以看到,之前初始化的“rect1”对象也具有了 AOP 的功能,其函数执行方式与之前的 rect2 对象无异。所以,由此得出,我们的 advise 不仅适用于对象,还适用于类本身。

如果我们想取消 advise,调用“unadvise”即可,如:“aop.unadvise(h1)”。

还需要说明一下,之前的 trace 方法里面我们都看到有这么一点代码:

“var joinPoint = aop.getContext().joinPoint”,它主要用于获取当前执行的上下文,其中“joinPoint.targetName”为方法名。

动态 advise

接下来我们会介绍一些比较复杂的 advise 示例。我们先来看看我们的新的示例类:

清单 6. 简单 tracer 定义示例
 var Trace = function(context){ 
 this.name = context.joinPoint.targetName; 
 }; 
 dojo.extend(Trace, { 
 before: function(/*arguments*/){ 
 this.args = Array.prototype.join.call(arguments, ", "); 
 }, 
 afterReturning: function(retVal){ 
 var buf = "-- " + this.name + "(" + this.args + ")"; 
 if(typeof retVal == "undefined"){ 
 // procedure without a return value 
 console.log(buf); 
 }else{ 
 // function with returned value 
 console.log(buf + " returns: " + retVal); 
 } 
 }, 
 afterThrowing: function(excp){ 
 console.log("-- " + this.name + "(" + this.args + ") throws: " + excp); 
 } 
 });

可以看到,这里的 trace 包含了“before”,“afterReturning”和“afterThrowing”,这个 Trace 对象已经包含了我们之前定义个两个 tracing 对象的功能之和。注意,它也是一个对象,所以在它的内部,成员变量在不同成员函数之间是可以共享的。这里的 Trace 与之前的最大不同是:它的“afterReturning”方法执行的功能代码是根据它所绑定的函数不同而不同的。参考如下使用案例:

清单 7. 简单动态 advise
 var rect3 = new Rect; 
 aop.advise(rect3, /^\S/, Trace); 
 rect3.move(100, 100); 
 rect3.makeSquare(200); 
 rect3.pointInside(150, 250); 
 console.log("perimeter: " + rect3.getPerimeter()); 
 console.log("area: " + rect3.getArea()); 
 rect3.assertSquare();

这里的“/^\S/”基本表示“切入”所有方法。执行起来就是:没有返回值的方法会打出函数名,有返回值的方法会加上返回值一起打出(console.log)。

再来看个更复杂的 trace 定义示例:

清单 8. 复杂 tracer 定义示例
 var TraceAll = function(context, id){ 
 this.name = context.joinPoint.targetName; 
 this.prefix = dojo.string.pad("", context.depth * 2, "--", true) + "-- #" + (id || 1); 
 }; 
 dojo.extend(TraceAll, { 
 before: function(/*arguments*/){ 
 var args = Array.prototype.join.call(arguments, ", "); 
 console.log(this.prefix + " => before " + this.name + "(" + args + ")"); 
 }, 
 around: function(/*arguments*/){ 
 var args = Array.prototype.join.call(arguments, ", "); 
 console.log(this.prefix + " => around " + this.name + "(" + args + ")"); 
 var retVal = aop.proceed.apply(null, arguments); 
 console.log(this.prefix + " <= around " + this.name + " returns " + retVal); 
 return retVal; 
 }, 
 afterReturning: function(retVal){ 
 console.log(this.prefix + " <= afterR " + this.name + " returns " + retVal); 
 }, 
 afterThrowing: function(excp){ 
 console.log(this.prefix + " <= afterT " + this.name + " throws: " + excp); 
 }, 
 after: function(){ 
 console.log(this.prefix + " <= after  " + this.name); 
 } 
 });

这个“TraceAll”几乎实现了所有的切入功能代码,包括“before”,“after”,“around”,“afterReturning”,“afterThrowing”等。这里要注意:

  1. “around”是在切入方法执行的时候执行的,即在“before”之后,“after”之前。
  2. “afterReturning”是在“after”之前执行的。

参考如下执行代码:

清单 9. 复杂动态 advise
aop.advise(rect4, /^\S/, [
function(context){ return new TraceAll(context, 1); },
function(context){ return new TraceAll(context, 2); }
]);
 rect4.move(100, 100); 
 rect4.makeSquare(200); 
 rect4.pointInside(150, 250); 
 console.log("perimeter: " + rect4.getPerimeter()); 
 console.log("area: " + rect4.getArea());

注意,这里的 advise 其实是对所有方法都切入了 TraceAll,而且每个方法都切入了两个 TraceAll(id 为 1 和 id 为 2),所以,以“move”方法为例,它的执行顺序应为:2 个“before” == 》 2 个“around” == 》2 个 “afterReturning”和“after”。有一点需要强调:“after”是紧跟在“afterReturning”之后调用的。

之前的 trace 都是切入所有成员方法的,如果我们的成员方法里调用了另外一个成员方法,也会被切入。其实,Dojo 也支持支切入“顶层”的方法。比如“assertSquare”成员方法里面就调用了“getHeight”和“getWidth”两个成员方法,在之前的例子中“getHeight”和“getWidth”也会切入执行绑定的功能代码,但是,这里我们可以让它忽略内部的切入。

清单 10. 顶层动态 advise
 var TraceTopLevel = function(context){ 
 this.name = context.joinPoint.targetName; 
 }; 
 dojo.extend(TraceTopLevel, { 
 before: function(/*arguments*/){ 
 var args = Array.prototype.join.call(arguments, ", "); 
 console.log("=> " + this.name + "(" + args + ")"); 
 }, 
 afterReturning: function(retVal){ 
 console.log("<= " + this.name + " returns: " + retVal); 
 }, 
 afterThrowing: function(excp){ 
 console.log("<= " + this.name + " throws: " + excp); 
 } 
 }); 


 aop.advise(rect5, /^\S/, 
 function(context){ 
 return aop.cflow(context.instance) ? {} : new TraceTopLevel(context); 
 } 
 );

其实这里的关键点不是在“TraceTopLevel”这个对象上,而是在 advise 里面的第三个参数“aop.cflow(context.instance) ? {} : new TraceTopLevel(context)”上。这里的“context”就是当前绑定(切入)函数的上下文,“aop.cflow(context.instance)”这里就是判断当前上下文的实例(instance)是否已经存在上下文栈中,用它可以判断是否为内部调用,如果是,则使用“{}”作为 trace,否则才用“TraceTopLevel”。所以,这样切入的结果是:我们看不到“getHeight”和“getWidth”的切入功能代码的执行了。

自带的 tracer

我们之前定义了很多 tracer,实现了它们的“before”,“after”,“around”,“afterReturning”,“afterThrowing”等等方法,其实 Dojo 的 AOP 工具包里面有很多自带的 tracer 也很好用:

清单 11. aop.tracer
 h1 = aop.advise(rect1, /^\S/, aop.tracer(true)); 

 rect1.move(100, 100); 
 rect1.makeSquare(200); 
 rect1.pointInside(150, 250); 
 console.log("perimeter: " + rect1.getPerimeter()); 
 console.log("area: " + rect1.getArea()); 
 try{ 
 rect1.assertSquare(); 
 rect1.width = 300; // triggering exception 
 rect1.assertSquare(); 
 }catch(e){ 
 // squelch 
 } 
 aop.unadvise(h1);

重点看第一行的“aop.tracer(true)”,这是一个 Dojo 的 AOP 自带的 tracer,它其实也是一个自定义的对象(可以参见“dojox.lang.aspect.tracer”类),它的功能就是输出(log)您所切入方法的重要相关信息,有兴趣的同学可以参考一下它的源代码,可以指导自己如何写 tracer。

还有一些比较好用的小 tracer:

清单 12. aop.counter()
 h1 = aop.advise(rect1, /^get/, aop.counter()); 

 rect1.move(100, 100); 
 rect1.makeSquare(200); 
 rect1.pointInside(150, 250); 
 console.log("perimeter: " + rect1.getPerimeter()); 
 console.log("area: " + rect1.getArea()); 
 try{ 
 rect1.assertSquare(); 
 rect1.width = 300; // triggering exception 
 rect1.assertSquare(); 
 }catch(e){ 
 // squelch 
 } 
 aop.unadvise(h1);

“aop.counter()”用于计算所切入函数的数量。由于它绑定所有 get 方法,所以它最后的输出为:“aop.counter().calls”为 6,“aop.counter().errors”为 0.

清单 13. aop.timer()
 h1 = aop.advise(rect1, /^\S/, aop.timer()); 

 rect1.move(100, 100); 
 rect1.makeSquare(200); 
 rect1.pointInside(150, 250); 
 console.log("perimeter: " + rect1.getPerimeter()); 
 console.log("area: " + rect1.getArea()); 
 try{ 
 rect1.assertSquare(); 
 rect1.width = 300; // triggering exception 
 rect1.assertSquare(); 
 }catch(e){ 
 // squelch 
 } 
 aop.unadvise(h1);

其实从字面上我们就可以猜出来,“aop.timer()”其实就是一个计时器,记录每个方法执行的时间,这个方法用来白盒测试测性能再好不过了,它会告诉您每个被切入方法执行的时间。

最后我们来看一个计算 Fibonacci 数列的例子:

清单 14. Fibonacci 类
 var Fibonacci = function(order){ 
 if(arguments.length == 1){ 
 this.setOrder(order); 
 }else{ 
 this.offset = 2; 
 } 
 } 
 dojo.extend(Fibonacci, { 
 setOrder: function(order){ 
 this.offset = order + 1; 
 }, 
 getOrder: function(){ 
 return this.offset - 1; 
 }, 
 calculate: function(n){ 
 if(n < 0){ return 0; } 
 if(n == 0){ return 1; } 
 return this.calculate(n - 1) + this.calculate(n - this.offset); 
 }, 
 calculateN: function(n, o){ 
 if(n < 0){ return 0; } 
 if(n == 0){ return 1; } 
 return this.calculateN(n - 1, o) + this.calculateN(n - 1 - o, o); 
 } 
 });

Fibonacci 数列的特点是后一个数是前两个数的和,所以这里“calculate”和“calculateN”都是一种递归计算的模式。看如下计算:

清单 15. 没有加入记忆棒
 h1 = aop.advise(fib, /^calculate/, aop.timer("fib")); 

 fib.setOrder(0); 

 fib.caculate(1); 
 fib.caculate(2); 
 fib.caculate(3); 
。。。。。。
 fib.caculate(15);--- fib: 259ms

通过 timer 的跟踪,计算第 15 位的 Fibonacci 数列的值需要 259 毫秒。

下面我们换一种方式:

清单 16. 加入记忆棒
 h2 = aop.advise(fib, "calculate", aop.memoizer()); 
 h3 = aop.advise(fib, /^set/, aop.memoizerGuard("calculate")); 

 fib.setOrder(0); 

 fib.caculate(1); 
 fib.caculate(2); 
 fib.caculate(3); 
。。。。。。
 fib.caculate(15);--- fib: 1ms

这次需要 1 毫秒了,为什么?其实是因为我们用了“记忆棒” -“aop.memoizer()”。它会记录之前所计算的所有值,所以当我们算到 Fibonacci 数列第 15 位时,其实之前的第 14 位和第 13 位已经缓存住了,所以简单的相加就行了,故只需要 1 毫秒。但是之前没用“memoizer”时,计算第 15 位时需要把它前面的第 1,2,3........,14 位全部重新计算一遍,这也是为什么需要 259 毫秒的原因。大家可以思考一下自己的项目中什么地方比较适合应用“aop.memoizer()”。这种用“空间”换“时间”的方式来提高效率,有时候会给我们的项目带来意想不到的效果。

“aop.memoizer()”还可以接受一个函数作传入参数,用于计算一个 key 值来做 index,作用有点类似计算 hashCode 值,这里不做过多介绍,有兴趣的同学可以深入研究一下。

结束语

这篇文章介绍了 Dojo 开发中关于面向方面编程(“dojox.aspect”)的一些方法,基于 Dojo 的实例源代码,阐述了 Dojo 的 AOP 组件的很多基本功能和高级功能。介绍了如何自定义 tracer 来切入函数内部,也介绍了 Dojo 的 AOP 组件自带的一些 tracer。基于这些 tracer,我们可以实现我们 web 开发中的 AOP 特性。关于这些 Dojo 的 AOP 的特性,我们可以在开发过程中稍微关注一下,也许会带来意想不到的效果。


相关主题

  • Dojo 校园文档主页:Dojo 中控件的比较完全的 API 文档主页,包括 Dojo,Dijit,Dojox 等等。
  • Dojo 官方文档主页:Dojo 官方的很多支持 Ajax 应用程序开发的组件的文档。
  • developerWorks Web development 专区:通过专门关于 Web 技术的文章和教程,扩展您在网站开发方面的技能。
  • developerWorks Ajax 资源中心:这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。
  • Dojo 技术专题:本专题既有入门的基础知识内容,也有使用的开发技巧,同时您还可以了解到很多高级的应用技巧。
  • developerWorks Web 2.0 资源中心,这是有关 Web 2.0 相关信息的一站式中心,包括大量 Web 2.0 技术文章、教程、下载和相关技术资源。您还可以通过 Web 2.0 新手入门 栏目,迅速了解 Web 2.0 的相关概念。
  • 查看 HTML5 专题,了解更多和 HTML5 相关的知识和动向。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=800407
ArticleTitle=深入了解 Dojo 的面向方面编程
publish-date=03052012