③悄无声息的扩展-装饰者模式

简单说装饰可以让你不用修改底层代码给对象赋予新的职责。

从分离改变,更进一步

看了前两个设计模式,相信你一定感觉到继承只能解决静态时对类的扩展,如果想动态的对类的行为扩展就需要用到组合。

既然我们已经可以用策略模式分离改变的部分,还有什么做不到的么? 看看下面一个问题:

  • 奶茶店有几十种品种的饮料,他们都需要继承自一个抽象类 Beverage,因为每种饮料有自己的产品说明,并且各自实现了一个计算金额的方法 cost。
  • 每种奶茶除了自己特有的配料外,还可以额外付费添加配料,比如加两份的珍珠,加一份椰果,并且需要计算总价。

如果想枚举出店内的每一种产品是不现实的,那时非常庞大的一个排列组合,因为不可能知道客户要加那些配料。并且严重的违反了设计中的两个原则:

1
2
3
4
5
6
7
8
9
abstract class BeverageAbstract {
description = "some description";
cost() {}
}
// 果茶加牛奶
class FruitTeaWithMilk extends BeverageAbstract {}
// 柠檬茶加两份珍珠
class LemonTeaWith2Pearls extends BeverageAbstract {}
//...

既然不能枚举考虑是不是应该有一个统一的 Beverage 类,用于实现抽象类 BeverageAbstract,并且把所有的配料都添加在 Beverage 上,并记录配料的数量。
子类会调用父类 cost 方法计算所有配料,并加上自己品种的价格。

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
abstract class BeverageAbstract {
description = "some description";
cost() {}
}
class Beverage extends BeverageAbstract {
milk = 2;
milkCount = 0;
setMilk(count: number) {
this.milkCount = count;
}
coffee = 5;
coffeeCount = 0;
setCoffee(count: number) {
this.coffeeCount = count;
}
cost() {
let total = 0;
if (this.milkCount > 0) {
total += this.milkCount * this.milk;
}
if (this.coffeeCount > 0) {
total += this.coffeeCount * this.coffee;
}
return total;
}
}

class FruitTea extends Beverage {
cost() {
return 10 + super.cost();
}
}

const fruitTea = new FruitTea();
fruitTea.setMilk(2);
console.log(fruitTea.cost());

const fruitTea2 = new FruitTea();
fruitTea2.setMilk(1);
console.log(fruitTea.cost());

这样的设计还有一些问题:

  • 并不是所有的配料都需要继承,每种饮料都有自己特有的配料。
  • 一但需要新的配料或新的品种或价钱的改变,就需要修改父类。
  • 当有新的品种出现的时候,他可能继承了不必要的方法。

定义装饰者

设计类的一个原则是 ❤‍🔥 类应该对扩展开放,对修改关闭,也就是类设计中提到的开放关闭原则

开放且关闭并不冲突,想想观察中模式中的案例,可以通过调用类的方法添加观察者,而不改变原有的类的方法。并不需要每一个类都遵循开放关闭原则,避免过度设计,只需要针对可能经常会发生变化的类应用开发-关闭原则。

装饰者模式:动态的将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案

对于上面的问题,按以下的方式思考:

  • 我们已经有一个果茶类 FruitTea
  • 需要加两份额外的牛奶,用两个牛奶配料的类去修饰它
  • 需要加一份额外的咖啡,用一个咖啡配料的类去修饰它
  • 依赖修饰者的 cost 方法计算价格
  • 在编码的时候为了明确修饰者和被修饰者的关系,需要让修饰者和被修饰者继承同样的类或实现相同的接口,因为被修饰的类仍然被视作原有的类,它的属性以及方法的作用不应该发生变化,只是扩展了方法实现。
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
interface BeverageInterface {
description: string;
cost(): number;
getDescription(): string;
}
class MilkDecorator implements BeverageInterface {
beverage: BeverageInterface;
description = "MilkDecorator";
constructor(beverage: BeverageInterface) {
this.beverage = beverage;
}
getDescription() {
return this.description;
}
cost() {
return 2 + this.beverage.cost();
}
}
class CoffeeDecorator implements BeverageInterface {
beverage: BeverageInterface;
description = "CoffeeDecorator";
constructor(beverage: BeverageInterface) {
this.beverage = beverage;
}
getDescription() {
return this.description;
}
cost() {
return 5 + this.beverage.cost();
}
}
class FruitTea implements BeverageInterface {
description = "FruitTea";
getDescription() {
return this.description;
}
cost() {
return 10;
}
}

let fruitTea = new FruitTea();
// 原价fruitTea
console.log(fruitTea.cost());
// 添加配料一份牛奶 一份咖啡
fruitTea = new MilkDecorator(fruitTea);
fruitTea = new CoffeeDecorator(fruitTea);
console.log(fruitTea.cost());

现在已经分离了装饰着对象,但是使用装饰者模式是基于一下几个前提:

  • 被修饰的对象是可以抽象的,也就是说被修饰的对象不会轻易改变,这样针对它的修饰类才有意义
  • 修饰对象是不关心外部状态的,它只关心被修饰的对象,因为你想让他控制修饰链中的每个节点,需要更好的设计。
  • 被装饰的对象可能拥有特定的类型,在使用装饰的时候需要小心,避免功能或类型丢失。

与 ES6 修饰器的区别

ES6 提供修饰器方提案,在 babel 转译后支持,可以修饰类或类的属性和方法,但并不是一种设计模式。

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
interface BeverageInterface {
description: string;
cost(): number;
getDescription(): string;
}
function milkDecorator(target: () => number): () => number {
return () => 2 + target();
}

class FruitTea implements BeverageInterface {
description = "FruitTea";
getDescription() {
return this.description;
}
cost() {
return 10;
}
}

class FruitTeaWithMilk extends FruitTea {
@milkDecorator
cost(): number {
return super.cost();
}
}
let fruitTea = new FruitTeaWithMilk();
console.log(fruitTea.cost());
  • 继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式。
  • 在我们的设计中,应该允许行为可以被扩展,而无须修改现有的代码。
  • 组合和委托可用于在运行时动态地加上新的行为。
  • 除了继承,装饰者模式也可以让我们扩展行为。
  • 装饰者模式意味着一群装饰者类,这些类用来包装具体组件。
  • 开放一关闭原则
  • 装饰者类反映出被装饰的组件类型(事实上,他们具有相同的类型,都经过接口或继承实)
  • 装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。
  • 你可以用无数个装饰者包装一个组件。
  • 装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型。
  • 装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂。
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信