Golang 实现设计模式 —— 装饰模式
概念
“用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能”
“动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活”
何时用
需要扩展一个类的功能,或给一个类增加附加责任
需要动态的给一个对象增加功能,且可以动态地撤销它
需要增加一些基本功能的排列组合而产生的大量的功能,而使得继承变得非常困难的时候
实现构件
抽象构件(Component)
表示“被”装饰的本体的抽象定义,这个定义通常是一个接口(Interface),定义了若干方法(能力),这些方法可以用来在被具体装饰角色(ConcreteDecorator)实现时改变原有构件本体的方法(能力),如原来本体伤害输出是 10,装饰角色把它再增加 10 而不会影响本体的原有逻辑(代码)。
具体构件(ConcreteComponent)
表示实现了抽象构件(Component)的对象,即将要接受附加能力的对象,本文中比喻的“本体”。
抽象装饰(Decorator)
持有一个抽象构件的实例(即具体构件),并定义(实现)与抽象构件接口一致的接口。抽象装饰的(部分)作用应该是应用了依赖倒置原则,在结构上使具体装饰(ConcreteDecorator)不要直接依赖于抽象构件,因为二者作用性质也不同,直接依赖灰常奇怪,就好像都是50岁的男子也不能把隔壁老王当成爸爸一样。
具体装饰(ConcreteDecorator)
实现了抽象装饰的定义,负责给构件对象添加附加能力的对象。具体装饰通过实现抽象装饰定义的接口,拥有了和具体构件一样的“能力”(方法/函数/属性),再通过抽象装饰定义中所持有的抽象构件的实例而获得对该实例“相同”能力的结果,并在结果上进行一些装饰。
实现步骤
- 定义抽象构件,提供抽象接口
- 定义具体构件并实现抽象构件,构造后的具体构件即理解为“本体”,被装饰的对象
- 定义抽象装饰,它要做两件事,实现抽象构件和保存一个抽象构件对象
- 定义具体装饰,具体装饰要实现抽象装饰,并在实现的接口方法中对构件进行具体装饰操作
- 之后,要增加“本体”就创建具体构件,要增加装饰物,就创建具体装饰
- 使用时,把本体“传递进”装饰对象,在装饰对象(同样继承自抽象构件)的方法里去使用本体的方法和结果,加工它,并输出进行了“调整”的结果
原理与代码
用 Golang 描述代码结构(代码模仿自 github,但不好意思忘记来自哪位作者了)
package component
/*
抽象构件(Component)接口
*/
type Beverage interface {
// 计算价格
Cost() int
// 返回描述
Me() string
}
上面定义了抽象构件接口“饮料”接口,它包含了两个方法,输出价格和描述自己,饮料接口作为最底层接口是所有饮料都必须要实现的(能力)。
package component
type Tea struct {
Beverage // 作用?
name string
price int
}
func (self *Tea) Me() string {
return self.name
}
func (self *Tea) Cost() int {
return self.price
}
有了饮料这个概念,就在此基础之上创建第一款具体的产品:“茶”。茶是饮料,因此它要继承饮料的特性(实现接口)。如何表达茶实现了饮料接口,使得上层调用茶时可以访问茶的接口呢?按照 Golang 的语法特性先定义一个 Tea
结构(类),先有了茶。
Golang 中实现接口无需声明,实现该接口所有方法即为(自动)实现接口,因此 Tea
类要通过实现 Me
和 Cost
两个具体方法来实现对接口的实现(这话说的)。与普通方法不同的是在 func
和 Me
中间要增加 (self *Tea)
,这种语法糖的作用简单说就是当前这个 Me
方法被 Tea
这个类包含(实现)了,以后可以 Tea.Me()
这么用了。
在两个方法里需要实现具体的逻辑,要输出对自身的描述和价格,那值从哪儿来,于是给 Tea
定义了两个私有字段 name
和 price
,以便在构造类实例时对其赋值。
Tea
中还包含了一个 Beverage
,意思是通过组合的方式让类有了 Beverage 对象,但个人理解在本例中没有起到实质作用,因为 Tea
已经是 Beverage
的具体实现了,除非再创建出茶下面的红茶、绿茶继承自茶,它可用被用做标记上层结构是谁,否则在本例中只有茶一种饮品,或创建咖啡这种与茶是平级关系的构件,那这个内部的 Beverage
就没有作用了。
package component
type Moli struct {
*Tea
}
func NewMoli() *Moli {
return &Moli{&Tea{name: "茉莉", price: 48}}
}
func (self *Moli) Me() string{
return self.name
}
func (self *Moli) Cost() int {
return self.price
}
package component
type Puer struct {
*Tea
}
func NewPuer() *Puer {
return &Puer{&Tea{name: "普洱", price: 38}}
}
func (self *Puer) Me() string{
return self.name
}
func (self *Puer) Cost() int {
return self.price
}
上面创建两种具体的茶,茉莉和普洱。可以看到两种茶实际是一种结构,为了表达装饰模式的特性这样写更为清晰。类中只包含了一个对象,就是指向茶的指针,也就是“指向某个茶的指针”。普洱类就像个壳,名字叫普洱,壳里边只有一种(个)对象就是茶。
NewPuer
的语法可以帮助我们方便的实例化一个普洱,它的返回值是指针,内在的逻辑是返回一个袋子,这种袋子叫 Puer
,它里面(只)有一种(个)东西名叫普洱价格是38元的茶。茉莉逻辑与此相同。
到此,完成了抽象构件和具体构件的设计和创建,实际可以喝茶了,沏上两杯试一下
package main
func main() {
moli := component.NewMoli()
puer := component.NewPuer()
fmt.Printf("第 %v 杯是 %s 售价 %v 元\n", 1, moli.Me(), moli.Cost())
fmt.Printf("第 %v 杯是 %s 售价 %v 元\n", 2, puer.Me(),puer.Cost())
fmt.Printf("好喝吗,欢迎再来 ^_^ ")
}
上面代码会输出两杯茶的信息
第 1 杯是 茉莉 售价 48 元
第 2 杯是 普洱 售价 38 元
下面该装饰了,我要创建一些辅料,比如糖和冰,并希望能自由的放进想放的饮料中而不会和某种饮料硬性绑定,最终实现的逻辑是点一杯加糖的茉莉而不是点一杯茉莉自己再买一包糖倒里边。按照原理先定义出一个抽象装饰,它要同样实现抽象构件 Beverage
接口,并(最好)还能保持对构件的引用,因为要有“本体”才能装饰,不然对谁做装饰呢。
package decorator
import "golang-design-pattern/decorator/component"
type Condiment struct {
*component.Tea //作用?
beverage component.Beverage
name string
price int
}
func (self *Condiment) Me() string {
return self.name
}
func (self *Condiment) Cost() int {
return self.price
}
上面是抽象装饰 Condiment,与 Tea
一样实现了两个具体的方法,并拥有两个方法要使用到的私有字段。beverage component.Beverage
让它能够保存一个符合抽象构件接口要求的对象,即只要是满足 Beverage
接口定义的对象我就能保存着以后用。
package decorator
import "golang-design-pattern/decorator/component"
type Sugar struct {
*Condiment
}
func NewSugar(beverage component.Beverage) *Sugar {
return &Sugar{ &Condiment{beverage:beverage, name:"糖", price:3 }}
}
func (self *Sugar) Me() string{
return self.beverage.Me() + " 加点 " + self.name
}
func (self *Sugar) Cost() int {
return self.beverage.Cost() + self.price
}
package decorator
import "golang-design-pattern/decorator/component"
type Ice struct {
*Condiment
}
func NewIce(beverage component.Beverage) *Ice {
return &Ice{ &Condiment{beverage: beverage, name: "冰", price: 3 }}
}
func (self *Ice) Me() string {
return "加了" + self.name + "的" + self.beverage.Me()
}
func (self *Ice) Cost() int {
return self.beverage.Cost() + self.price
}
上面定义两种辅料,Sugar
和 Ice
。角色是具体装饰,内部保存着对 Condiment
的引用,并且它们也要实现 Beverage
接口,是为了履行装饰模式的特性,即对上层调用是透明的,调用装饰件和调用具体构件方法一样,否则就违背或污染了装饰模式的优势。
具体装饰的接口方法是关键,以 Sugar
中的 Cost
方法为例,它的实现是通过将 Sugar (里)的 Condiment (里)的 beverage 的价格叠加上 Sugar 自己的价格,作为这一杯“加糖饮料”的价格。
好了,辅料也有了,让我们来做一杯加糖的茉莉和加冰的普洱吧
package main
import (
"fmt"
"golang-design-pattern/decorator/component"
"golang-design-pattern/decorator/decorator"
)
func main() {
moli := component.NewMoli()
puer := component.NewPuer()
fmt.Printf("第 %v 杯是 %s 售价 %v 元\n", 1, moli.Me(), moli.Cost())
fmt.Printf("第 %v 杯是 %s 售价 %v 元\n", 2, puer.Me(),puer.Cost())
fmt.Printf("下面我们给刚才那杯茉莉加点糖...\n")
sugar := decorator.NewSugar(moli)
fmt.Printf("刚刚给茉莉加了点糖,现在准备尝一下\n")
fmt.Printf("第 %v 杯是 %s 售价 %v 元\n", 3, sugar.Me(), sugar.Cost())
ice := decorator.NewIce(puer)
fmt.Printf("来一杯加冰的普洱,现在准备尝一下\n")
fmt.Printf("第 %v 杯是 %s 售价 %v 元\n", 4, ice.Me(), ice.Cost())
fmt.Printf("好喝吗,欢迎再来 ^_^ ")
首先做了两杯标准的茶,一杯48元叫做“茉莉”的Moli茶,一杯38元的叫做“普洱”的Puer茶。为了给这杯Moli加点糖,创建了 sugar,并把刚才那杯 moli “传”给了它。它拿到了 moli 后加了糖(sugar.Me方法),并把价格提高到了 48+3 元,加冰的Puer亦是如此。
感受
为什么要把本体传给装饰,而不是往本体上“添加”装饰,这个逻辑让我想不通别扭了很久,其实到现在也是别扭。越别扭越佩服创造逻辑创造模式的聪明人,因为在本体上做动作,一定会增加本体的额外工作,甚至会破坏本体原有的结构,本体会怎么想,我就是一杯茉莉茶,我为什么要实现加糖、加醋、加冰这些方法。所以换个角度看,把具体装饰想象成一个厨师,把本体(具体构件)给TA,TA来做操作就好理解一些了。
单点一杯Moli,再点一个Sugar,把它们加一起也能达成效果,这和装饰模式有什么区别?个人理解装饰模式是“官方组装”,是对于客户端而言的。客户端需要一杯加了糖的茉莉茶,这是一杯经过组合加工的整体产品交付,而不是扔给客户一杯茶一袋糖,这有本质的区别。在应用中,装饰模式往往被用来做更有趣的功能扩展,核心优点是通过“组合”而不是“继承”的方式,在不改变本体的情况下,改变结果。
一定要尽量理解逻辑本身的逻辑,而不能仅从文字字面意思理解,中文英文本身意思就差距甚远,更何况中文自己又那么博大精深。