聊一聊 AOP in Javascript

5,301 阅读5分钟

前言

我们都知道 面向对象编程, 在 JavaScript 的世界中,我们至少或许常常听到 函数式编程,但是你之前听说过 面向切面编程吗 ?

就像 JavaScript 中的 OOP 和 FP 一样,你可以不费吹灰之力的将 AOP 与 FP 或 OOP 混合使用。 下面,我们来了解一下 AOP 主要是干什么的 以及它对 JavaScript 开发者有什么用处。

正文

1. 编程范式

编程范式(程序设计思想),一般指一种典型的编程风格。常见的编程范型有:函数式编程、指令式编程、过程式编程、面向对象编程 等等。

编程范型提供并决定了程序员对程序执行的看法。例如,在面向对象编程中,一般认为程序是一系列相互作用的对象,由于方法论的不同,面向对象编程范型又分为基于类编程和基于原型编程,而在函数式编程中一个程序会被看作是一个无状态的函数计算的序列。

2. AOP

面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。面向切面的程序设计思想也是面向切面软件开发的基础。切面的概念源于对面向对象的程序设计和计算反射的融合,但并不只限于此,它还可以用来改进传统的函数。与切面相关的编程概念还包括元对象协议、主题(Subject)、混入(Mixin)和委托(Delegate)。

面向切面编程为我们提供了一种将代码注入现有函数或对象的方法,而无需修改目标逻辑。

注入的代码虽然不是必需的,但在具有横切关注点的,例如添加日志记录功能、调试元数据、性能统计、安全控制、事务处理、异常处理或不那么通用的功能,可以在不影响原始代码的情况下注入额外的行为。把它们抽离出来,用“动态”插入的方式嵌到各业务逻辑中。业务模块可以变得比较干净,不受污染,同时这些功能点能够得到很好的复用,给模块解耦。

举一个很好的例子,假设您已经编写了业务逻辑,但现在您意识到您没有日志记录代码。对此的正常方法是通过添加日志信息的函数将日志逻辑集中在一个新模块和函数中。

为了对上面的定义进行一些形式化,一般会涵盖 3 个有关 AOP 的概念:

  1. Aspects (What) 切面
  2. Advice (When) 通知
  3. Pointcut (Where) 切点

3. AOP 的简单实现

如何基于 AOP 实现一种注入方法来添加行为,用一个基本的例子可能更有助于理解。


/** Helping function used to get all methods of an object */

const getMethods = (obj) => Object.getOwnPropertyNames(Object.getPrototypeOf(obj)).filter(item => typeof obj[item] === 'function')

/** Replace the original method with a custom function that will call our aspect when the advice dictates */

function replaceMethod(target, methodName, aspect, advice) {
    const originalCode = target[methodName]
    target[methodName] = (...args) => {
        if(["before", "around"].includes(advice)) {
            aspect.apply(target, args)
        }
        const returnedValue = originalCode.apply(target, args)
        if(["after", "around"].includes(advice)) {
            aspect.apply(target, args)
        }
        if("afterReturning" == advice) {
            return aspect.apply(target, [returnedValue])
        } else {
            return returnedValue
        }
    }
}

  // Main method exported: inject the aspect on our target when and where we need to
  
export const inject = function(target, aspect, advice, pointcut, method = null) {
        if(pointcut == "method") {
            if(method != null) {
                replaceMethod(target, method, aspect, advice)    
            } else {
                throw new Error("Tryin to add an aspect to a method, but no method specified")
            }
        }
        if(pointcut == "methods") {
            const methods = getMethods(target)
            methods.forEach( m => {
                replaceMethod(target, m, aspect, advice)
            })
        }
    }
}

请注意 replaceMethod 函数,这就是所有魔法发生的地方。这就是创建新函数的地方,也是我们决定何时调用方面以及如何处理其返回值的地方。

import AOP from "./aop.js"

class MyBussinessLogic {

    add(a, b) {
        console.log("Calling add")
        return a + b
    }

    concat(a, b) {
        console.log("Calling concat")
        return a + b
    }

    power(a, b) {
        console.log("Calling power")
        return a ** b
    }
}

const o = new MyBussinessLogic()

function loggingAspect(...args) {
    console.log("== Calling the logger function ==")
    console.log("Arguments received: " + args)
}

function printTypeOfReturnedValueAspect(value) {
    console.log("Returned type: " + typeof value)
}

AOP.inject(o, loggingAspect, "before", "methods")
AOP.inject(o, printTypeOfReturnedValueAspect, "afterReturning", "methods")

o.add(2,2)
o.concat("hello", "goodbye")
o.power(2, 3)

注:请慎重的在JS的中使用AOP!因为部分JS的方法是异步的。 必要时使用 async/await/Promise,以保证代码的顺序执行。

4. AOP 的好处

  1. 封装横切关注点

这意味着您可以更轻松地阅读和维护在整个项目中重复使用的代码。

  1. 灵活的逻辑

在注入切面时,围绕切入点实现的逻辑可以为你提供很大的灵活性。这反过来也可以帮助动态的打开和关闭逻辑。

  1. 跨项目复用

可以将切面视为组件,可以在任何地方运行小而解耦的代码段。如果您正确地编写切面,可以轻松地在不同的项目中共享它们。

5. 更多

在纯前端领域,其实我们经常会见到 AOP 思想的身影。

  1. Vue.js的数据绑定原理
Object.defineProperties() 或
Proxy 中的 setter / getter 的应用
  1. 各大前端框架中 hooks 的设计思想
new Vue({
  beforeUpdate() { //插入逻辑 },
  updated() { //插入逻辑 },
  ...
})

总结

并非一切都是完美的,一些人反对这种范式。它的主要问题是, 它的好处实际上是隐藏了逻辑和复杂性,但是,它可能也会引起副作用,当对此不太清楚时。

面向切面编程 AOP 是 OOP 的完美补充,特别是由于 JavaScript 的动态特性,我们可以很容易地实现它。它提供了强大的功能,能够模块化和解耦许多逻辑,你甚至可以与其他项目共享这些逻辑。