脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - Go语言设计模式之实现观察者模式解决代码臃肿

Go语言设计模式之实现观察者模式解决代码臃肿

2022-11-08 10:54kevinyan Golang

今天学习一下用 Go 实现观察者模式,观察者模式主要是用来实现事件驱动编程。事件驱动编程的应用还是挺广的,除了我们都知道的能够用来解耦:用户修改密码后,给用户发短信进行风险提示之类的典型场景,在微服务架构实现

引言

我们先来简单学习一下用 Go 实现观察者设计模式,给怎么实现事件驱动编程、事件源这些模式做个铺垫。主要也是我也老没看设计模式了,一起再复习一下。以前看的设计模式教程都是 Java 的,这次用 Go 实现一番。

观察者模式

咱们先来看一下观察者模式的概念,我尽量加一些自己的理解,让它变成咱们都能理解的大俗话:

概念

观察者模式 (Observer Pattern),定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知,依赖对象在收到通知后,可自行调用自身的处理程序,实现想要干的事情,比如更新自己的状态。

发布者对观察者唯一了解的是它实现了某个接口(观察者接口)。这种松散耦合的设计最大限度地减少了对象之间的相互依赖,因此使我们能够构建灵活的系统来处理主体的变化。

我的理解

上面这段话看完,相信几乎对于理解观察者模式能起到的作用微乎其微,类似于现实职场里加班对项目进度起到的作用一样,加班的时候谁还没打过几把王者荣耀,嘿。下面我用自己的理解再给你们唠一下。

观察者模式也经常被叫做发布 - 订阅(Publish/Subscribe)模式、上面说的定义对象间的一种一对多依赖关系,一 - 指的是发布变更的主体对象,多 - 指的是订阅变更通知的订阅者对象。

发布的状态变更信息会被包装到一个对象里,这个对象被称为事件,事件一般用英语过去式的语态来命名,比如用户注册时,用户模块在用户创建好后发布一个事件 UserCreated 或者 UserWasCreated 都行,这样从名字上就能看出,这是一个已经发生过的事件。

事件发布给订阅者的过程,其实就是遍历一下已经注册的事件订阅者,逐个去调用订阅者实现的观察者接口方法,比如叫 handleEvent 之类的方法,这个方法的参数一般就是当前的事件对象。

至于很多人会好奇的,事件的处理是不是异步的?主要看我们的需求是什么,一般情况下是同步的,即发布事件后,触发事件的方法会阻塞等到全部订阅者返回后再继续,当然也可以让订阅者的处理异步执行,完全看我们的需求。

大部分场景下其实是同步执行的,单体架构会在一个数据库事务里持久化因为主体状态变更,而需要更改的所有实体类。

微服务架构下常见的做法是有一个事件存储,订阅者接到事件通知后,会把事件先存到事件存储里,这两步也需要在一个事务里完成才能保证最终一致性,后面会再有其他线程把事件从事件存储里搞到消息设施里,发给其他服务,从而在微服务架构下实现各个位于不同服务的实体间的最终一致性。

所以观察者模式,从程序效率上看,大多数情况下没啥提升,更多的是达到一种程序结构上的解耦,让代码不至于那么难维护。

Go 实现观察者模式

说了这么多,我们再看下用 Go 怎么实现最简单的观察者模式:

?
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
package main
import "fmt"
// Subject 接口,它相当于是发布者的定义
type Subject interface {
    Subscribe(observer Observer)
    Notify(msg string)
}
// Observer 观察者接口
type Observer interface {
    Update(msg string)
}
// Subject 实现
type SubjectImpl struct {
    observers []Observer
}
// Subscribe 添加观察者(订阅者)
func (sub *SubjectImpl) Subscribe(observer Observer) {
    sub.observers = append(sub.observers, observer)
}
// Notify 发布通知
func (sub *SubjectImpl) Notify(msg string) {
    for _, o := range sub.observers {
        o.Update(msg)
    }
}
// Observer1 Observer1
type Observer1 struct{}
// Update 实现观察者接口
func (Observer1) Update(msg string) {
    fmt.Printf("Observer1: %s\n", msg)
}
// Observer2 Observer2
type Observer2 struct{}
// Update 实现观察者接口
func (Observer2) Update(msg string) {
    fmt.Printf("Observer2: %s\n", msg)
}
func main(){
    sub := &SubjectImpl{}
    sub.Subscribe(&Observer1{})
    sub.Subscribe(&Observer2{})
    sub.Notify("Hello")
}

这就是 Go 实现观察者模式的代码,实际应用的时候,一般会定义个事件总线 EventBus 或者事件分发器 Event Dispatcher,来管理事件和订阅者间的关系和分发事件,它们两个就是名不一样,角色定位一样。

下面看一下用 Go 怎么实现事件总线。

Go 实现事件总线

下面我们实现一个支持以下功能的事件总线

  • 异步不阻塞
  • 支持任意参数值

这个代码不是我自己写的,出处见代码注释首行。

代码

