HXWfromDJTU/blog

Decorator - 从 AOP IOC descriptor、decorator mode 开始说

Opened this issue · 0 comments

Decorator

本文的主角是decorator,字面意思是装饰器。前端的同学大概都知道,它当前处于stage 2阶段(草案原文),可以用babel进行转码后进行使用。

使用过Angular 2或者Nest.js(或者Midway.js)的同学,一定对@Component@Inject@ViewChild@get()@post()@provide()不陌生。

了解设计模式的同学,大概还记得修饰器模式这东西,也许至今也还分不太清楚它和代理模式的差别。

但这次,我们想要追本溯源,从AOPIOCdescriptor这些东西说起,认识一下修饰器这个熟悉的陌生人。

"脑壳疼"de问题

在正文开始之前,我们先来一个需求,我们将陆续用不同阶段的思维去实现这个要求。

要求是:已知一个超过10几个人维护的代码,在不修改原函数的情况下,如何实现在每个函数执行后打印出指定内容的一行日志。

AOP

In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. It does so by adding additional behavior to existing code (an advice) without modifying the code itself, instead separately specifying which code is modified via a "pointcut" specification.

以上是维基百科对AOP的基本解释,主要着重于以下几点

  • 横切关注点与中体业务的进一步分离。
  • 现有代码的基础上,通过在切入点增加通知的方式实现。
  • 减少与主体业务没有这么密切的代码主题代码的入侵。

了解过Javascript 高阶函数的同学,可能见到过以下方式对👆题目需求的实现。

// 注意在执行 after的时候,原函数也会被一并执行
Function.prototype.after = function(afterfn){
    let _self =this;
    return function(){
        // 执行原方法
        let result = _self.apply(this,arguments);
        // 额外添加 after 函数的执行
        afterfn.apply(this,arguments);
        return result;
    }
}

实现过程本身不做过多解释,主要思维是将要添加的行为目标函数(主函数)包装到了一起,实现了不对原函数(主函数)入侵的预期,但写法上仍不够优雅

Spring AOP

在《Spring实战》第四章中提到了

散布于应用中多处的功能(日志、安全、事务管理等)被称为横切关注点。

把横切关注点与业务逻辑分离是AOP要解决的问题。

Spring中的AOP实现,给调用者的实际已经是经过加工的对象,开发者表面上调用的是Fun方法,但其实Spring为你做的是a + b + c --> Fun -->d + e + f 的调用过程。这里的abcdef都是函数动态的编入点,也就是定义中描述的pointcut

我们称这种切入方式为运行时织入

