装饰器模式允许通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。也称为Decorator,Wrapper [ˈræpər]

背景

假设正在开发一个提供通知功能的库Notifier,包含一个send(message)方法。当客户端调用send方法时,会向事先设置的右键列表中发送message信息。

现在有的用户希望除了邮箱信息外,还能另外收到手机短信信息;有的用户希望收到微信信息;有的信息收到QQ信息。

我们可以在Notifier类上,派生出子类SMS Notifier/Wechat Notifier/QQ Notifier。

很快又有人希望,同时组合多种通知方式,如邮箱+短信+微信;又有人希望用邮箱+短信+QQ。

你可以尝试创建一个特殊子类来将多种通知方法组合在一起以解决该问题。 但这种方式会使得代码量迅速膨胀, 不仅仅是程序库代码, 客户端代码也会如此。

解决方案

当你需要更改一个对象的行为时,第一个跳入脑海的想法就是扩展它所属的类。但是,不能忽视继承可能引发的几个严重问题

  • 继承是静态的。无法在运行时更改已有对象的行为,只能使用由不同子类创建的对象来替代当前的整个对象。
  • 子类只能有一个父类。大部分编程语言不允许一个类同时继承多个类的行为。

其中一种解决方法是用聚合或组合,而不是继承

一个对象可以使用多个类的行为,包含多个指向其他对象的引用,并将各种工作委派给引用对象,从而能在运行时改变容器的行为。

聚合 (或组合) 组合是许多设计模式背后的关键原则 (包括装饰在内)。

封装器Wrapper装饰模式Decorator的别称。

“封装器” 是一个能与其他 “目标” 对象连接的对象。 封装器包含与目标对象相同的一系列方法, 它会将所有接收到的请求委派给目标对象。 封装器可以在将请求委派给目标前后对其进行处理,所以可能会改变最终结果。从客户端的角度来看,这些对象是完全一样的。

装饰模式

使用场景

  • 如果希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式。
    • 装饰能将业务逻辑组织为层次结构,你可为各层创建一个装饰,在运行时将各种不同逻辑组合成对象。由于这些对象都遵循通用接口,客户端代码能以相同的方式使用这些对象。
  • 如果用继承来扩展对象行为的方案难以实现或者根本不可行,可以使用该模式。
    • 许多编程语言使用final关键字来限制对某个类的进一步扩展。复用最终类已有行为的唯一方法是使用装饰模式,用封装器对其进行封装。

实现步骤

  1. 确保业务逻辑可用一个基本组件及多个额外可选层次表示。
  2. 找出基本组件和可选层次的通用方法。创建一个组件接口并在其中声明这些方法。
  3. 创建一个具体组件类,并定义其基础行为。
  4. 创建装饰基类,使用一个成员变量存储指向被封装对象的引用。
    • 该成员变量必须被声明为组件接口类型,从而能在运行时连接具体组件和装饰。
    • 装饰基类必须将所有工作委派给被封装的对象。
  5. 确保所有类实现组件接口。
  6. 将装饰基类扩展为具体装饰。具体装饰必须在调用父类方法(总是委派给被封装对象)之前或之后执行自身的行为。
  7. 客户端代码负责创建装饰并将其组合成客户端所需的形式。

优缺点

优点

  • 无需创建新子类即可扩展对象的行为。
  • 可以在运行时添加或删除对象的功能。
  • 可以用多个装饰封装对象来组合几种行为(装饰器嵌套)。
  • 单一职责原则。可以将实现了许多不同行为的一个大类拆分为多个较小的类。

缺点

  • 在封装器栈中删除特定封装器比较困难。
  • 实现行为不受装饰栈顺序影响的装饰比较困难。
  • 各层的初始化配置代码看上去可能会很糟糕。

与其它模式的关系

  • 适配器模式可以对已有对象的接口进行修改,装饰模式则能在不改变对象接口的前提下强化对象功能。
    • 此外,装饰还支持递归组合,适配器则无法实现。
  • 适配器能为被封装对象提供不同的接口,代理模式能为对象提供相同的接口,装饰则能为对象提供加强的接口。
  • 责任链模式和装饰模式的类结构非常相似。两者都依赖递归组合将需要执行的操作传递给一系列对象。两者有几点重要的不同之处
    • 责任链的管理者可以相互独立地执行一切操作,还可以随时停止传递请求。
    • 各种装饰可以在遵循基本接口的情况下扩展对象的行为。此外,装饰无法中断请求的传递。
  • 组合模式和装饰的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
    • 装饰类似于组合, 但其只有一个子组件。
    • 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。
    • 模式之间也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。
  • 大量使用组合和装饰的设计通常可从对原型模式的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。
  • 装饰可让你更改对象的外表,策略模式则让你能够改变其本质。
  • 装饰和代理有着相似的结构,但是意图却非常不同。
    • 这两个模式的构建都基于组合原则,一个对象应该将部分工作委派给另一个对象。
    • 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制

示例

1
2
3
4
// pizza 零件接口
type pizza interface {
	getPrice() int
}
1
2
3
4
5
6
7
8
9
const basePrice int = 15

// veggieMania 具体零件
type veggieMania struct {
}

func (p *veggieMania) getPrice() int {
	return basePrice
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

const tomatoToppingFee = 7

// tomatoTopping 具体装饰
type tomatoTopping struct {
	pizza pizza
}

func (c *tomatoTopping) getPrice() int {
	return c.pizza.getPrice() + tomatoToppingFee
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

const cheeseToppingFee = 10

// cheeseTopping 具体装饰
type cheeseTopping struct {
	pizza pizza
}

func (c *cheeseTopping) getPrice() int {
	return c.pizza.getPrice() + cheeseToppingFee
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {

	pizza := &veggieMania{}

	//Add cheese topping
	pizzaWithCheese := &cheeseTopping{
		pizza: pizza,
	}

	//Add tomato topping
	pizzaWithCheeseAndTomato := &tomatoTopping{
		pizza: pizzaWithCheese,
	}

	fmt.Printf("Price of veggeMania with tomato and cheese topping is %d\n", pizzaWithCheeseAndTomato.getPrice())
}

References

guru