装饰者模式

在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活, 还会带来许多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变;另一方面,继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的, 在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式

故事背景

假设我们在编写一个飞机大战的游戏,随着经验值的增加,我们操作的飞机对象可以升级成更厉害的飞机,一开始这些飞机只能发射普通的子弹,升到第二级时可以发射导弹,升到第三级时可以发射原子弹。

传统面向对象实现

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
let Plane = function () {};
Plane.prototype.fire = function () {
console.log("发射普通子弹");
};

// 导弹类
let MissileDecorator = function (plane) {
this.plane = plane;
};
MissileDecorator.prototype.fire = function () {
this.plane.fire();
console.log("发射导弹");
};

// 原子弹类
let AtomDecorator = function (plane) {
this.plane = plane;
};
AtomDecorator.prototype.fire = function () {
this.plane.fire();
console.log("发射原子弹");
};

let plane = new Plane();
plane = new MissileDecorator(plane);
plane = new AtomDecorator(plane);
plane.fire();

装饰者对象和它所装饰的对象拥有一致的接口,通过传入被装饰对象,调用其对应方法,并添加职责形成聚合对象。

动态语言实现

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
let plane = {
fire: function () {
console.log("发射普通子弹");
},
};

let missileDecorator = function () {
console.log("发射导弹");
};

let atomDecorator = function () {
console.log("发射原子弹");
};

let fire1 = plane.fire;
plane.fire = function () {
fire1();
missileDecorator();
};

let fire2 = plane.fire;
plane.fire = function () {
fire2();
atomDecorator();
};
plane.fire();

因为js可以直接修改对象或者对象的某个方法,那么可以通过一个变量a暂存被装饰函数,然后替换旧方法,在新方法内通过a调用旧方法。

但是直接替换有this指向问题

AOP实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.before = function (beforefn) {
let _self = this; // 保存原函数的引用
return function () { // 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
return _self.apply(this, arguments); // 执行原函数并返回原函数的执行结果, // 并且保证 this 不被劫持
};
};
Function.prototype.after = function (afterfn) {
let _self = this;
return function () {
let ret = _self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
};
};

缺点

  • 通过AOP实际上放回的是新函数,在原函数上保存的属性则会丢失
  • 叠加了作用域,如果装饰的链条过长,性能上会受到影响

与代理模式的区别

代理模式的目的:当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。

装饰者模式的作用:为对象动态的加入行为。

换句话说,代理模式的关系一开始就可以被确定,而装饰者模式用于一开始不能确定对象的全部功能时

参考

《javascript设计模式与开发实践》

作者

Liang

发布于

2021-02-09

更新于

2021-12-25

许可协议


评论