JavaScript设计模式与开发实践(十一)装饰器模式

装饰器模式

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

适用场景

我们可以在以下几种场景中考虑使用装饰器模式

  • 在不影响其它对象的情况下,以动态、透明的方式给单个对象添加新的行为或特征
  • 处理可以撤销的行为或特征(即行为或特征是临时属性)
  • 当使用子类扩展的方式不切实际的时候,可考虑使用装饰器模式

例如在LOL中每个英雄都有血条、等级,但是不同英雄的技能跟外观不同,不可能每个英雄都去实现一个子类,这时候就可以采用装饰器模式。

我们可以自由的向对象中注入行为,而且这些行为是可控的。

实现

Java

适配器模式由以下四种角色组成:

  • 抽象组件角色(Component): 定义可以动态添加任务的对象的接口
  • 具体组件角色(ConcreteComponent):定义一个要被装饰器装饰的对象,即 Component 的具体实现
  • 抽象装饰器(Decorator): 维护对象和其子类的引用
  • 具体装饰器角色(ConcreteDecorator):向对象添加新的功能或行为特征

例如我们实现一个飞机类,随着等级的增加他可以使用不同的武器。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 抽象组件类
*/
abstract class APlane {
public abstract void fire();
}

/**
* 具体组件类
*/
class Plane extends APlane {
@Override
public void fire() {
System.out.println("firing ordinary bullets");
}
}

/**
* 抽象装饰器类
*/
abstract class ADecorator extends APlane {
protected APlane plane;

public ADecorator(APlane plane) {
this.plane = plane;
}

@Override
public void fire() {
plane.fire();
}
}

/**
* 具体装饰器类
*/

class MissileDecorator extends ADecorator {
public MissileDecorator(APlane plane) {
super(plane);
}

public void up() {
System.out.println("firing Missile");
}

@Override
public void fire() {
up();
super.fire();
}
}

class Nbombecorator extends ADecorator {
public Nbombecorator(APlane plane) {
super(plane);
}

public void down() {
System.out.println("firing N-Bomb");
}

@Override
public void fire() {
super.fire();
down();
}
}

/**
* test
*/
public class test {
public static void main(String[] args) {
APlane aPlane = new Plane();
MissileDecorator decorator = new MissileDecorator(aPlane);
Nbombecorator downDecorator = new Nbombecorator(decorator);
downDecorator.fire();
}
}

JavaScript

而在JavaScript中不需要实现各种抽象类和装饰器类,更多的是采用切面编程的思想实现。

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
class Plane {
fire() {
console.log('firing ordinary bullets');
}
}

Function.prototype.before = function (beforefn) {
const _this = this
return function () {
beforefn.apply(this, arguments)
return _this.apply(this, arguments)
}
}
Function.prototype.after = function (afterfn) {
const _this = this;
return function () {
const res = _this.apply(this, arguments);
afterfn.apply(this, arguments);
return res;
}
};

const plane = new Plane()

const decorator = plane.fire.before(() => {
console.log('firing Missile');
}).after(() => {
console.log('firing N-Bomb');
})

decorator()

AOP的其他应用

动态改变函数参数

假设我们有这样一个函数ajax,负责发送网络请求给后端。在一般情况下不需要传递token,但是在某些情况下需要将token字段传递给后端。

我们不想过多修改ajax内部的代码,因为我们想要保证ajax函数的纯净。那我们就可以在 before中动态注入参数。因为 before函数将 ·arguments·传递给了原来的函数,因此可以在 before中动态修改参数。

1
2
3
4
5
6
7
8
function ajax(type, url, param) {
console.log(param);
// 发送 ajax 请求的代码略
};
ajax = ajax.before((type, url, param) => {
param.token = '注入token'
})
ajax('get', 'http:// xxx.com/userinfo', { name: 'sven' });

插件式的表单验证

我们在编写网页表单,需要判断输入项是否符合要求,比如用户名和密码是否为空:

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
<html>

<body>
用户名:<input id="username" type="text" />
密码: <input id="password" type="password" />
<input id="submitBtn" type="button" value="提交">
</body>
<script>
const username = document.getElementById('username'),
password = document.getElementById('password'),
submitBtn = document.getElementById('submitBtn');
const formSubmit = function () {
if (username.value === '') {
return alert('用户名不能为空');
}
if (password.value === '') {
return alert('密码不能为空');
}
const param = {
username: username.value,
password: password.value
}
// ajax('http:// xxx.com/login', param);
}
submitBtn.onclick = function () {
formSubmit();
}
</script>

</html>

如果验证项频繁改动,我们就需要频繁修改formSubmit函数的代码,而且校验逻辑和提交逻辑也耦合在了一起。因此我们需要将二者分离开来,在执行真正提交代码逻辑之前,执行校验逻辑。这里的validata函数也可以通过策略模式进行优化。

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
40
41
42
43
44
45
46
47
48
49
Function.prototype.before = function (beforefn) {
const _this = this
return function () {
// 将before逻辑做了简单的修改
const { isvaild,message } = beforefn.apply(this, arguments)
if (isvaild) {
return _this.apply(this, arguments)
}else{
alert(message)
}
}
}

const username = document.getElementById('username'),
password = document.getElementById('password'),
submitBtn = document.getElementById('submitBtn');

function validata() {
if (username.value === '') {
return {
isvaild: false,
message: '用户名不能为空'
};
}
if (password.value === '') {
return {
isvaild: false,
message: '密码不能为空'
};
}
return {
isvaild: true
}
}

let formSubmit = function () {
const param = {
username: username.value,
password: password.value
}
console.log(param);
// ajax('http:// xxx.com/login', param);
}
formSubmit = formSubmit.before(() => {
return validata()
})
submitBtn.onclick = function () {
formSubmit();
}

优化的validata函数(参考JavaScript设计模式与开发实践(二)策略模式):

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const strategies = {
isNonEmpty: function (value, errorMsg) { // 不为空
if (value === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) { // 限制最小长度
if (value.length < length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) { // 手机号码格式
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};

var Validator = function () {
this.cache = []; // 保存校验规则
};

Validator.prototype.add = function (dom, rule, errorMsg) {
var ary = rule.split(':'); // 把 strategy 和参数分开
this.cache.push(function () { // 把校验的步骤用空函数包装起来,并且放入 cache
var strategy = ary.shift(); // 用户挑选的 strategy
ary.unshift(dom.value); // 把 input 的 value 添加进参数列表
ary.push(errorMsg); // 把 errorMsg 添加进参数列表
return strategies[strategy].apply(dom, ary);
});
};

Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
if (msg) { // 如果有确切的返回值,说明校验没有通过
return msg;
}
}
};

const validataFunc = function () {
const validator = new Validator(); // 创建一个 validator 对象
// 添加一些校验规则
validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空');
validator.add(registerForm.password, 'minLength:6', '密码长度不能少于 6 位');
validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');
const errorMsg = validator.start(); // 获得校验结果
return errorMsg; // 返回校验结果
}

const registerForm = document.getElementById('registerForm');

registerForm.onsubmit = function () {
const errorMsg = validataFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验
if (errorMsg) {
alert(errorMsg);
return false; // 阻止表单提交
}
};

JavaScript设计模式与开发实践(十一)装饰器模式
https://jing-jiu.github.io/jing-jiu/2023/01/16/notebooks/JavaScript设计模式与实践/设计模式(十一)/
作者
Jing-Jiu
发布于
2023年1月16日
许可协议