Menu
CoddyTech

观察者模式

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 89 课,共 107 课。

观察者模式定义了对象之间的一对多关系。当一个对象(主题)的状态发生变化时,它的所有依赖对象(观察者)都会自动收到通知。这种模式非常适合用于事件系统、通知,或任何需要多个组件响应变化的场景。

在 Go 中,我们使用观察者接口和管理订阅的主题结构体来实现这种模式:

type Observer interface {
    Update(event string)
}

type Subject struct {
    observers []Observer
}

func (s *Subject) Subscribe(o Observer) {
    s.observers = append(s.observers, o)
}

func (s *Subject) Notify(event string) {
    for _, observer := range s.observers {
        observer.Update(event)
    }
}

任何实现了 Observer 接口的类型都可以订阅以接收通知:

type EmailAlert struct{ Address string }

func (e EmailAlert) Update(event string) {
    fmt.Printf("Email to %s: %s\n", e.Address, event)
}

type Logger struct{}

func (l Logger) Update(event string) {
    fmt.Println("Log:", event)
}

主体会通知所有观察者,而无需知道它们的具体类型:

subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})

subject.Notify("Order placed")
// 发送给 user@example.com 的邮件:订单已提交
// 日志:订单已提交

这种模式将主题与观察者解耦,因此无需修改现有代码,就能轻松添加新的观察者类型。它常用于事件驱动架构、GUI 框架和发布/订阅消息系统。

challenge icon

挑战

简单

让我们使用观察者模式来构建一个股票价格提醒系统!你将创建一个系统,让多个提醒服务在股票价格发生变化时自动收到通知。这非常适合用来演示观察者如何响应事件,而主题无需知道观察者的具体类型。

你将把代码分成 3 个文件:

  • observer.go:定义观察者接口和具体的观察者类型。

    创建一个 Observer 接口,其中包含方法 Update(stockName string, price float64),在价格变化时接收股票信息。

    实现两种观察者类型:

    • PriceAlert,包含一个 Threshold 字段(float64)。更新时,如果价格高于阈值,则返回 ALERT: [stockName] at $[price] crossed threshold $[threshold];如果低于阈值,则返回 WATCHING: [stockName] at $[price] (threshold: $[threshold])
    • PriceLogger,包含一个 LogName 字段(string)。更新时,返回 [logName] recorded [stockName]: $[price]

    两个观察者都应有一个方法,用于返回格式化后的响应字符串。

  • stock.go:创建一个用于管理观察者并在价格变化时通知它们的主题。

    创建一个 Stock 结构体,包含 Name(string)、Price(float64)和观察者切片字段。添加以下方法:

    • Subscribe(o Observer),用于添加观察者
    • SetPrice(price float64) []string,用于更新价格并通知所有观察者,然后返回包含它们所有响应的切片

    还要创建一个 NewStock(name string, initialPrice float64) *Stock 构造函数。

  • main.go:为股票设置观察者,并模拟价格变化。

    读取股票名称和初始价格。然后读取要创建的观察者数量。对于每个观察者,读取其类型(alert 或 logger)及其配置(提醒器的阈值,记录器的日志名称)。将每个观察者订阅到股票。

    最后,读取一个新价格,更新股票价格,并将每个观察者的响应分别打印在单独一行。

输入内容如下:

  • 第 1 行:股票名称
  • 第 2 行:初始价格
  • 第 3 行:观察者数量
  • 每个观察者:类型(alert 或 logger),然后是配置值
  • 最后一行:要设置的新价格

所有价格都保留两位小数。

例如,给定以下输入:

GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50

输出应为:

ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00

再给定以下输入:

AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25

输出应为:

DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)

注意,Stock 不知道自己通知的是提醒器还是记录器。它只会对每个观察者调用 Update。这种解耦方式让你可以轻松添加新的观察者类型,而无需修改 Stock 代码!

亲手试试

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// 读取股票名称
	stockName, _ := reader.ReadString('\n')
	stockName = strings.TrimSpace(stockName)

	// 读取初始价格
	initialPriceStr, _ := reader.ReadString('\n')
	initialPrice, _ := strconv.ParseFloat(strings.TrimSpace(initialPriceStr), 64)

	// 读取观察者数量
	numObserversStr, _ := reader.ReadString('\n')
	numObservers, _ := strconv.Atoi(strings.TrimSpace(numObserversStr))

	// TODO:使用 NewStock 构造函数创建一个新的 Stock

	// TODO:遍历每个观察者
	for i := 0; i < numObservers; i++ {
		// 读取观察者类型("alert" 或 "logger")
		observerType, _ := reader.ReadString('\n')
		observerType = strings.TrimSpace(observerType)

		// 读取配置值
		configValue, _ := reader.ReadString('\n')
		configValue = strings.TrimSpace(configValue)

		// TODO:根据类型创建相应的观察者
		// - 对于 "alert":将 configValue 解析为 float64 类型的阈值,创建 PriceAlert
		// - 对于 "logger":将 configValue 用作 LogName,创建 PriceLogger
		// TODO:将观察者订阅到股票
	}

	// 读取新价格
	newPriceStr, _ := reader.ReadString('\n')
	newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)

	// TODO: 设置新价格并获取响应

	// TODO: 每个响应单独打印一行
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器