Mẫu Observer
Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 89 trên 107.
Mẫu Observer xác định mối quan hệ một với nhiều giữa các đối tượng. Khi một đối tượng (chủ thể) thay đổi trạng thái, tất cả các đối tượng phụ thuộc vào nó (đối tượng quan sát) sẽ tự động nhận thông báo. Mẫu này phù hợp với các hệ thống sự kiện, thông báo hoặc mọi tình huống cần nhiều thành phần phản hồi trước các thay đổi.
Trong Go, chúng ta triển khai mẫu này bằng cách dùng một giao diện cho các đối tượng quan sát và một cấu trúc subject quản lý các lượt đăng ký:
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)
}
}Bất kỳ kiểu nào triển khai giao diện Observer đều có thể đăng ký để nhận thông báo:
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)
}Đối tượng chủ thể thông báo cho tất cả đối tượng quan sát mà không cần biết kiểu cụ thể của chúng:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placedMẫu này tách đối tượng được theo dõi khỏi các đối tượng quan sát, giúp bạn dễ dàng thêm kiểu đối tượng quan sát mới mà không cần sửa code hiện có. Mẫu này thường được dùng trong kiến trúc hướng sự kiện, framework giao diện đồ họa và hệ thống nhắn tin pub/sub.
Thử thách
DễHãy xây dựng một hệ thống cảnh báo giá cổ phiếu bằng mẫu Observer! Bạn sẽ tạo một hệ thống để nhiều dịch vụ cảnh báo tự động nhận thông báo mỗi khi giá cổ phiếu thay đổi, rất phù hợp để minh họa cách các observer phản ứng với sự kiện mà đối tượng được theo dõi không cần biết kiểu cụ thể của chúng.
Bạn sẽ tổ chức code trong ba tệp:
observer.go: Định nghĩa interface observer và các kiểu observer cụ thể.Tạo interface
Observercó phương thứcUpdate(stockName string, price float64)để nhận thông tin cổ phiếu khi giá thay đổi.Triển khai hai kiểu observer:
PriceAlertcó trườngThreshold(float64). Khi được cập nhật, nó trả vềALERT: [stockName] at $[price] crossed threshold $[threshold]nếu giá cao hơn ngưỡng, hoặcWATCHING: [stockName] at $[price] (threshold: $[threshold])nếu giá thấp hơn ngưỡngPriceLoggercó trườngLogName(string). Khi được cập nhật, nó trả về[logName] recorded [stockName]: $[price]
Cả hai observer đều cần có phương thức trả về chuỗi phản hồi đã định dạng.
stock.go: Tạo đối tượng được theo dõi để quản lý các observer và thông báo cho chúng khi giá thay đổi.Tạo struct
Stockvới các trườngName(string),Price(float64) và một lát cắt observer. Thêm các phương thức sau:Subscribe(o Observer)để thêm một observerSetPrice(price float64) []stringđể cập nhật giá và thông báo cho tất cả observer, sau đó trả về một lát cắt chứa mọi phản hồi
Đồng thời tạo hàm khởi tạo
NewStock(name string, initialPrice float64) *Stock.main.go: Thiết lập cổ phiếu cùng các observer và mô phỏng những lần thay đổi giá.Đọc tên cổ phiếu và giá ban đầu. Sau đó đọc số lượng observer cần tạo. Với mỗi observer, đọc kiểu của nó (
alerthoặclogger) và cấu hình (ngưỡng cho cảnh báo, tên log cho logger). Đăng ký từng observer với cổ phiếu.Cuối cùng, đọc giá mới, cập nhật cổ phiếu và in phản hồi của từng observer trên một dòng riêng.
Đầu vào sẽ có định dạng như sau:
- Dòng 1: Tên cổ phiếu
- Dòng 2: Giá ban đầu
- Dòng 3: Số lượng observer
- Với mỗi observer: kiểu (
alerthoặclogger), sau đó là giá trị cấu hình - Dòng cuối: Giá mới cần đặt
Định dạng tất cả giá với hai chữ số thập phân.
Ví dụ, với đầu vào:
GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50Đầu ra của bạn sẽ là:
ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00Với đầu vào:
AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25Đầu ra của bạn sẽ là:
DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)Lưu ý rằng Stock không biết mình đang thông báo cho alert hay logger. Nó chỉ gọi Update trên từng observer. Cách tách rời này giúp bạn dễ dàng thêm kiểu observer mới mà không cần sửa code của Stock!
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc tên cổ phiếu
stockName, _ := reader.ReadString('\n')
stockName = strings.TrimSpace(stockName)
// Đọc giá ban đầu
initialPriceStr, _ := reader.ReadString('\n')
initialPrice, _ := strconv.ParseFloat(strings.TrimSpace(initialPriceStr), 64)
// Đọc số lượng đối tượng quan sát
numObserversStr, _ := reader.ReadString('\n')
numObservers, _ := strconv.Atoi(strings.TrimSpace(numObserversStr))
// TODO: Tạo Stock mới bằng hàm khởi tạo NewStock
// TODO: Lặp qua từng đối tượng quan sát
for i := 0; i < numObservers; i++ {
// Đọc loại đối tượng quan sát ("alert" hoặc "logger")
observerType, _ := reader.ReadString('\n')
observerType = strings.TrimSpace(observerType)
// Đọc giá trị cấu hình
configValue, _ := reader.ReadString('\n')
configValue = strings.TrimSpace(configValue)
// TODO: Tạo đối tượng quan sát phù hợp theo loại
// - Với "alert": chuyển configValue thành ngưỡng kiểu float64, tạo PriceAlert
// - Với "logger": dùng configValue làm LogName, tạo PriceLogger
// TODO: Đăng ký đối tượng quan sát với cổ phiếu
}
// Đọc giá mới
newPriceStr, _ := reader.ReadString('\n')
newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)
// TODO: Đặt giá mới và lấy các phản hồi
// TODO: In mỗi phản hồi trên một dòng riêng
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online