0%

AOP 面向切片编程

AOP 面向切片编程

AOP(面向切面编程) 的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计安全控制异常处理 等。把这些功能抽离出来之后,再通过 动态植入 的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的 纯净高内聚性 ,其次是可以很方便地复用日志统计等功能模块。

在 JavaScript中实现 AOP,都是指把一个函数动态植入到另外一个函数之中,通常是基于高阶函数实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 高阶函数,返回一个新的函数
Function.prototype.before = function (beforefn) {
var __self = this; // 保存原函数的引用
return function () { // 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments); // 执行新函数,修正 this
return __self.apply(this, arguments); // 执行原函数 并返回原函数执行结果
}
};

// 高阶函数,返回一个新的函数
Function.prototype.after = function (afterfn) {
var __self = this;
return function () {
var ret = __self.apply(this, arguments); // 执行原函数
afterfn.apply(this, arguments); // 执行新函数,修正 this
return ret; // 返回原函数执行结果
}
};
var func = function () {
console.log(2);
};
func = func.before(function () {
console.log(1);
}).after(function () {
console.log(3);
});
func();
// 1
// 2
// 3

数据统计上报

分离业务代码和数据统计 代码,无论在什么语言中,都是 AOP 的经典应用之一。在项目开发的结尾阶段难免要加上很多统计数据的代码,这些过程可能让我们被迫改动早已封装好的函数。

比如页面中有一个登录 button,点击这个 button会弹出登录浮层,与此同时要进行数据上报,来统计有多少用户点击了这个登录 button:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<button tag="login" id="button">点击打开登录浮层</button>
<script>
var showLogin = function(){
console.log('打开登录浮层');
log( this.getAttribute( 'tag' ) );
}
var log = function( tag ){
console.log('上报标签为: ' + tag);
// (new Image).src = 'http:// xxx.com/report?tag=' + tag; // 真正的上报代码略
}
document.getElementById( 'button' ).onclick = showLogin;
</script>
</html>

showLogin 函数里,既要负责打开登录浮层,又要负责数据上报,这是两个层面的功能,在此处却被耦合在一个函数里。使用 AOP 分离之后,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html>
<button tag="login" id="button">点击打开登录浮层</button>
<script>
Function.prototype.after = function (afterfn) {
var __self = this;
return function () {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
}
};
var showLogin = function () {
console.log('打开登录浮层');
}
var log = function () {
console.log('上报标签为: ' + this.getAttribute('tag'));
}
showLogin = showLogin.after(log); // 打开登录浮层之后上报数据
document.getElementById('button').onclick = showLogin;
</script>

</html>

用AOP动态改变函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
var ajax = function (type, url, param) {
console.log(param); // 发送 ajax 请求的代码略
};
var getToken = function () {
return 'Token';
}
ajax = ajax.before(function (type, url, param) {
param.Token = getToken();
});
ajax('get', 'http:// xxx.com/userinfo', { name: 'sven' });
// 从 ajax 函数打印的 log可以看到, Token 参数已经被附加到了 ajax 请求的参数中:
// { name: "sven", Token: "Token" }

beforefn 和原函数 __self 共用一组参数列表arguments ,当我们在 beforefn 的函数体内改变 arguments 的时候,原函数 __self 接收的参数列表自然也会变化。

明显可以看到,用 AOP 的方式给 ajax 函数动态装饰上 Token 参数,保证了 ajax 函数是一个相对纯净的函数,提高了 ajax 函数的可复用性,它在被迁往其他项目的时候,不需要做任何修改。

插件式的表单验证

在表单数据提交给后台之前,常常要做一些校验,比如登录的时候需要验证用户名和密码是否为空,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<html>

<body>
用户名:<input id="username" type="text" />
密码: <input id="password" type="password" />
<input id="submitBtn" type="button" value="提交">
</body>
<script>
var username = document.getElementById('username'),
password = document.getElementById('password'),
submitBtn = document.getElementById('submitBtn');
Function.prototype.before = function (beforefn) {
var __self = this;
return function () {
if (beforefn.apply(this, arguments) === false) {
// beforefn 返回 false 的情况直接 return,不再执行后面的原函数
// 可以做相关提示操作
return;
}
return __self.apply(this, arguments);
}
}

var validata = function () {
if (username.value === '') {
alert('用户名不能为空');
return false;
}
if (password.value === '') {
alert('密码不能为空');
return false;
}
}

var formSubmit = function () {
var param = {
username: username.value,
password: password.value
}
ajax('http:// xxx.com/login', param);
}
formSubmit = formSubmit.before(validata);
submitBtn.onclick = function () {
formSubmit();
}
</script>

</html>

校验输入和提交表单的代码完全分离开来,它们不再有任何耦合关系。

值得注意的是,因为函数通过 Function.prototype.before 或者 Function.prototype.after 被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失。代码如下:

1
2
3
4
5
6
7
8
var func = function () {
alert(1);
}
func.a = 'a';
func = func.after(function () {
alert(2);
});
alert(func.a); // 输出:undefined

另外,这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响。

参考

javascript 设计模式与开发实践

-------------本文结束感谢您的阅读-------------