Spring AOP 的织入点
  • 前置通知(Before Advice)
  • 后置通知(After Advice)
  • 返回通知(After Return Advice
  • 环绕通知(Around Advice
  • 抛出异常后通知(After Throwing Advice)
// 基本实现代码
try{
    try{
        //@Before
        method.invoke(..);
    }finally{
        //@After
    }
    //@AfterReturning
} catch() {
    //@AfterThrowing
}

IOC 与 DI

控制反转,是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入,还有一种方式叫“依赖查找”。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。

以上是来自于维基百科对”控制反转“的基本解释。那么,我们如何实现一个控制反转呢,需要了解以下几个关键步骤。

创建 IOC 容器

所谓IOC容器,它的作用是:在应用初始化的时候自动处理对类的依赖,并且将类进行实例化,在需要的时候,使用者可以随时从容器中去除实力进行使用,而不必关心所使用的的实例何时引入、何时被创建。

const container = new Container()
绑定对象

有了容器,我们需要将”可能会被用到“的对象类,绑定到容器上去。

class Rabbit {}
class Wolf {}
class Tiger {}
// 绑定到容器上
container.bind('rabbit', Rabbit)
container.bind('wolf', Wolf)
container.bind('tiger', Tiger)

需要时取出实例

const rabbit = container.get('rabbit')
const wolf = container.get('wolf')
// 对象的创建有可能是一个异步的过程,所以这里采用 getAsync 表示经过异步调用才能够完成的实例获取
const tigger = container.getAsync('tiger')

Javascript 中的 decorator

tc 39 - decorator原文中,笔者没有找到总结性的描述语句。这里分别引用PythonTS中对decorator这一特性的描述。

A Python decorator is a function that takes another function, extending the behavior of the latter function without explicitly modifying it.
Python装饰器是一种 能拓展另一个函数行为而不明确地修改原函数 的函数。

Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members.
装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。

Python - decorator中可以看出,其着重在extendingwithout explicitly modifying it上,基本上沿用了AOP的设计**。

  • Javascript 种 decorator 主要的用法有以下两种: 类的装饰类方法的装饰
  • 修饰器不能够用于一般方法的修饰,因为方法的声明存在变量提升,修饰器方法和被修饰方法不知道哪一个会先被声明,导致装饰效果未知
  • 执行顺序:若一个方法有多个修饰器,会从外向内扫描,然后从内向外执行

类的装饰

function longHair(target) {
  target.isLongHair = true;
}

// 金发女郎,一般都是长头发
@longHair
class Blonde {
  // ...
}

Blonde.isLongHair // true

类方法的装饰

// 修改 descriptor.writable 使得对象不可被修改
function readonly(target, name, descriptor){
  descriptor.writable = false;
  return descriptor;
}

class Blonde {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

Decorator 实现原理

babel 转码看看

我们把上面这一堆东西扔到babel中试了一下,得到以下内容。

最后最显眼的地方出现了 Class 的 babel 实现,有兴趣的同学可以看这里的全部源码

// 对类本身的修饰
var Blonde = _decorate([longHair], function (_initialize) {
  var Blonde = function Blonde () {
    _classCallCheck(this, Blonde)

    _initialize(this)
  }

  return {
    F: Blonde,
    d: [{
      kind: 'field',
      decorators: [readonly],  // 对类方法的修饰
      key: 'abc',
      value: function value () {
        return 12
      }
    }]
  }
})
// 经过多次调用后......

for (var i = decorators.length - 1; i >= 0; i--) {
      // 函数本身
      var obj = this.fromClassDescriptor(elements)
      // decorators 逐个被执行,传入的参数是一个类的模拟对象 { kind: 'class', elements: elements.map(this.fromElementDescriptor, this) }
      var elementsAndFinisher = this.toClassDescriptor((0, decorators[i])(obj) || obj)
 }

decorator 只是个语法糖

从前面的转码实验看出 ,Decorator语法转为ES 5后,其实就是使用Object.defineProperty(target, name, description)进行的。

针对前面的例子,其实就是执行了。

let descriptor = {
  value: function(){console.log('hello boys~')},
  enumerable: false,
  configurable: true,
  writeable: true
};

// 此处也对应上述 babel 转码后展示的最后一行代码
descriptor = readonly(Blonde.prototype,'sayHello',descriptor)||descriptor;
Object.defineProperty(Blonde.prototype,'sayHello',descriptor);

descriptor

细心的你已经发现,decorator方法的参数 与 Object.defineProperty一模一样。这是因为Javascript中的decorator的设定就是后者的拦截器。

首先获取到原对象上的descriptor对象属性(非额外添加的那些),然后再执行修饰器自身,实现对原descriptor添加属性。类似于这样

function readonly () {
  let  descriptor  = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHello')
  Object.defineProperty(constructor.prototype, 'sayHello', {
     ...descriptor,  // 保留原来的对象
     writable: false, // 进行新的修改
  })
}

日志模块的构建

针对一开始的问题,我们也写一个Javascript版本的解决方案吧

const logger = type => {
  return (target, name, description) => {
        const originFun = description.value; // 取出原方法
        description.value = (...args) => {
            console.info('ready')
            let ret
            try {
               ret = originFun.apply(target, args) // 执行方法,并将this指向原函数
               console.log('excuted success')
            } catch (err) {
               console.log('excuted error')
            }
            return ret
        } 
  }
}

装饰模式 与 代理模式

看完了上面的内容,我们只要简单地回想一下代理模式的定义,就能轻松梳理出二者的异同点。

代理模式: 为其它对象提供一种代理以控制对这个对象的访问。

实际应用: 图片代理下载、缓存计算等

装饰模式:动态地给一个对象添加一些额外的职责。

实际应用:日志模块、模块鉴权等

区别有以下几点:

  • 从定义上来说
    • 代理模式仅仅是被代理方法的一层包装,对外透明。
    • 而装饰模式,却是从切面对目标方法进行功能拓展。
  • 从目标对象的性质来说。
    • 代理模式中的被代理方法在设定时就已经固定了。
    • 而装饰模式的目标方法需要在调用时动态传入才能确定。
  • 从调用者的感知程度来说。
    • 代理模式的基本原则就决定了调用者不需要额外学习代理方法的语法。
    • 而装饰模式,调用者需要知道装饰方法的传参规则,也需要主观地将装饰方法作用域某个方法/属性之上

更详细的例子,推荐参考这篇文章

日常应用

我们日常开发中,还会有一些功能用Decorator能够优美的实现,比如类型检查单位转换字段映射方法鉴权、代替部分注释`等。

midway 中的实现

midway.js 封装了许多装饰器,部分是用于实现IOC,如 @provide@inject

import { provide, inject } from 'midway' // 这里 midway 也是转发了

@provide()
export class FlowerService {

    @inject()
    flowerMobel;

    async getFlowerInfo () {
        return this.flowerModel.findByIds([12,28,31])
    }
}

// 封装了和 koa-router 所支持的多种请求方法相对应的修饰器
@get、@post、@del、@put、@patch、@options、@head、@all

总结

  • 代理模式制作包装转发,而装饰模式主要会对目标函数进行切面拓展。
  • 依赖注入只是IOC思维实现的一种表现,而装饰器只是依赖注入的一种实现手段。
  • Decorator是Javascript未来发展的趋势,也会是Javascript逐渐实现静态检查的�里程碑式的特性

参考文章

[1] �我们来聊聊装饰器 -by 讶羽

[2] JS 装饰器实战 -by 芋头

[3] ES6 教程 -by 阮一峰

[4] ES7 Decorator 装饰器 | 淘宝前端团队

[5] 什么是面向切面编程AOP? - 柳树的回答 - 知乎

[6] 什么是面向切面编程AOP? - 夏昊的回答 - 知乎

[7] tc39 - decorator 原文