设计模式-装饰器模式(Decorator)

装饰器模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

JavaScript的装饰器提案历经一波三折,目前仍处于Stage 2阶段,而且在语法和实现上经历了较大的改版,距离正式成为ECMA语言标准尚需时日。在TypeScript愈发流行的今天,它已推出了这个实验性功能,一些框架如angular、nestjs都已经大量使用了装饰器。

介绍

装饰器是一种特殊类型的声明,它能够被附加到类声明,属性, 访问符,方法或方法参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器。

参考lib.es5.d.tsECMAScript APIs部分对于装饰器的定义:

1
2
3
4
5
6
7
8
9
// 1481行-1484行
// 类装饰器
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
// 属性装饰器
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
// 方法装饰器
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
// 参数装饰器
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

类装饰器

接口定义:

1
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

类装饰器表达式,由定义知道,传入1个参数:

  • target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

先看一个最简单的装饰器,普通装饰器,只有一个参数target,当把这个@helloWord装饰器作用在HelloWordClass类上,这个target参数传递的就是HelloWordClass类。

1
2
3
4
5
6
7
8
9
10
11
function helloWord(target: any) {
console.log('hello Word!');
}

@helloWord
class HelloWordClass {
constructor() {
console.log('我是构造函数')
}
name: string = 'mulianju';
}

执行结果:

1
'hello Word!'

类装饰器3种类型

  • 普通装饰器(无法传参)
  • 装饰器工厂(可传参)
  • 重载构造函数

普通装饰器(无法传参)

1
2
3
4
5
6
7
8
9
10
11
function helloWord(target: any) {
console.log('hello Word!');
}

@helloWord
class HelloWordClass {
constructor() {
console.log('我是构造函数')
}
name: string = 'mulianju';
}

装饰器工厂(可传参)

增加了一个静态变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function helloWord(isTest: boolean) {
return function(target: any) {
// 添加静态变量
target.isTestable = isTest;
}
}

@helloWord(false)
class HelloWordClass {
constructor() {
console.log('我是构造函数')
}
name: string = 'mulianju';
}
let p = new HelloWordClass();
console.log(HelloWordClass.isTestable);

重载构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function helloWord(target: any) {
return class extends target {
sayHello(){
console.log("Hello")
}
}
}

@helloWord
class HelloWordClass {
constructor() {
console.log('我是构造函数')
}
name: string = 'mulianju';
}

属性装饰器

接口定义:

1
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

属性装饰器表达式会在运行时当作函数被调用,由定义知道,传入2个参数:

  • target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey —— 属性的名称。
  • 没有返回值。

按照上面的接口形式,定义了一个defaultValue()装饰器方法,就算是用private也是能生效的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function defaultValue(value: string) {
return function (target: any, propertyName: string) {
target[propertyName] = value;
}
}

class HelloWordClass {
constructor() {
console.log('我是构造函数')
}
@defaultValue('mulianju')
private name: string | undefined;
}
let p = new HelloWordClass();
console.log(p.name);

输出结果:

1
2
我是构造函数
mulianju // 这里打印出设置的默认值

通过属性描述来修改属性值

对于属性的装饰器,是没有返回descriptor的,并且装饰器函数的返回值也会被忽略掉。还可以通过自己获取descriptor,并进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function defaultValue(value: string) {
return function (target: any, propertyName: string) {
let descriptor = Object.getOwnPropertyDescriptor(target, propertyName);

Object.defineProperty(target, propertyName, {
...descriptor,
value
})
}
}

class HelloWordClass {
constructor() {
console.log('我是构造函数')
}
@defaultValue('mulianju')
private name: string | undefined;
}
let p = new HelloWordClass();
console.log(p.name);

输出结果:

1
2
我是构造函数
mulianju // 这里打印出设置的默认值

这种方式通过Object.getOwnPropertyDescriptor和Object.defineProperty方法对属性进行定义。这种方式适用面更广,可以针对属性描述进行修改。

方法装饰器

接口定义:

1
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器接受三个参数:

  • target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey —— 属性的名称。
  • descriptor —— 方法的属性描述符。

