手撸golang 行为型设计模式 观察者模式

ioly

手撸golang 行为型设计模式 观察者模式

缘起

最近复习设计模式
拜读谭勇德的<<设计模式就该这样学>>
本系列笔记拟采用golang练习之

观察者模式

观察者模式(Observer Pattern)又叫作发布-订阅(Publish/Subscribe)模式、
模型-视图(Model/View)模式、
源-监听器(Source/Listener)模式, 
或从属者(Dependent)模式。
定义一种一对多的依赖关系,
一个主题对象可被多个观察者对象同时监听,
使得每当主题对象状态变化时,
所有依赖它的对象都会得到通知并被自动更新,
属于行为型设计模式。

(摘自 谭勇德 <<设计模式就该这样学>>)

场景

  • 某智能app, 需添加自定义闹铃的功能
  • 闹铃可设定时间, 以及是否每日重复
  • 可设定多个闹铃
  • 根据观察者模式, 每个闹铃对象, 都是时间服务的观察者, 监听时间变化的事件.

设计

  • ITimeService: 定义时间服务的接口, 接受观察者的注册和注销
  • ITimeObserver: 定义时间观察者接口, 接收时间变化事件的通知
  • tMockTimeService: 虚拟的时间服务, 自定义时间倍率以方便时钟相关的测试
  • AlarmClock: 闹铃的实现类, 实现ITimeObserver接口以订阅时间变化通知

单元测试

observer_pattern_test.go, 定义了一个临时会议的一次性闹铃, 以及一系列日常作息的重复闹铃.

package behavioral_patterns

import (
    "learning/gooop/behavioral_patterns/observer"
    "testing"
    "time"
)

func Test_ObserverPattern(t *testing.T) {
    _ = observer.NewAlarmClock("下午开会", 14,30, false)

    _ = observer.NewAlarmClock("起床", 6,0, true)
    _ = observer.NewAlarmClock("午饭", 12,30, true)
    _ = observer.NewAlarmClock("午休", 13,0, true)
    _ = observer.NewAlarmClock("晚饭", 18,30, true)
    clock := observer.NewAlarmClock("晚安", 22,0, true)


    for {
        if clock.Occurs() >= 2 {
            break
        }
        time.Sleep(time.Second)
    }
}

测试输出

$ go test -v observer_pattern_test.go 
=== RUN   Test_ObserverPattern
下午开会.next = 2021-02-11 14:30:00
起床.next = 2021-02-12 06:00:00
午饭.next = 2021-02-11 12:30:00
午休.next = 2021-02-11 13:00:00
晚饭.next = 2021-02-11 18:30:00
晚安.next = 2021-02-11 22:00:00
2021-02-11 11:51:05 时间=2021-02-11 12:30:04 闹铃 午饭
2021-02-11 11:51:06 时间=2021-02-11 13:00:04 闹铃 午休
2021-02-11 11:51:09 时间=2021-02-11 14:30:04 闹铃 下午开会
2021-02-11 11:51:17 时间=2021-02-11 18:30:04 闹铃 晚饭
2021-02-11 11:51:24 时间=2021-02-11 22:00:04 闹铃 晚安
2021-02-11 11:51:40 时间=2021-02-12 06:00:04 闹铃 起床
2021-02-11 11:51:53 时间=2021-02-12 12:30:04 闹铃 午饭
2021-02-11 11:51:54 时间=2021-02-12 13:00:04 闹铃 午休
2021-02-11 11:52:05 时间=2021-02-12 18:30:04 闹铃 晚饭
2021-02-11 11:52:12 时间=2021-02-12 22:00:04 闹铃 晚安
--- PASS: Test_ObserverPattern (69.01s)
PASS
ok      command-line-arguments  69.012s

ITimeService.go

定义时间服务的接口, 接受观察者的注册和注销

package observer

type ITimeService interface {
    Attach(observer ITimeObserver)
    Detach(id string)
}

ITimeObserver.go

定义时间观察者接口, 接收时间变化事件的通知

package observer

import "time"

type ITimeObserver interface {
    ID() string
    TimeElapsed(now *time.Time)
}

tMockTimeService.go

虚拟的时间服务, 自定义时间倍率以方便时钟相关的测试

package observer

import (
    "sync"
    "sync/atomic"
    "time"
)

type tMockTimeService struct {
    observers map[string]ITimeObserver
    rwmutex *sync.RWMutex
    speed int64
    state int64
}

func NewMockTimeService(speed int64) ITimeService {
    it := &tMockTimeService{
        observers: make(map[string]ITimeObserver, 0),
        rwmutex: new(sync.RWMutex),
        speed: speed,
        state: 0,
    }
    it.Start()
    return it
}

func (me *tMockTimeService) Start() {
    if !atomic.CompareAndSwapInt64(&(me.state), 0, 1) {
        return
    }

    go func() {
        timeFrom := time.Now()
        timeOffset := timeFrom.UnixNano()

        for range time.Tick(time.Duration(100)*time.Millisecond) {
            if me.state == 0 {
                break
            }

            nanos := (time.Now().UnixNano() - timeOffset) * me.speed
            t := timeFrom.Add(time.Duration(nanos) * time.Nanosecond)

            me.NotifyAll(&t)
        }
    }()
}


func (me *tMockTimeService) NotifyAll(now *time.Time) {
    me.rwmutex.RLock()
    defer me.rwmutex.RUnlock()

    for _,it := range me.observers {
        go it.TimeElapsed(now)
    }
}


func (me *tMockTimeService) Attach(it ITimeObserver) {
    me.rwmutex.Lock()
    defer me.rwmutex.Unlock()

    me.observers[it.ID()] = it
}


func (me *tMockTimeService) Detach(id string) {
    me.rwmutex.Lock()
    defer me.rwmutex.Unlock()

    delete(me.observers, id)
}

var GlobalTimeService = NewMockTimeService(1800)

AlarmClock.go

闹铃的实现类, 实现ITimeObserver接口以订阅时间变化通知

package observer

import (
    "fmt"
    "sync/atomic"
    "time"
)

type AlarmClock struct {
    id string
    name string
    hour time.Duration
    minute time.Duration
    repeatable bool
    next *time.Time
    occurs int
}

var gClockID int64 = 0
func newClockID() string {
    id := atomic.AddInt64(&gClockID, 1)
    return fmt.Sprintf("AlarmClock-%d", id)
}

func NewAlarmClock(name string, hour int, minute int, repeatable bool) *AlarmClock {
    it := &AlarmClock{
        id: newClockID(),
        name: name,
        hour: time.Duration(hour),
        minute: time.Duration(minute),
        repeatable: repeatable,
        next: nil,
        occurs: 0,
    }
    it.next = it.NextAlarmTime()
    GlobalTimeService.Attach(it)

    return it
}

func (me *AlarmClock) NextAlarmTime() *time.Time {
    now := time.Now()
    today, _ := time.ParseInLocation("2006-01-02 15:04:05", fmt.Sprintf("%s 00:00:00", now.Format("2006-01-02")), time.Local)
    t := today.Add(me.hour *time.Hour).Add(me.minute * time.Minute)
    if t.Unix() < now.Unix() {
        t = t.Add(24*time.Hour)
    }
    fmt.Printf("%s.next = %s\n", me.name, t.Format("2006-01-02 15:04:05"))
    return &t
}

func (me *AlarmClock) ID() string {
    return me.name
}

func (me *AlarmClock) TimeElapsed(now *time.Time) {
    it := me.next
    if it == nil {
        return
    }

    if now.Unix() >= it.Unix() {
        me.occurs++
        fmt.Printf("%s 时间=%s 闹铃 %s\n", time.Now().Format("2006-01-02 15:04:05"), now.Format("2006-01-02 15:04:05"), me.name)

        if me.repeatable {
            t := me.next.Add(24*time.Hour)
            me.next = &t

        } else {
            GlobalTimeService.Detach(me.ID())
        }
    }
}

func (me *AlarmClock) Occurs() int {
    return me.occurs
}

观察者模式小结

观察者模式的优点
(1)观察者和被观察者是松耦合(抽象耦合)的,符合依赖倒置原则。
(2)分离了表示层(观察者)和数据逻辑层(被观察者),
    并且建立了一套触发机制,使得数据的变化可以响应到多个表示层上。
(3)实现了一对多的通信机制,支持事件注册机制,支持兴趣分发机制,
    当被观察者触发事件时,只有感兴趣的观察者可以接收到通知。

观察者模式的缺点
(1)如果观察者数量过多,则事件通知会耗时较长。
(2)事件通知呈线性关系,如果其中一个观察者处理事件卡壳,则会影响后续的观察者接收该事件。
(3)如果观察者和被观察者之间存在循环依赖,则可能造成两者之间的循环调用,导致系统崩溃。

(摘自 谭勇德 <<设计模式就该这样学>>)

(end)

阅读 811

想当将军 首先要肯打

42 声望
15 粉丝
0 条评论
你知道吗?

想当将军 首先要肯打

42 声望
15 粉丝
宣传栏