装饰者模式

装饰者模式

下面这端代码是装饰者模式中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>
相关推荐
酷酷的阿云7 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落2 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
理想不理想v3 小时前
vue经典前端面试题
前端·javascript·vue.js
小阮的学习笔记3 小时前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜3 小时前
Vue实现登录功能
前端·javascript·vue.js