?
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
49
50
51
52
53
54
55
56
// 代码来自https://lailin.xyz/post/observer.html
package eventbus
import (
    "fmt"
    "reflect"
    "sync"
)
// Bus Bus
type Bus interface {
    Subscribe(topic string, handler interface{}) error
    Publish(topic string, args ...interface{})
}
// AsyncEventBus 异步事件总线
type AsyncEventBus struct {
    handlers map[string][]reflect.Value
    lock     sync.Mutex
}
// NewAsyncEventBus new
func NewAsyncEventBus() *AsyncEventBus {
    return &AsyncEventBus{
        handlers: map[string][]reflect.Value{},
        lock:     sync.Mutex{},
    }
}
// Subscribe 订阅
func (bus *AsyncEventBus) Subscribe(topic string, f interface{}) error {
    bus.lock.Lock()
    defer bus.lock.Unlock()
    v := reflect.ValueOf(f)
    if v.Type().Kind() != reflect.Func {
        return fmt.Errorf("handler is not a function")
    }
    handler, ok := bus.handlers[topic]
    if !ok {
        handler = []reflect.Value{}
    }
    handler = append(handler, v)
    bus.handlers[topic] = handler
    return nil
}
// Publish 发布
// 这里异步执行,并且不会等待返回结果
func (bus *AsyncEventBus) Publish(topic string, args ...interface{}) {
    handlers, ok := bus.handlers[topic]
    if !ok {
        fmt.Println("not found handlers in topic:", topic)
        return
    }
    params := make([]reflect.Value, len(args))
    for i, arg := range args {
        params[i] = reflect.ValueOf(arg)
    }
    for i := range handlers {
        go handlers[i].Call(params)
    }
}

单测

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package eventbus
import (
    "fmt"
    "testing"
    "time"
)
func sub1(msg1, msg2 string) {
    time.Sleep(1 * time.Microsecond)
    fmt.Printf("sub1, %s %s\n", msg1, msg2)
}
func sub2(msg1, msg2 string) {
    fmt.Printf("sub2, %s %s\n", msg1, msg2)
}
func TestAsyncEventBus_Publish(t *testing.T) {
    bus := NewAsyncEventBus()
    bus.Subscribe("topic:1", sub1)
    bus.Subscribe("topic:1", sub2)
    bus.Publish("topic:1", "test1", "test2")
    bus.Publish("topic:1", "testA", "testB")
    time.Sleep(1 * time.Second)
}

毫不意外这个事件总线,只是个例子,咱也不能在项目开发里使用,这篇文章咱们先搞清概念,我其实前两天关注了下,没有发现什么好用的事件分发、事件总线的三方库,好在实现起来也不难,后面我准备自己写一个能用的到时候分享给大家,最起码是在学习、练习项目里能使用的吧。

总结

今天给大家用大白话瞎唠了一下观察者模式的原理和实际怎么应用,感觉文章的精髓主要在前半部分,可能有的不你还不能理解,后面我会再通过后续文章逐一解释,其实这些都是事件驱动和事件源这些模式里的基础内容。

至于这次给出的代码,其实没啥实战意义,就是大家先了解一下。Go 里边关于事件驱动之类的内容,感觉不多,有 Spring 使用经验的可以先看看 Spring 提供的@EventListener 注解,需要订阅者异步执行可以配合 @Async 注解使用,至于我上面说的需要保证事件发布的主体和订阅者的原子性持久化的话,则是通过@Transitional 和 @TransactionalEventListener 结合使用来实现。

以上就是Go语言设计模式之实现观察者模式解决代码臃肿的详细内容,更多关于Go 观察者模式的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/7130435348823277598

延伸 · 阅读

精彩推荐
  • Golanggo语言中for range使用方法及避坑指南

    go语言中for range使用方法及避坑指南

    Go中的for range组合可以和方便的实现对一个数组或切片进行遍历,但是在某些情况下使用for range时很可能就会被"坑",下面这篇文章主要给大家介绍了关于go语...

    peachesTao8582022-09-08
  • Golanggolang json性能分析详解

    golang json性能分析详解

    json格式可以算我们日常最常用的序列化格式之一了,Go语言作为一个由Google开发,号称互联网的C语言的语言,自然也对JSON格式支持很好。下面这篇文章主...

    hatlonely3872020-05-14
  • GolangGo 语言结构体链表的基本操作

    Go 语言结构体链表的基本操作

    链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,这篇文章主要介绍了Go 语言结构体链表...

    隐姓埋名48699172022-09-25
  • Golanggolang判断key是否在map中的代码

    golang判断key是否在map中的代码

    这篇文章主要介绍了golang判断key是否在map中的代码,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    chrispink_yang11692021-05-27
  • GolangGo语言的管道Channel用法实例

    Go语言的管道Channel用法实例

    这篇文章主要介绍了Go语言的管道Channel用法,实例分析了Go语言中管道的原理与使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下 ...

    go语言入门指南5792020-04-17
  • Golang使用Go语言实现微信公众平台

    使用Go语言实现微信公众平台

    这篇文章主要介绍了使用Go语言实现微信公众平台,虽然不是全部代码,但是也是给我们提供了一个非常好的思路,需要的朋友可以参考下 ...

    hebedich4962020-04-12
  • GolangGolang中文字符串截取函数实现原理

    Golang中文字符串截取函数实现原理

    在golang中可以通过切片截取一个数组或字符串,但是当截取的字符串是中文时,可能会出现问题,下面我们来自定义个函数解决Golang中文字符串截取问题...

    wdc7472020-05-14
  • GolangGo语言基础单元测试与性能测试示例详解

    Go语言基础单元测试与性能测试示例详解

    这篇文章主要为大家介绍了Go语言基础单元测试与性能测试示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助祝大家多多进步...

    枫少文7922021-12-05