dojo1.7功能介绍:面向方面编程(AOP)功能与原理

日前发布的dojo 1.7版本对其源码进行了很大的变更。在迈向 2.0版本之际,dojo提供了许多新的功能,也对许多已有的功能进行了修改,具体来说新版本更好地支持AMD规范、提供了新的事件处理系统(计划在 2.0版本中替换dojo.connect API)和DOM查询模块、更新对象存储相关接口(ObjectStore)等。在本文中我们将会介绍在dojo 1.7版本中新增的面向方面编程(AOP)功能以及其实现原理。

1. 简介

AOP即面向方面编程,是面向对象编程思想的延续。利用该思想剥离一些通用的业务,可以有效降低业务逻辑间的耦合度,提高程序的可重用性。随着 Java领域中Spring框架的流行,其倡导的AOP理念被更多的人所熟识(要注意的是Spring并不是该思想的首创者,但说Spring框架的流行 让更多人了解和学习了AOP思想并不为过)。因为Java为静态语言,所以在实现AOP功能时较为复杂,一般采取两种方式即动态代理和字节码生成技术(如 CGLib)来实现该功能。在JavaScript领域,因为以前模块化需求并不是很强烈,所以AOP的理念并没有被广泛引入进来。但是随着RIA技术的 发展,越来越多的业务逻辑在前台完成,JavaScript代码的组织和可维护性越发重要,正是在这样的背景下,出现了很多JavaScript模块化管 理的类库,而dojo也在这方面积极探索,新版本的dojo已经更好地支持AMD规范,并提供了面向方面编程的支持。

在面向方面编程功能推出之前,dojo可以通过使用connect方法来实现类似的功能。connect方法主要可以实现两类功能即为dom对象绑定事件和为已有的方法添加后置方法。已经有不少文章分析dojo的connect方法的使用原理,再加上dojo计划在将来版本中移除该API,所以在此不对这个方法进行更细致的分析了。

2. dojo的aspect模块使用简介

在dojo 1.7的版本中新增了aspect模块,该模块主要用来实现AOP的功能。借助于此项功能可以为某个对象的方法在运行时添加before、after或 around类型的增强(advice,即要执行的切面方法)。为了介绍此功能,我们先用dojo的类机制声明一个简单的类:

define("com.levinzhang.Person", ["dojo/_base/declare"], function (declare) {
    declare("com.levinzhang.Person", null, {
        name: null,
        age: null,
        constructor: function (name, age) {
            this.name = name;
            this.age = age;
        },
        getName: function () {
            return this.name;
        },
        getAge: function () {
            return this.age;
        },
        sayMyself: function () {
            alert("Person's name is " + this.getName() + "!");
        }
    });
})

这里声明类的方法,与我们在介绍类机制时略有不同,因为dojo从1.6版本开始支持AMD规范,通过define方法来声明模块及其依赖关系。有了类以后,我们需要创建一个实例,如下:

dojo.require("com.levinzhang.Person");
var person = new com.levinzhang.Person("levin",30);

现在我们要借助dojo的aspect模块为这个类的实例添加AOP功能。假设我们需要在sayMyself方法的调用前后分别添加对另一个方法的调用(即所谓的增强advice),示例代码如下:

var aspect = dojo.require("dojo.aspect"); //引入aspect模块
//声明在person 的sayMyself方法调用前要调用的方法
var signal = aspect.before(person, "sayMyself", function () {
    alert("调用了before");
});

//声明在person 的sayMyself方法调用后要调用的方法
aspect.after(person, "sayMyself", function () {
    alert("调用了after");
});

//此时调用sayMyself方法将会先后打印出:
//“调用了before”、“Person's name is levin!”、“调用了after”
//即按照before、目标方法、after的顺序执行
person.sayMyself();

在以上的代码片段中,我们使用了aspect的before和after方法实现了在目标方法前后添加advice。在调用before和 after方法后将会返回一个signal对象,这个对象记录了目标advice并提供了移除方法,如要移除上文添加的before advice,只需执行以下代码:

signal.remove(); //移除前面添加的beforeadvice
//此时调用sayMyself方法将会先后打印出:
// “Person's name is levin!”、“调用了after”
//即通过aspect.before添加的方法已经被移除
person.sayMyself();

除了before和after类型的advice,dojo还支持around类型的advice,在这种情况下,需要返回一个function,在这个function中可以添加任意的业务逻辑代码并调用目标方法,示例代码如下:

var signal = aspect.around(person, "sayMyself", function (original) {
    return function () {
        alert("before the original method");
        original.apply(person, arguments); //调用目标方法,即原始的sayMyself方法
        alert("after the original method");
    }
});

//此时调用sayMyself方法将会先后打印出:
//“before the original method”、“Person's name is levin!”、“after the original method”
person.sayMyself();

从上面的示例代码我们可以看到,around类型的advice会有更多对业务逻辑的控制权,原始的目标方法会以参数的形式传递进来,以便在advice中进行调用。

