本文字数:2504字
预计阅读时间:11 分钟
观察者模式(Observer Pattern)是一种发布/订阅模式,定义了对象间的一种一对多的依赖关系,一个主题对象(Subject)持有若干个依赖其状态的观察者对象(Observer),而观察者模式则允许多个观察者对象自动接收主题对象的状态变更事件,以各自处置。
观察者模式是一种行为型模式,和其它面向对象设计模式一样,不难在现实生活中找到实际的场景对照。
为了验证“不难”这件事,我特意突袭了旁边工位的天总:“天总,快给我举一个观察者模式的例子,急!”
天总未停下疯狂打码的双手,嘴角一撇:“互联网时代的芸芸众生,不都是观察者本者么,接收着各大 APP 上发布的热点事件”
我说:“那各大 APP 都算是主题对象喽,那观察者模式两种对象间不就变成多对多的关系了么?”
天总微一沉吟:“设计模式是死的,你是活的”(PS:多对多是一对多的延伸而已)
我:“……”
天总看了看呆滞的我,摇了摇头:“不过都是用来摸鱼的工具罢了,殊途同归,殊途同归”
我:“行吧,那我这就去把代码敲出来!啧啧,不过多对多现实中也是很不友好的事情呀,各种 APP 多到爆炸,手机内存都不够用了!”
天总:“说的倒也不无道理……去 Coding 吧,大聪明”
首先,定义用来摸鱼的 APP 们的抽象接口:摸鱼工具(对应主题的抽象)
type Platform string
const (
WEIBO Platform = "weibo"
BILIBILI Platform = "bilibili"
ZHIHU Platform = "zhihu"
)
// MoYu 用来摸鱼的 APP 们的抽象接口
type MoYu interface {
// Platform 用来摸鱼的平台名称
Platform() Platform
// GetName 获取平台热点事件发布人名称
GetName() string
// GetContent 获取热点事件内容
GetContent() string
}
其次,定义芸芸众生(用户)的抽象接口及公共实现(对应观察者的抽象)
// Mortal 芸芸众生(用户)
type Mortal interface {
// Accept 接收热点事件
Accept()
}
type BaseMortal struct {
// MoYu 持有摸鱼工具实例,便于获取当前发布的热点事件(摸鱼工具的状态)
MoYu
}
定义完成,我抓紧冲天总喊了一句:“天总,快来看我这接口定义的帅不帅!”
天总带着耳机(假装)没听到我深情地呐喊~~~得,我继续。
以 B 站举例,下面将定义摸鱼工具的具体实现。为了易于扩展,以便增加更多摸鱼工具的实现,首先抽象出公共部分的结构体代码如下:
type BaseMoYu struct {
Name string
Content string
// MortalList 热点事件发布后,需要通知的用户列表
MortalList []Mortal
}
func (b *BaseMoYu) Attach(mortal Mortal) {
b.MortalList = append(b.MortalList, mortal)
}
func (b *BaseMoYu) Publish(name, content string) {
b.Name = name
b.Content = content
// 依次通知
for _, item := range b.MortalList {
item.Accept()
}
}
func (b *BaseMoYu) GetName() string {
return b.Name
}
func (b *BaseMoYu) GetContent() string {
return b.Content
}
具体实现结构体只需再定义所属平台即可(对应主题的实现),代码如下:
type MoYuByBilibili struct {
BaseMoYu
}
func NewMoYuByBilibili() *MoYuByBilibili {
return &MoYuByBilibili{}
}
func (m *MoYuByBilibili) Platform() Platform {
return BILIBILI
}
接下来,定义两个时刻关注热点事件的芸芸众生 Tom && Jerry(对应观察者的实现)
type Tom struct {
BaseMortal
}
func NewTom(moYu MoYu) *Tom {
return &Tom{BaseMortal{MoYu: moYu}}
}
func (t Tom) Accept() {
fmt.Printf("Tom accept from: %v, content: %v, published by: %v\n",
t.BaseMortal.Platform(), t.BaseMortal.GetContent(), t.BaseMortal.GetName())
}
type Jerry struct {
BaseMortal
}
func NewJerry(moYu MoYu) *Jerry {
return &Jerry{BaseMortal{MoYu: moYu}}
}
func (j Jerry) Accept() {
fmt.Printf("Jerry accept from: %v, content: %v, published by: %v\n",
j.BaseMortal.Platform(), j.BaseMortal.GetContent(), j.BaseMortal.GetName())
}
客户端代码如下:
func main() {
bilibili := NewMoYuByBilibili()
tom := NewTom(bilibili)
jerry := NewJerry(bilibili)
bilibili.Attach(tom)
bilibili.Attach(jerry)
bilibili.Publish("张朝阳的物理课", "张朝阳、俞敏洪是怎样“摸鱼”的")
bilibili.Publish("哔哩哔哩国创", "《暂停!让我查攻略》")
}
输出:
Tom accept from: bilibili, content: 张朝阳、俞敏洪是怎样“摸鱼”的, published by: 张朝阳的物理课
Jerry accept from: bilibili, content: 张朝阳、俞敏洪是怎样“摸鱼”的, published by: 张朝阳的物理课
Tom accept from: bilibili, content: 《暂停!让我查攻略》, published by: 哔哩哔哩国创
Jerry accept from: bilibili, content: 《暂停!让我查攻略》, published by: 哔哩哔哩国创
当摸鱼工具 B 站(主题对象)有新的内容发布(状态变更)时,便会自动通知下载(Attach)了该工具的用户(观察者),用户收到(Accept)通知后,便可各自处置了。
代码敲完,正兀自回想着刚才和天总讨论的问题:“现在的摸鱼工具可是真的多,微博、知乎、抖音、B站……随之而来的是手机内存不足,来回切换使用体验变差等诸多问题,更严重的,自己因此浪费掉的时间显著增多!QvQ”
此时,天总深邃的声音从背后传来:“不错,汝已窥得摸鱼之法,只需再加一点 kik 作引,便可摸鱼达成!”
我一脸困惑的回头:“摸鱼?kik?摸鱼达成?”
天总邪魅一笑:“应用商店搜索‘摸鱼kik’,摸鱼即刻达成,告别内存不足,告别 APP 内卷,拯救你的时间!自建主题,关心你真正关心的事~”
我抓紧搜索了一下:“诶嘿~全网内容订阅,热门事件跟踪!我就说观察者模式还是一对多好理解嘛!!!整那么多主题对象徒增烦恼~~”
观察者模式将观察者绑定到主题之上,当主题状态变化时,可以自动通知绑定其上的观察者们。
其核心思想在于建立一套触发机制,但在日常开发中,往往一不小心就会面向过程编码,打破这套机制,将代码逻辑耦合在主题对象之中。
举个例子:
在项目开发中,使用 Nacos 作为配置中心是一种不错的选择,可以通过 ListenConfig
传入自定义的 OnChange()
方法以动态更新必要的配置。
// ListenConfig use to listen config change,it will callback OnChange() when config change
// dataId require
// group require
// onchange require
// tenant ==>nacos.namespace optional
ListenConfig(params vo.ConfigParam) (err error)
而项目配置往往会对应到若干个不同的业务,此时 OnChange()
方法很容易写成下面的模样:
onChange := func(namespace, group, dataId, data string) {
newConfig := config.UnMarshal(data)
GetConfig().BizA.Property1 = newConfig.BizA.Property1
GetConfig().BizA.Property2 = newConfig.BizA.Property2
GetConfig().BizB.Property1 = newConfig.BizB.Property1
}
在业务较少,且配置更新逻辑不复杂时,简单可用问题不大,可反之呢?
先不说当新增业务配置时,此处代码不可避免的需要改动,这就已经违反了开闭原则。单说当业务越来越多、配置逻辑变得复杂这件事,这段代码便很容易变得冗长,难以阅读和维护。
实际上,这里的 OnChange()
方法被调用,完全可以对应到观察者模式中,主题对象状态变更的行为;而各个业务的配置集合,则是对应了观察者对象的集合。通过观察者模式来改造这段代码便是很自然的选择了。
藉由上例,私以为开发实践中,需要额外留意文章开头定义里的“各自处置”这件事,以判定是否需要使用观察者模式。
开发实践中,“处置”这个行为当然可以“堆”在主题对象中去做,但很不优雅,而通过观察者模式,我们便可以对这一“堆”行为进行合理的拆分。
以上~