返回属性描述符或者没有返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function logFunc(params: string) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
// target === HelloWordClass.prototype
// propertyName === "sayHello"
// propertyDesciptor === Object.getOwnPropertyDescriptor(HelloWordClass.prototype, "sayHello")

console.log(params);
// 被装饰的函数
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
let start = new Date().valueOf();
// 将 sayHello 的参数列表转换为字符串
args = args.map(arg => String(arg));
console.log('参数args = ' + args);
try {
// // 调用 sayHello() 并获取其返回值
return method.apply(this, args)
} finally {
let end = new Date().valueOf();
console.log(`start: ${start} end: ${end} consume: ${end - start}`)
}
};
return descriptor;
}
}

class HelloWordClass {
constructor() {
console.log('我是构造函数')
}
private nameVar: string | undefined;

@logFunc('log装饰器')
sayHello(name: string) {
console.log(name + ' sayHello');
}
}
let pHello = new HelloWordClass();
pHello.sayHello('mulianju');

输出结果:

1
2
3
4
5
log装饰器
我是构造函数
参数args = mulianju
mulianju sayHello
start: 1574331292433 end: 1574331292434 consume: 1

方法参数装饰器

接口定义:

1
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

方法参数装饰器会接收三个参数:

  • target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey —— 属性的名称。
  • parameterIndex —— 参数数组中的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function logParameter(target: any, propertyName: string, index: number) {
// 为相应方法生成元数据键,以储存被装饰的参数的位置
const metadataKey = `log_${propertyName}_parameters`;
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
} else {
target[metadataKey] = [index];
}
}


class HelloWordClass {
constructor() {
console.log('我是构造函数')
}
private nameVar: string | undefined;

sayHello(@logParameter name: string) {
console.log(name + ' sayHello');
}
}
let pHello = new HelloWordClass();
pHello.sayHello('mulianju');

访问器装饰器

访问器就是添加有get、set前缀的函数,用于控制属性的赋值及取值操作。
访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。

访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 成员的名字。
  • 成员的属性描述符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function enumerable(value: boolean) {
return function (
target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}

class HelloWordClass {
constructor() {
console.log('我是构造函数')
}

private _name: string = 'mulianju';
private _age: number = 10;

@enumerable(true)
get name() {
return this._name;
}

set name(name: string) {
this._name = name;
}

@enumerable(false)
get age() {
return this._age;
}

set age(age: number) {
this._age = name;
}
}
let pHello = new HelloWordClass();
for (let prop in pHello) {
console.log(`property = ${prop}`);
}

enumerable属性描述符:
当且仅当该属性的enumerabletrue时,该属性才能够出现在对象的枚举属性中。通过Object.defineProperty()创建属性,enumerable默认为false

如果一个属性的enumerable为false,通过对象还是能访问的到这个属性,但下面三个操作不会取到该属性。

  • for…in循环
  • Object.keys方法
  • JSON.stringify方法

我们定义了两个访问器 name 和 age,并通过装饰器设置是否将其列入清单,据此决定对象的行为。name 将列入清单,而 age 不会。

所以控制台输出结果:

1
2
3
4
5
我是构造函数 
property = _name
property = _age
property = name
// 少了一个属性age

装饰器执行顺序

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

  1. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

注意:TypeScript 不允许同时装饰单一成员的 get 和 set 访问器。这是因为装饰器可以应用于属性描述符,属性描述符结合了 get 和 set 访问器,而不是分别应用于每项声明。

装饰器模式的优缺点

优点

  • 动态扩展类功能,比类继承灵活,且对客户端透明;
  • 继承关系的一种替代方案。相比与类继承的父子关系,装饰模式 更像是一种-组合关系(is-a);
  • 可以对同一个被装饰对象进行多次装饰,创建出不同行为的复合功能。

缺点

  • 多层装饰比较复杂(灵活的同时会带来复杂性的增加);
  • 装饰嵌套过多,会产生过多小对象(每个装饰层都创建一个相应的对象);
  • 装饰嵌套过多,易于出错,且调试排查比较麻烦(需要一层一层对装饰器进行排查,以确定是哪一个装饰层出错)。

参考资料

本文永久链接: https://www.mulianju.com/decorator/