什么是装饰器模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。

装饰器模式的定义就比较直白啦,就是对我们现有的一个类去添加了一个新的功能,但是呢,新的功能并不会改变这个类原先的结构。那怎么做呢?所以我们就需要去添加一个新的类,通过这个新的类来去为原有的类增加功能,这个新的类就是一个装饰类,也就是装饰器,这样一种模式,就是装饰器模式。

举例说明

知道了装饰器模式的定义之后,我们来看举例说明,装饰器模式的例子也很贴近生活,就比如我们的手机壳。

在这里插入图片描述

像这种手机壳,他并没有改变我们手机原有的功能,比如打电话,听音乐什么的。但却为手机提供了新的功能,比如后面的指环,这就是提供的新的功能。这就是一个典型的装饰器模式在生活中的例子。

绘制UML类图

然后我们就根据这个手机壳的实例,来绘制UML类图。

在这里插入图片描述

我们看一下绘制出来的UML类图,首先是Client代表是我们,我们有一个手机Phone,然后有一个手机壳作为手机的装饰器,我们叫他Decorator,这个main方法是一个入口函数,可以理解为我们对手机进行了一个打电话的操作,或者是对手机进行了一个使用的操作都可以。

然后手机拥有一个call方法,表示这个手机具有打电话的功能,然后装饰器Decorator它持有了一个手机的引用,所以它也具备了一个打电话的功能, 但是这个功能我们要知道它是借助手机Phone来完成的, 这个一定注意, 装饰器本身并没有被装饰类的功能,它是因为持有了被装饰类的引用,所以才具备了被装饰类的功能,其实这个功能还是通过被装饰类来完成的。 然后我们的装饰器对手机提供了一个新的功能antiFall,借助antiFall我们的手机具备了防止坠落的功能。

梳理完UML类图的逻辑之后,我们来看一下上面的这些内容,我们如何通过代码去实现。

代码实现

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
class Phone {
call () {
console.log('打电话');
}
}

class Decorator {

constructor (phone) {
this.phone = phone;
}

call () {
this.antiFall();
this.phone.call();
}

antiFall () {
console.log('防止坠落');
}
}

class Client {
constructor () {
const phone = new Phone();
this.decorator = new Decorator(phone);
}

main () {
this.decorator.call();
}
}

const client = new Client();
client.main();

我们来分析一下上面的代码,首先Phone具有一个打电话的功能call,当我们执行call方法的时候,会打印打电话这三字,然后有一个装饰器类Decorator,它持有Phone的引用,借助PhoneDecorator也拥有的打电话的功能call,并且又提供了一个附加功能antiFall,当我们执行Decoratorcall方法的时候,它会调用Phonecall方法,完成打电话的功能,并会调用它自己的antiFall方法完成它本身防止坠落的功能。

最后我们通过Client来初始化了PhoneDecorator,并调用antiFall方法。最终打印的结果为:

1
2
防止坠落
打电话

这样我们就通过Decorator来为Phone装饰上了一个新的功能。

使用场景

关于装饰器的使用场景,其最好的解释就是ES7Decorator 提案了,关于这块内容,阮一峰老师早在这里就做了详细的解释,我们在这里就不在去做一遍重复了,不过如果我们想要去使用 修饰器语法@Decorator (我们后面会使用@Decorator来表示修饰器语法) 的话,那么还需要额外做一些操作,这一块内容阮一峰老师并没有说,那么我们就在这里把使用@Decorator的准备工作说一下,并且通过这个新的修饰器语法来把我们上面的项目进行一个改造,如果大家想要对@Decorator语法进行更深的了解,那么可以点击这里

因为@DecoratorES7提案所以对想在的浏览器来说,绝大多数并不兼容,所以我们就需要使用babel对我们的代码进行一个转义。那么我们先通过npm来安装一下babel,我们执行 npm install --save-dev @babel/core babel-cli babel-preset-es2015, 然后如果我们想要babel能够识别@Decorator我们还需要安装babel-plugin-transform-decorators-legacy,我们执行npm install --save-dev babel-plugin-transform-decorators-legacy,最终我们的package.json的配置如下:

1
2
3
4
5
6
7
8
{
"devDependencies": {
"@babel/core": "^7.1.6",
"babel-cli": "^6.26.0",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-preset-es2015": "^6.24.1"
}
}

然后我们创建一个.babelrc的文件,用以对babel完成基础配置,配置内容如下:

1
2
3
4
5
6
7
8
{
"presets": [
"es2015"
],
"plugins": [
"transform-decorators-legacy"
]
}

这些配置操作都完成之后,我们就是用@Decorator来重构一下我们的实例代码,我们在项目根目录下创建decorator.js,此时我们的项目目录如下:

1
2
3
4
5
6
- node_modules
- index.html
- .babelrc
- package-lock.json
- package.json
- decorator.js

然后我们来写一下decorator.js中的代码,使用@Decorator来重构我们的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class Phone {
@antiFall
call () {
console.log('打电话');
}
}

function antiFall () {
console.log('防止坠落');
}

let phone = new Phone();

phone.call();

在上面的代码中,我们就对call方法进行了一个装饰,装饰的方法就是antiFall,至于上面的代码什么意思,我就不再这里说了,我强烈推荐大家去看阮一峰老师关于ES7修饰器的文章。

然后我们可以通过npx babel decorator.js -o build.js 来把decorator.js编译成build.js,然后在index.html中引入build.js,代码的执行结果应该为:

1
2
防止坠落
打电话

总结

进入总结部分,装饰器我们会在什么情况下使用呢? 如果大家听了我的去看了阮一峰老师的文章,那么就应该直达了core-decorators这个第三方类库了哈,其实这个类库就是一个很经典的装饰器使用方式。

首先装饰器模式不会改变被装饰类的现有结构。
其次装饰器模式是对被装饰类的现有功能的一个升级。
最后装饰器模式可以对被装饰类进行提供额外的注释功能。