我们再来看看结构型设计模式的另一种,装饰器模式。
装饰器模式和桥接模式有着类似的概念,桥接模式是把若干类别的对象其中具体的类桥接在一起,组合成一个具体的组合对象。而装饰器模式也是把多个对象组合在一起,但是对类别的概念比较模糊。
我们还是通过一个实际的案例来理解装饰器模式。假设有个通知类,在系统触发报警时给用户发送邮件。而此时又引入了短信、飞书、电话通知,且在不同报警等级发送不同组合的通知,你会怎样设计这个实现呢?我们先来看看两种实现方式,然后再来讨论两者的利弊。
方案一:
首先把各种通知类型封装自己的类,以及自己的Send方法,
type SmsNotify struct {
}
type TelNotify struct {
}
type FlyNotify struct {
}
func (s SmsNotify) Send() {
fmt.Println("send sms msg")
}
func (t TelNotify) Send() {
fmt.Println("call tel")
}
func (f FlyNotify) Send() {
fmt.Println("send fly msg")
}
然后定义发送消息的结构,且实现了 Notifier 接口:
type Notifier interface {
Send()
}
type PanicNotifier struct {
tel TelNotify
fly FlyNotify
}
func (p PanicNotifier) Send() {
p.tel.Send()
p.fly.Send()
}
type ErrorNotifier struct {
fly FlyNotify
sms SmsNotify
}
func (e ErrorNotifier) Send() {
e.fly.Send()
e.sms.Send()
}
方案二,具体的消息结构不变,而是发送消息类不区分报警等级,而是通过成员函数的实现来区分。
type Notifier struct {
tel TelNotify
fly FlyNotify
sms SmsNotify
}
func NewNotifier(t TelNotify, f FlyNotify, s SmsNotify) Notifier{
return Notifier{t,fly,sms}
}
func (n Notifier) SendPanic() {
n.tel.Send()
n.fly.Send()
}
func (n Notifier) SendError() {
n.fly.Send()
n.sms.Send()
}
方案一和二的实现都可以满足业务需求,如果我们再考虑长远一点,未来有更多种发送消息方式引入,方案二中得对 Notifier结构进行成员添加。我们得为 Notifier 的构造方法(这里的NewNotifier方法)我们得初始化所有的成员 ,当添加的成员越来越多,我们需要初始化的成员也越来越多。而我们如果单单只要发送 SendPanic 方法,那么不需要出示好的 sms 成员也得进行初始化。
当然这里有人说我可以传个nil,但是如果传入nil,又使用该对象误使用了 SendError 要么需要逻辑来处理不崩溃,要么返回错误(成员nil判断)。这个时候外层调用又得对错误进行处理。
当需要有类似不同对象组合方式的需求时,这里建议用方案一,也就是装饰模式来处理,虽然定义比较多,但是减少了在对象使用过程中的出错。方案二也是装饰模式实现的,但是这个装饰有些不那么聪明。
就像一个人有很多的衣服,在每个季节应该有不同衣服的搭配,而方案二就像是我为了避免换衣服,无论什么季节,那我把所有的衣服都穿上吧。
接下来我们再讲讲创建模式中的外观模式。
外观模式正好就是上述所说的 “把所有衣服都穿上” 的这种情况。但是这里使用的场景与上述的案例有些不同。假设我们有一个通知类,需要发送所有类型的通知,而对于使用者无需关注具体发送了哪些通知,只需要调用成员方法即可。例如以下代码,用户只需要关注Send 而不需要知道具体发了哪些通知:
func (n Notifier) Send() {
n.tel.Send()
n.fly.Send()
n.sms.Send()
}
通知的案例用在外观模式比较牵强,大家可以想想例如转账(或者很多流程很繁琐的过程),对于使用者只需要知道我要完成转账功能就行,我不用关注你的细节,细节可能包括账户审核、余额查询、发起转账等等过程。
通过以上代码示例来看,模式并没有好坏之分,模式其实也都是长久的代码实践总结出来的概念,只有与实际的使用场景结合才能选择最适合的模式。大家可以通过上述代码和分析来了解装饰器模式、外观模式在代码和使用场景上的不同。
往期回顾: