Go 观察者模式讲解和代码实例

92 阅读6分钟

简介

观察者是一种行为设计模式, 允许一个对象将其状态的改变通知其他对象。

观察者模式提供了一种用于任何实现了订阅者接口的对象的机制,可对其事件进行订阅和取消订阅。

概念示例

在电商网站中, 商品时不时地会出现缺货情况。 可能会有客户对于缺货的特定商品表现出兴趣。 这一问题有三种解决方案:

  1. 客户定时去电商网站上查看商品是否上新了
  2. 客户关注电商网站,电商网站向用户推送所有上新的商品信息
  3. 客户只订阅自己感兴趣的特定商品,只有该商品上新时才会收到通知(一件商品支持任意数量的客户订阅)

通过比较选项 3 最具有可行性,实际上我们现在的电商网站使用的就是这种模式,比如你想购买的某个商品卖断货了,下方有个到货通知按钮,你点击之后就可以在这个商品补货的第一时间收到通知了,这其实就是观察者模式的思想。

观察者模式的主要组成部分有:

  • 任何事件发生时负责发布事件的主体(发布者 publisher)
  • 订阅了主体事件并在事件发生时收到通知的观察者(watcher、subscriber)

subject.go 主体

package main

type Subject interface {
    register(observer Observer)
    deregister(observcer Observer)
    notifyAll()
}

item.go 具体主体

package main

import "fmt"

type Item struct {
    observerList []Observer
    name         string
    inStock      bool
}

func newItem(name string) *Item {
    return &Item {
        name: name,
    }
}

func (i *Item) updateAvaiability() {
    fmt.Printf("Item %s is now in stock\n", i.name)
    i.inStock = true
    i.notifyAll()
}

func (i *Item)  register(o Observer) {
    i.observerList = append(i.observerList, o)
}

func (i *Item) deregister(o Observer) {
    i.observerList = removeFromSlice(i.observerList, o)
}

func (i *Item) notifyAll() {
    for _, observer := range i.observerList {
        observer.update(i.name)
    }
}

func removeFromSlice(observerList []Observer, observerToRemove Observer) []Observer {
    // 删除逻辑是:找到待删除的 observer 后交换到 observerList 末尾方便处理
    observerListLength := len(observerList)
    for i, observer := range observerList {
        if observerToRemove.getID() == observer.getID() {
            observerList[observerListLength-1], observerList[i] = observerList[i], observerList[observerListLength-1]
            return observerList[:observerListLength-1]
        }
    }
    return observerList
}

observer.go 观察者

package main

type Observer interface {
    update(string)
    getID() string
}

customer.go 具体观察者

package main

import "fmt"

type Customer struct {
    id string // 指定用户的标识符
}

func (c *Customer) update(itemName string) {
    // item 到货了,给它的观察者发送通知
    fmt.Printf("sending email to customer %s for item %s\n", c.id, itemName)
}

func (c *Customer) getID() string {
    return c.id
}

main.go 客户端代码

package main

func main() {
    // nike shirt 缺货
    shirtItem := newItem("Nike Shirt")

    observerFirst := &Customer{id: "abc@gmail.com"}
    observerSecond := &Customer{id: "xyz@gmial.com"}
    // 两个用户点击到货通知
    shirtItem.register(observerFirst)
    shirtItem.register(observerSecond)

    // 到货通知
    shirtItem.updateAvailability()
}

output.txt:  执行结果

Item Nike Shirt is now in stock
Sending email to customer abc@gmail.com for item Nike Shirt
Sending email to customer xyz@gmail.com for item Nike Shirt

Full Code In One File

package main

import "fmt"

type Pulisher interface {
	register(watcher Watcher)
	deregister(watcher Watcher)
	notifyAll()
}

type Watcher interface {
	update(item string)
	getID() string
}

type Item struct {
	wacherset []Watcher
	name      string
	inStock   bool
}

func newItem(name string) *Item {
	return &Item{name: name}
}

func (i *Item) updateAvailability() {
	fmt.Printf("Item %s is now in stock\n", i.name)
	i.inStock = true
	i.notifyAll() // 通知所有观察者
}

func (i *Item) register(w Watcher) {
	i.wacherset = append(i.wacherset, w)
}

func (i *Item) deregister(w Watcher) {
	remove(i.wacherset, w)
}

func (i *Item) notifyAll() {
	for _, watcher := range i.wacherset {
		watcher.update(i.name)
	}
}

func remove(watcherSet []Watcher, target Watcher) {
	watcherLen := len(watcherSet)
	for i, watcher := range watcherSet {
		if watcher.getID() == target.getID() {
			watcherSet[watcherLen-1], watcherSet[i] = watcherSet[i], watcherSet[watcherLen-1]
			watcherSet = watcherSet[:watcherLen-1]
			return
		}
	}
}

type customer struct {
	email string
}

func (c *customer) update(item string) {
	fmt.Printf("Sending email to customer %s for item %s\n", c.email, item)
}

func (c *customer) getID() string {
	return c.email
}

func main() {
	item := newItem("Samsung Galaxy S20")

	customer1 := &customer{email: "john.doe@example.com"}
	customer2 := &customer{email: "jane.doe@example.com"}

	item.register(customer1)
	item.register(customer2)

	item.updateAvailability()
}

image.png

项目中的观察者模式使用

package main

import (
	"context"
	"crypto/sha256"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
)

const (
	defaultResultChanSize           = 8
	Modified              EventType = "modified"
	Added                 EventType = "added"
	baseDir                         = "/Users/apple/Study/Go_Study/test_code/data/"
)

// 为可扩展性考虑
type BootStrapService interface {
	GetStorageService() *Storage
}

type bootStrapService struct {
	ctx     context.Context
	lock    sync.RWMutex
	cancel  context.CancelFunc
	configs *Storage
	dc      *diskCache
}

func getStorageService(ctx context.Context) *Storage {
	return &Storage{
		ctx:         ctx,
		objects:     make(map[string]string),
		watchers:    watcherSet{},
		keyWatchers: map[string]watcherSet{},
		wlock:       sync.RWMutex{},
	}
}

type EventType string

type ConfigEvent struct {
	KeyLink string
	Type    EventType
	Config  string
}

func newEvent(key string, old, new string) *ConfigEvent {
	if old == "" {
		return &ConfigEvent{
			KeyLink: key,
			Type:    Added,
			Config:  new,
		}
	}
	if checksum(old) != checksum(new) {
		return &ConfigEvent{
			KeyLink: key,
			Type:    Modified,
			Config:  new,
		}
	}
	// 未发生更改
	return nil
}

type watcher struct {
	result   chan ConfigEvent
	stopped  *int32
	onRemove func(*watcher)
}

func (w *watcher) ResultChan() <-chan ConfigEvent {
	return w.result
}

func (w *watcher) Stop() {
	if atomic.CompareAndSwapInt32(w.stopped, 0, 1) {
		if w.onRemove != nil {
			w.onRemove(w)
		}
	}
}

// 内存型存储服务,它可以作为发布者
// 用户可以 watch 指定 key 对应配置的变更
type watcherSet map[*watcher]struct{}

func (ws watcherSet) Add(w *watcher) {
	ws[w] = struct{}{}
}

func (ws watcherSet) Delete(w *watcher) {
	delete(ws, w)
}

type Storage struct {
	ctx         context.Context
	wlock       sync.RWMutex
	objects     map[string]string
	keyWatchers map[string]watcherSet
	watchers    watcherSet // 全局
}

func New(ctx context.Context) *Storage {
	return &Storage{
		ctx:         ctx,
		objects:     make(map[string]string),
		watchers:    watcherSet{},
		keyWatchers: map[string]watcherSet{},
	}
}

func (s *Storage) Get(key string) (string, error) {
	s.wlock.RLock()
	defer s.wlock.RUnlock()
	return s.objects[key], nil
}

func (s *Storage) Put(key, value string) {
	event := newEvent(key, s.objects[key], value)
	s.notifyAll(event)
	s.objects[key] = value
}

func (s *Storage) Watch(key string) *watcher {
	s.wlock.Lock()
	defer s.wlock.Unlock()

	w := &watcher{
		result:  make(chan ConfigEvent, defaultResultChanSize),
		stopped: new(int32),
		onRemove: func(w *watcher) {
			s.wlock.Lock()
			defer s.wlock.Unlock()
			ws, ok := s.keyWatchers[key]
			if ok {
				delete(ws, w)
			}
			close(w.result)
		},
	}

	ws, ok := s.keyWatchers[key]
	if !ok {
		ws = watcherSet{}
		s.keyWatchers[key] = ws
	}
	ws.Add(w)
	return w
}

func (s *Storage) WatchAll() *watcher {
	s.wlock.RLock()
	defer s.wlock.RUnlock()
	wa := &watcher{
		result:  make(chan ConfigEvent, defaultResultChanSize),
		stopped: new(int32),
		onRemove: func(w *watcher) {
			s.wlock.Lock()
			defer s.wlock.Unlock()
			delete(s.watchers, w)
			close(w.result)
		},
	}

	s.watchers.Add(wa)
	return wa
}

func (s *Storage) notifyAll(event *ConfigEvent) {
	// 全局的
	for watcher := range s.watchers {
		watcher.result <- *event
	}

	// 针对特定key的
	watcher := s.keyWatchers[event.KeyLink]
	for w := range watcher {
		w.result <- *event
	}
}

func checksum(config string) string {
	h := sha256.New()
	h.Write([]byte(config))
	return string(h.Sum(nil))
}

type diskCache struct {
	ctx     context.Context
	configs *Storage
	baseDir string
}

func newDiskCache(ctx context.Context, configs *Storage) *diskCache {
	dc := &diskCache{
		ctx:     ctx,
		configs: configs,
		baseDir: baseDir,
	}
	w := configs.WatchAll()
	go dc.sync(w)
	return dc
}

func (d *diskCache) sync(w *watcher) {
	for {
		select {
		case event, ok := <-w.result:
			fmt.Printf("persistent store watcher receive update event, new config 'key:value' is '%v:%v'\n\n", event.KeyLink, event.Config)
			if !ok {
				return
			}
			d.writeToDisk(event.Config)
		case <-d.ctx.Done():
			return
		}

	}
}

func (d *diskCache) writeToDisk(config string) {
	file := filepath.Join(d.baseDir, config)
	fmt.Printf("new config already write to file %v\n", file)
	err := writeFile(file, []byte(config), 0644)
	if err != nil {
		log.Fatalf("write data to file %s failed, err: %v", file, err)
	}
}

func writeFile(path string, data []byte, perm os.FileMode) error {
	file, err := OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = file.Write(data)
	return err
}

func OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) {
	if PathExist(path) {
		return os.OpenFile(path, flag, perm)
	}
	if err := CreateDirectory(filepath.Dir(path)); err != nil {
		return nil, err
	}
	return os.OpenFile(path, flag, perm)
}

func PathExist(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func CreateDirectory(dirPath string) error {
	f, e := os.Stat(dirPath)
	if e != nil {
		if os.IsNotExist(e) {
			return os.MkdirAll(dirPath, 0755)
		}
		return fmt.Errorf("failed to create dir %s: %v", dirPath, e)
	}
	if !f.IsDir() {
		return fmt.Errorf("failed to create dir %s: file already exist", dirPath)
	}
	return e
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	s := getStorageService(ctx)
	dc := newDiskCache(ctx, s)

	bs := bootStrapService{
		ctx:     ctx,
		lock:    sync.RWMutex{},
		cancel:  cancel,
		configs: s,
		dc:      dc,
	}

	w1 := bs.configs.Watch("smy")
	w2 := bs.configs.Watch("hello")
	w3 := bs.configs.Watch("world")

	handlerPut := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		key, value := r.PostFormValue("key"), r.PostFormValue("value")
		fmt.Println("receive POST Request For", key, value)
		s.Put(key, value)
		w.WriteHeader(http.StatusOK)
	})

	handlerGet := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		key := r.URL.Query().Get("key")
		fmt.Println("receive GET Request For Key", key)
		value, err := s.Get(key)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
		}

		if value == "" {
			w.Write([]byte("key not found"))
		} else {
			w.Write([]byte(value))
		}
	})

	http.Handle("/put", handlerPut)
	http.Handle("/get", handlerGet)

	fmt.Println("starting server in goroutine, listening :8080")
	go http.ListenAndServe(":8080", nil)

	fmt.Printf("\nstart watch key changing in main groutine...\n\n")
	for {
		select {
		case e := <-w1.result:
			fmt.Printf("smy config changed, its event type is %s and its new value is %s\n\n", e.Type, e.Config)
		case e := <-w2.result:
			fmt.Printf("hello config changed, its event type is %s and its new value is %s\n\n", e.Type, e.Config)
		case e := <-w3.result:
			fmt.Printf("world config changed, its event type is %s and its new value is %s\n\n", e.Type, e.Config)
		case <-ctx.Done():
			return
		}
	}
}

未存入时的 Get 测试

curl "localhost:8080/get?key=smy"

curl "localhost:8080/get?key=hello"

curl "localhost:8080/get?key=world"

压入新 config

  • put 操作

curl -X PUT localhost:8080/put -d "key=hello&value=hellohello"

image.png

  • 持久化

image.png

  • 更新操作

curl -X PUT localhost:8080/put -d "key=smy&value=smysmy"

curl -X PUT localhost:8080/put -d "key=smy&value=smyupdate"

image.png

image.png

再次测试 Get

curl "localhost:8080/get?key=smy"

image.png