观察者模式
本课属于 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 框架和发布/订阅消息系统。
挑战
简单让我们使用观察者模式来构建一个股票价格提醒系统!你将创建一个系统,让多个提醒服务在股票价格发生变化时自动收到通知。这非常适合用来演示观察者如何响应事件,而主题无需知道观察者的具体类型。
你将把代码分成 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: 每个响应单独打印一行
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器