Netherspite

在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面编程(Aspect Oriented Program, AOP)。

面向切面编程

面向对象编程(OOP)的特点是封装、继承与多态。封装意味着对象内部封装着实现不同功能的方法,这通常被称为职责分配,这使得类可重用。假设你要实现一个与业务无关的代码,比如日志打印、统计数据、异常处理等,最简单粗暴的方法就是直接修改原函数,但这种方法违反了开放-封闭原则,同时会增加代码的重复率和业务代码的耦合性。

面向切面编程的主要作用就是将这些业务无关的功能抽离出来,在运行时,动态地将代码切入到类或方法的指定位置中。一般地,我们称这部分代码叫切面,切入的类或方法叫切入点

装饰器模式

上文提到的这种给对象动态地增加职责的设计模式称为装饰器模式(也称装饰者模式)。由于语言的特性,在JavaScript中改变对象相当容易,并不需要类来实现装饰器模式。

在实际开发中,当我们想给window绑定onload事件,但是又不清楚这个事件是否被他人绑定过,为了避免覆盖之前onload中的行为,一般做法是保存原先的函数:

window.onload = function(){
alert(1);
}
let _onload = window.onload || function(){}
window.onload = function(){
_onload();
alert(2);
}

这个代码符合开放-封闭原则,但存在两个问题:

  • 必须维护中间变量_onload,当需要装饰的函数变多,或是更多的人要装饰这个函数,则中间变量的数量便会变得特别多。
  • this指向可能存在问题,上述例子中,_onload函数调用时,原this和现在的内部this都指向window,所以不会存在问题,但是对于其他方法,比如_getElementById = document.getElementById,这种情况下在装饰函数内部直接调用就会报错,必须手动绑定this的指向_getElementById = document.getElementById.bind(document)_getElementById.apply(document, arguments),但是这样做非常不方便。

AOP思想

给出before和after这两个切面函数。

Function.prototype.before = function(func){
let _self = this;
return function(){
func.apply(this, arguments);
return _self.apply(this, arguments);
}
}

Function.prototype.after = function(func){
let _self = this;
return function(){
let ret = _self.apply(this, arguments);
func.apply(this, arguments);
return ret;
}
}

这两个函数接受新添加的函数为参数,并保存原函数然后返回一个新函数,这个函数让新添加的函数在原函数的之前或之后执行,这两个方法保证了this在装饰之后不会被劫持。将切面函数运用到onload函数的装饰上:

window.onload = function(){
alert(1);
}
window.onload = (window.onload || function(){}).after(function(){
alter(2);
}).after(function(){
alter(3);
})

当然,上述切面函数污染了原型,可以定义将原函数和新函数都作为参数传入的切面函数beforeafter来解决污染问题。

装饰器模式与代理模式

代理模式和装饰器模式的结构非常相似,都描述了怎样为对象提供一定程度上的间接引用,实现部分都保留了对另外一个对象的引用,并向那个对象发送请求。

代理模式和装饰者模式最重要的区别就在于他们的意图和设计目的。

  • 代理模式的目的是当直接访问本体不方便或不符合需要时,为本体提供一个替代者,而装饰器模式的目的是为对象动态地加入行为。
  • 代理模式的本体定义了关键功能,而代理会根据条件提供或拒绝对它的访问,或者在访问之前或之后进行一些额外的处理,他的功能还是原来的功能。而装饰器模式是实实在在的为对象增加新的职责和行为。
  • 代理模式通常只有代理-本体这一层关系,而装饰器模式经常会形成一条长长的装饰链。

装饰器模式的应用

TypeScript中的装饰器

装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。

装饰器使用@expression的形式,其中expression求值后必须为在运行时调用的函数,被装饰的声明信息作为参数传入。

装饰器工厂

装饰器工厂返回一个表达式,以供装饰器在运行时调用:

function color(value: string){	//装饰器工厂
return function (target){ //装饰器
}
}

装饰器组合

多个装饰器可以同时应用到一个声明上:

@f @g x		//单行

@f
@g
x //多行

多个装饰器会由上至下依次对装饰器表达式求值,求值的结果会被当做函数,由下至上依次调用。如:

function f() {
console.log("f(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}
class C {
@f()
@g()
method() {}
}
//打印结果:f(): evaluated
//g(): evaluated
//g(): called
//f(): called

而类中不同声明上的装饰器按以下规定的顺序应用:

  1. 参数装饰器->方法装饰器->访问符装饰器/属性装饰器,应用到每个实例成员,然后应用到每个静态成员
  2. 参数装饰器应用到构造函数
  3. 类装饰器应用到类

类装饰器

类装饰器在类声明之前被声明。类装饰器应用于类构造函数,可以用来监视、修改或替换类的定义。类装饰器不能用在声明文件.d.ts)和任何外部上下文中declare)的类。

类装饰器表达式会在运行时被当做函数,类的构造函数作为其唯一的参数。如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
great() {
return `hello, ${this.greeting}`;
}
}
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

@sealed被执行时,它将密封此类的构造函数和原型。下面是一个重载构造函数的例子:

function classDecorator<T extends {new(...args:any[]): {}}>(constructor:T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}

@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}

方法装饰器

方法装饰器不能用在声明文件、重载或者任何外部上下文中。

方法装饰器的参数有三个:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. 成员的名字
  3. 成员的属性描述符

方法装饰器返回的值会被用作方法的属性描述符

class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
great() {
return "Hello," + this.greeting;
}
}
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
}
}

当装饰器被调用时,它会修改属性描述符的enumerable属性。

访问器装饰器

访问器装饰器应用于访问器的属性描述符。TypeScript不允许同时装饰一个成员的getset访问器。传入的参数和方法装饰器一致,返回情况也一致。

class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {return this._x;}
@configurable(false)
get y() {return this._y;}
}
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
}
}

属性装饰器

属性装饰器传入的参数是访问器装饰器传入参数的前两个。可以用其记录这个属性的元数据。

import 'reflect-metadata'
class Greeter {
@format("hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace('%s', this.greeting);
}
}
const formatMetadataKey = Symbol('format');
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadaKey, target, propertyKey);
}

当装饰器被调用时,它添加一条这个属性的元数据,当getFormat被调用时,它读取格式的元数据。

参数装饰器

参数装饰器应用于类构造函数方法声明。 参数装饰器不能用在声明文件、重载或其它外部上下文里。

参数装饰器传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引

参数装饰器只能用来监视一个方法的参数是否被传入。

参数装饰器的返回值会被忽略。

import "reflect-metadata"
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
}
}

@required装饰器添加了元数据实体把参数标记为必需的。 @validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。


参考文章

  1. 什么是面向切面编程AOP?
  2. 用AOP来让你的JS代码变得更有可维护性吧
  3. 曾探,Javascript设计模式与开发实践,中国工信出版集团,人民邮电出版社
  4. 使用 TypeScript 装饰器装饰你的代码
  5. TypeScript官方文档:装饰器

 评论