装饰者模式
下面这端代码是装饰者模式中AOP编程的应用:
kotlin
const originalAction = endpoints[method].action;
const api = this;
endpoints[method].action = function _internalRouteActionHandler() {
const rocketchatRestApiEnd = metrics.rocketchatRestApi.startTimer({
method,
version,
...(prometheusAPIUserAgent && { user_agent: this.request.headers['user-agent'] }),
entrypoint: route.startsWith('method.call')
? decodeURIComponent(this.request._parsedUrl.pathname.slice(8))
: route,
});
this.requestIp = getRequestIP(this.request);
const startTime = Date.now();
const log = logger.logger.child({
method: this.request.method,
url: this.request.url,
userId: this.request.headers['x-user-id'],
userAgent: this.request.headers['user-agent'],
length: this.request.headers['content-length'],
host: this.request.headers.host,
referer: this.request.headers.referer,
remoteIP: this.requestIp,
...getRestPayload(this.request.body),
});
const objectForRateLimitMatch = {
IPAddr: this.requestIp,
route: `${this.request.route}${this.request.method.toLowerCase()}`,
};
let result;
const connection = {
id: Random.id(),
close() {},
token: this.token,
httpHeaders: this.request.headers,
clientAddress: this.requestIp,
};
try {
api.enforceRateLimit(objectForRateLimitMatch, this.request, this.response, this.userId);
if (
shouldVerifyPermissions &&
(!this.userId || !hasAllPermission(this.userId, _options.permissionsRequired))
) {
throw new Meteor.Error(
'error-unauthorized',
'User does not have the permissions required for this action',
{
permissions: _options.permissionsRequired,
},
);
}
const invocation = new DDPCommon.MethodInvocation({
connection,
isSimulation: false,
userId: this.userId,
});
Accounts._accountData[connection.id] = {
connection,
};
Accounts._setAccountData(connection.id, 'loginToken', this.token);
if (_options.twoFactorRequired) {
api.processTwoFactor({
userId: this.userId,
request: this.request,
invocation,
options: _options.twoFactorOptions,
connection,
});
}
result =
DDP._CurrentInvocation.withValue(invocation, () => originalAction.apply(this)) ||
API.v1.success();
log.http({
status: result.statusCode,
responseTime: Date.now() - startTime,
});
} catch (e) {
const apiMethod =
{
'error-too-many-requests': 'tooManyRequests',
'error-unauthorized': 'unauthorized',
}[e.error] || 'failure';
result = API.v1[apiMethod](
typeof e === 'string' ? e : e.message,
e.error,
process.env.TEST_MODE ? e.stack : undefined,
e,
);
log.http({
err: e,
status: result.statusCode,
responseTime: Date.now() - startTime,
});
} finally {
delete Accounts._accountData[connection.id];
}
rocketchatRestApiEnd({
status: result.statusCode,
});
return result;
};
面向切面编程
面向切面编程(Aspect-Oriented Programming)是一种编程范式,旨在将横切关注点(Cross-cutting Concerns)从核心业务逻辑中分离出来,提高系统的模块化程度和代码的可维护性。
横切关注点指的是那些散布在应用程序多个模块中的功能,例如:
- 日志记录: 记录程序执行过程信息。
- 性能监控: 检测程序的性能指标。
- 事务管理: 确保一系列操作要么全部成功,要么全部失败。
- 异常处理: 捕获和处理程序运行时出现的异常。
在传统的面向对象(OOP)编程中,这些横切关注点通常分散在各个模块中,导致代码重复,耦合度高,难以维护。AOP概念可以解决这个问题。
切面(切点+通知)是一个独立的模块,封装了关注点逻辑。它包含了切点和通知。
- 切点是指核心业务逻辑。
- 通知定义了切点前后要执行的操作,包括前置通知、后置通知、环绕通知等等。
通过AOP可以将关注点集中管理,实现业务逻辑和关注点的分离。当我们需要修改或添加横切关注点时,只需要修改切面即可,不需要修改核心业务逻辑代码。
场景
假设我们正在开发一个电商网站,其中有一个OrderService
类负责处理订单相关的业务逻辑。我们希望在每次下单的时候记录其日志,以便后期跟踪订单。
javascript
class OrderService {
placeOrder(order) {
console.log(`Placing order:${JSON.stringfy(order)}`);
// 处理下单逻辑
console.log(`Order placed successfully: ${order.id}`);
}
}
问题: 记录日志的代码和核心业务逻辑耦合在一起。如果需要修改日志格式或添加其他类型的格式就要修改plcaeOrder方法。违反了开放封闭原则。
AOP方式:
javascript
const Aspect = require('aspect.js');
class OrderService {
placeOrder(order) {
// 处理下单逻辑
}
}
// 定义日志记录切面
// loginAspect是切面
// placeOrder是切点
// 两个console.log()是关注点
// Aspect.around创建环绕通知,在目标方法之前前后都执行。
const loginAspect = Aspect.create(OrderService.prototype, 'placeOrder', function(meta) {
return Aspect.around(meta.method, function(origin, ...args){
console.log(`Placing order:${JSON.stringfy(args[0])}`);
const result = origin.apply(this, args);
console.log(`Order placed successfully: ${result}`);
})
})
AOP优点:
- 关注点分离:关注点独立于切点。
- 灵活扩展:可以方便地扩展代码,而无需修改核心业务逻辑。
装饰者模式
从功能而言,decorator能够很好地描述这个模式,但从结构上看,wrapper的说法更加贴切。装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递到所有的对象,每个对象都有处理这条请求的机会。
代码示例:
javascript
var Plane = function(){}
Plane.prototype.fire = function(){
console.log( '发射普通子弹' );
}
var MissileDecorator = function( plane ){
this.plane = plane;
}
MissileDecorator.prototype.fire = function(){
this.plane.fire();
console.log( '发射导弹' );
}
var AtomDecorator = function( plane ){
this.plane = plane;
}
AtomDecorator.prototype.fire = function(){
this.plane.fire();
console.log( '发射原子弹' );
}
这种动态给对象增加职责的方式并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口,当请求达到链中的某个操作时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以任意嵌套多个装饰者对象。
AOP+装饰者模式
AOP编程的思想是将行为划分为粒度较细的函数,随后通过织入将这些函数和BL层代码结合在一起。这有助于帮助我们编写一个松耦合和高复用的系统。
比如:页面中有一个登录的button,点击则个button会弹出登录浮层,与此同时要进行数据上报,统计多少用户点击了这个登录按钮。
xml
<html>
<button tag="login" id="button">点击打开登录浮层</button>
<script>
var showLogin = function(){
console.log("打开登录浮层");
log(this.getAttribute("tag"));
}
var log = function(tag){
console.log("上报标签为:" + tag);
}
document.getElementById("button").onclick = showLogin;
</script>
</html>
在showLogin函数里,既要打开登录浮层,又要数据上报,这是两个层面的功能,此处被耦合在一起。
使用AOP思想以后,代码如下:
xml
<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>