Author 王平安
E-mail pingan8787@qq.com
博 客 www.pingan8787.com
微 信 pingan8787
每日文章推荐 https://github.com/pingan8787/Leo_Reading/issues
微信公众号 前端自习课

四、装饰者模式(Decorator Pattern)

1.概念介绍

装饰者模式(Decorator Pattern):在不改变原类和继承情况下,动态添加功能到对象中,通过包装一个对象实现一个新的具有原对象相同接口的新对象。

装饰者模式有以下特点:

  1. 添加功能时不改变原对象结构
  2. 装饰对象和原对象提供的接口相同,方便按照源对象的接口来使用装饰对象。
  3. 装饰对象中包含原对象的引用。即装饰对象是真正的原对象包装后的对象。

实际上,装饰着模式的一个比较方便的特征在于其预期行为的可定制和可配置特性。从只有基本功能的普通对象开始,不断增强对象的一些功能,并按照顺序进行装饰。

2.优缺点和应用场景

2.1优点

  • 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

2.2缺点

  • 多层装饰比较复杂。

2.3应用场景

  • 扩展一个类的功能。
  • 动态增加功能,动态撤销。

3.基本案例

我们这里实现一个基本对象sale,可以通过sale对象获取不同项目的价格,并通过调用sale.getPrice()方法返回对应价格。并且在不同情况下,用额外的功能来装饰它,会得到不同情况下的价格。

3.1创建对象

这里我们假设客户需要支付国家税和省级税。按照装饰者模式,我们就需要使用国家税和省级税两个装饰者来装饰这个sale对象,然后在对使用价格格式化功能的装饰者装饰。实际看起来是这样:

1
2
3
4
5
let sale = new Sale(100);
sale = sale.decorate('country');
sale = sale.decorate('privince');
sale = sale.decorate('money');
sale.getPrice();

使用装饰者模式后,每个装饰都非常灵活,主要根据其装饰者顺序,于是如果客户不需要上缴国家税,代码就可以这么实现:

1
2
3
4
let sale = new Sale(100);
sale = sale.decorate('privince');
sale = sale.decorate('money');
sale.getPrice();

3.2实现对象

接下来我们需要考虑的是如何实现Sale对象了。

实现装饰者模式的其中一个方法是使得每个装饰者成为一个对象,并且该对象包含了应该被重载的方法。每个装饰者实际上继承了目前已经被前一个装饰者进行装饰后的对象,每个装饰方法在uber(继承的对象)上调用同样的方法并获取值,此外还继续执行一些操作。

uber关键字类似Java的super,它可以让某个方法调用父类的方法,uber属性指向父类原型。

即:当我们调用sale.getPrice()方法时,会调用money装饰者的方法,然后每个装饰方法都会先调用父对象的方法,因此一直往上调用,直到开始的Sale构造函数实现的未被装饰的getPrice()方法。理解如下图:

装饰者模式1

我们这里可以先实现构造函数Sale()和原型方法getPrice()

1
2
3
4
5
6
function Sale (price){
this.price = price || 100;
}
Sale.prototype.getPrice = function (){
return this.price;
}

并且装饰者对象都将以构造函数的属性来实现:

1
Sale.decorators = {};

接下来实现country这个装饰者并实现它的getPrice(),改方法首先从父对象的方法获取值再做修改:

1
2
3
4
5
6
7
Sale.decorators.country = {
getPrice: function(){
let price = this.uber.getPrice(); // 获取父对象的值
price += price * 5 / 100;
return price;
}
}

按照相同方法,实现其他装饰者:

1
2
3
4
5
6
7
8
9
10
11
12
Sale.decorators.privince = {
getPrice: function(){
let price = this.uber.getPrice();
price += price * 7 / 100;
return price;
}
}
Sale.decorators.money = {
getPrice: function(){
return "¥" + this.uber.getPrice().toFixed(2);
}
}

最后我们还需要实现前面的decorate()方法,它将我们所有装饰者拼接一起,并且做了下面的事情:
创建了个新对象newobj,继承目前我们所拥有的对象(Sale),无论是原始对象还是最后装饰后的对象,这里就是对象this,并设置newobjuber属性,便于子对象访问父对象,然后将所有装饰者的额外属性复制到newobj中,返回newobj,即成为更新的sale对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
Sale.prototype.decorate = function(decorator){
let F = function(){}, newobj,
overrides = this.constructor.decorators[decorator];
F.prototype = this;
newobj = new F();
newobj.user = F.prototype;
for(let k in overrides){
if(overrides.hasOwnProperty(k)){
newobj[k] = overrides[k];
}
}
return newobj;
}

4.改造基本案例

这里我们使用列表实现相同功能,这个方法利用JavaScript语言的动态性质,并且不需要使用继承,也不需要让每个装饰方法调用链中前面的方法,可以简单的将前面方法的结果作为参数传递给下一个方法。

这样实现也有个好处,支持反装饰或撤销装饰,我们还是实现以下功能:

1
2
3
4
5
let sale = new Sale(100);
sale = sale.decorate('country');
sale = sale.decorate('privince');
sale = sale.decorate('money');
sale.getPrice();

现在的Sale()构造函数中多了个装饰者列表的属性:

1
2
3
4
function Sale(price){
this.price = (price > 0) || 100;
this.decorators_list = [];
}

然后还是需要实现Sale.decorators,这里的getPrice()将变得更简单,也没有去调用父对象的getPrice(),而是将结果作为参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Sale.decorators = {};
Sale.decorators.country = {
getPrice: function(price){
return price + price * 5 / 100;
}
}
Sale.decorators.privince = {
getPrice: function(price){
return price + price * 7 / 100;
}
}
Sale.decorators.money = {
getPrice: function(price){
return "¥" + this.uber.getPrice().toFixed(2);
}
}

而这时候父对象的decorate()getPrice()变得复杂,decorate()用于追加装饰者列表,getPrice()需要完成包括遍历当前添加的装饰者一级调用每个装饰者的getPrice()方法、传递从前一个方法获得的结果:

1
2
3
4
5
6
7
8
9
10
11
12
Sale.prototype.decorate = function(decorators){
this.decorators_list.push(decorators);
}

Sale.propotype.getPrice = function(){
let price = this.price, name;
for(let i = 0 ;i< this.decorators_list.length; i++){
name = this.decorators_list[i];
price = Sale.decorators[name].getPrice(price);
}
return price;
}

5.对比两个方法

很显然,第二种列表实现方法会更简单,不用设计继承,并且装饰方法也简单。
案例中getPrice()是唯一可以装饰的方法,如果想实现更多可以被装饰的方法,我们可以抽一个方法,来将每个额外的装饰方法重复遍历装饰者列表中的这块代码,通过它来接收方法并使其成为“可装饰”的方法。这样实现,saledecorators_list属性会成为一个对象,且该对象每个属性都是以装饰者对象数组中的方法和值命名。

参考资料

  1. 《JavaScript Patterns》

本部分内容到这结束

Author 王平安
E-mail pingan8787@qq.com
博 客 www.pingan8787.com
微 信 pingan8787
每日文章推荐 https://github.com/pingan8787/Leo_Reading/issues
JS小册 js.pingan8787.com
微信公众号 前端自习课

前端自习课