【译】JS中的面向切面编程

avatar
@智云健康

原文地址blog.bitsrc.io/aspect-orie…

译者:Gavin,未经授权禁止转载。

前言

写JS的同学应该都听说过面向对象程序设计(OOP)函数式编程(FP),对Java或nestjs有一些了解的同学应该也听说过面向切面的程序设计(AOP),但你知道通过原生JS怎么去实现一个最简的AOP功能吗?

幸运的是,就像JS中的OOP与FP一样,你可以毫不费力的将AOP与FP或OOP混用。

AOP简介

AOP为我们提供了一种不需要修改现有逻辑将代码注入到现有函数或对象中的方法。

AOP将逻辑分为不同的模块(即关注点,一段特定的逻辑功能)。几乎所有的编程思想都涉及代码功能的分类,将各个关注点封装成独立的抽象模块(如函数、过程、模块、类及方法等),后者又可供进一步实现、封装和重写。部分关注点“横切”程序代码中的数个模块,即在多个模块中都有出现,它们即被称为“横切关注点(Cross-cutting concerns, Horizontal concerns)”

AOP切面实现的3要素

  • Aspect:切面,用于封装要添加的行为
  • Advice:通知(增强),它指定了希望执行代码的常见时刻,如:beforeafteraroundwhen throwing
  • Pointcut:切入点,用于指明在具体需要进行方法增强的位置,比如:某个特殊的方法某个对象下的所有方法

基本实现

下面示例用于说明实现AOP的容易程度及其给代码带来的好处。

// 源文件地址:https://gist.github.com/deleteman/1b73da25feabf32db33c611674eb1ca6#file-aop-js

// aop.js
/** 用于获取对象上所有的方法 */
const getMethods = (obj) => Object.getOwnPropertyNames(Object.getPrototypeOf(obj)).filter(item => typeof obj[item] === 'function')

/** 用于在特殊时刻利用自定义函数替换原始方法 */
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
        }
    }
}

module.exports = {
    // 用于在需要的时刻和位置将注入切面功能
    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)
            })
        }
    } 
}
// 源文件地址:https://gist.github.com/deleteman/1efd939193400a569308945eb445e3cd#file-using-aop-js

// using-aop.js
const AOP = require("./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)

上面代码很简单,一个基本对象有3个方法。其中包含两个注入切面,一个用于记录接受到的属性,另一个用于分析其返回值并记录其类型。最终输出:

AOP的好处

  • 封装横切关注点:封装横切关注点有利于阅读和维护整个项目
  • 灵活的逻辑:当涉及到切面注入时,围绕通知(增强)和切入点实现的逻辑可以提供很大的灵活性
  • 跨项目重用:可以将切面视为组件,即可以在任何地方运行小而分离的代码片段,可以轻松在不同项目中共享使用

AOP的主要问题

  • 隐藏逻辑性与复杂性:用函数式编程的思维讲AOP具有副作用,它可以向现有的方法中添加不相关的行为,甚至可以替换原有方法的整个逻辑

总结

AOP提供了做任何想做事情的能力,如果缺乏良好的编程实践,可以导致非常大的代码混乱。简单总结就是权力越大,责任越大。如果想正确地使用AOP,那么首先必须得理解其核心思想和最佳实践。

AOP是OOP的完美补充,由于JS的动态特性,我们可以非常容易的实现它。同时它也提供了强大的功能,模块化和解耦大量逻辑的能力,甚至可以轻松实现跨项目的逻辑共享。

但如果不正确的使用,可能也会造成大量的代码混乱。

相关链接(译者注)