通过对以上几种类型advice使用方式的介绍,我们可以看到dojo的AOP功能在JavaScript中实现了AOP Alliance所倡导的advice类型。需要指出的是,每种类型的advice均可添加多个,dojo会按照添加的顺序依次执行。

3. 实现原理

了解了dojo AOP功能的基本语法后,让我们分析一下其实现原理。dojo aspect模块的实现在dojo/aspect.js文件中,整个文件的代码数在100行左右,因此其实现是相当简洁高效的。

通过var aspect = dojo.require(“dojo.aspect”);方法引入该模块时,会得到一个简单的JavaScript对象,我们调用 aspect.before、aspect.around、aspect.after时,均会调用该文件中定义的aspect方法所返回的 function。

define([], function () {
    ……
    return {
        before: aspect("before"),
        around: aspect("around"),
        after: aspect("after")
    };
});

现在我们看一下aspect方法的实现:

function aspect(type) {
    //对于不同类型的advice均返回此方法,只不过type参数会有所不同
    return function (target, methodName, advice, receiveArguments) {
        var existing = target[methodName],
            dispatcher;
        if (!existing || existing.target != target) {
            //经过AOP处理的方法均会被一个新的方法所替换,也就是这里的dispatcher
            dispatcher = target[methodName] = function () {
                // before advice
                var args = arguments;
                //得到第一个before类型的advice
                var before = dispatcher.before;
                while (before) {
                    //调用before类型的advice
                    args = before.advice.apply(this, args) || args;
                    //找到下一个before类型的advice
                    before = before.next;
                }
                //调用around类型的advice
                if (dispatcher.around) {
                    调用dispatcher.around的advice方法
                    var results = dispatcher.around.advice(this, args);
                }
                //得到第一个after类型的advice
                var after = dispatcher.after;
                while (after) {
                    //调用after类型的advice
                    results = after.receiveArguments ? after.advice.apply(this, args) || results :
                    after.advice.call(this, results);
                    //找到下一个after类型的advice
                    after = after.next;
                }
                return results;
            };

            if (existing) {
                //设置最初的around类型的advice,即调用目标方法
                dispatcher.around = {
                    advice: function (target, args) {
                        return existing.apply(target, args);
                    }
                };
            }
            dispatcher.target = target;
        }

        //对于不同类型的advice,通用advise方法来修改dispatcher,即对象的同名方法
        var results = advise((dispatcher || existing), type, advice, receiveArguments);
        advice = null;
        return results;
    };
}

我们可以看到,在第一次调用aspect方法时,原有的目标方法会被替换成dispatcher方法,而在这个方法中会按照内部的数据结构,依次调用各种类型的advice和最初的目标方法。而构建和调整这个内部数据结构是通过advise方法来实现的:

function advise(dispatcher, type, advice, receiveArguments) {

    var previous = dispatcher[type]; //得到指定类型的前一个advice
    var around = type == "around";
    var signal;

    if (around) {
        //对around类型的advice,只需调用advice方法,并将上一个advice(有可能即为//目标方法)作为参数传入即可
        var advised = advice(function () {
            return previous.advice(this, arguments);
        });

        //构建返回的对象,即aspect.around方法的返回值
        signal = {
            //移除方法
            remove: function () {
                signal.cancelled = true;
            },

            advice: function (target, args) {
                //即为真正执行的around方法
                return signal.cancelled ?
                previous.advice(target, args) : //取消,跳至下一个
                advised.apply(target, args); // 调用前面的advised方法
            }
        };
    } else {
        // 对于after或before类型的advice,构建移除方法
        signal = {
            remove: function () {
                var previous = signal.previous;
                var next = signal.next;
                if (!next && !previous) {
                    delete dispatcher[type];
                } else {
                    if (previous) {
                        previous.next = next;
                    } else {
                        dispatcher[type] = next;
                    }
                    if (next) {
                        next.previous = previous;
                    }
                }
            },
            advice: advice,
            receiveArguments: receiveArguments
        };
    }
    if (previous && !around) {
        if (type == "after") {
            //将新增的advice加到列表的尾部
            var next = previous;
            while (next) {
                //移到链表尾部
                previous = next;
                next = next.next;
            }
            previous.next = signal;
            signal.previous = previous;
        } else if (type == "before") {
            //将新增的advice添加到起始位置
            dispatcher[type] = signal;
            signal.next = previous;
            previous.previous = signal;
        }
    } else {
        // around类型的advice或第一个advice
        dispatcher[type] = signal;
    }
    return signal;
}

以上,我们分析了dojo的aspect模块的使用以及实现原理,尽管这种将静态语言编程风格移植到脚本语言中的做法能否被大家接受并广泛使用尚有待时间的检验,但这种尝试和实现方式还是很值得借鉴的。

参考资料

关于作者

张卫滨,关注企业级Java开发和RIA技术,个人博客:http://lengyun3566.iteye.com,微博:http://weibo.com/zhangweibin1981


感谢崔康对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

This entry was posted in ASP.NET. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s