תבנית Observer
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 89 מתוך 107.
תבנית Observer מגדירה קשר של אחד לרבים בין אובייקטים. כאשר אובייקט אחד (הנושא) משנה מצב, כל התלויים בו (המשקיפים) מקבלים הודעה באופן אוטומטי. התבנית מתאימה במיוחד למערכות אירועים, להתראות או לכל תרחיש שבו כמה רכיבים צריכים להגיב לשינויים.
ב-Go, אנחנו מממשים את התבנית הזו באמצעות ממשק עבור צופים ומבנה subject שמנהל הרשמות:
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: ההזמנה בוצעה
// יומן: ההזמנה בוצעהתבנית זו מנתקת את הנושא מהמשקיפים שלו, וכך מקלה על הוספת סוגים חדשים של משקיפים בלי לשנות קוד קיים. משתמשים בה לעיתים קרובות בארכיטקטורות מונחות אירועים, במסגרות עבודה של ממשקי משתמש גרפיים ובמערכות העברת הודעות מסוג פרסום/הרשמה.
אתגר
קלבואו נבנה מערכת להתראות על מחירי מניות באמצעות תבנית Observer! תיצרו מערכת שבה כמה שירותי התראות מקבלים באופן אוטומטי הודעות בכל פעם שמחיר מניה משתנה — מושלם להדגמה של האופן שבו משקיפים יכולים להגיב לאירועים בלי שהנושא יכיר את הטיפוסים הקונקרטיים שלהם.
תארגנו את הקוד שלכם בשלושה קבצים:
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: צור Stock חדש באמצעות הבנאי NewStock
// 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: הדפס כל תגובה בשורה נפרדת
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות OOP ב-Go
קבצים חיצונייםסביבת העבודה והמודולים של Goחבילות וייבואשמות מיוצאים לעומת שמות לא מיוצאיםמבוא ל-OOP ב-GoStructs כמחלקותהגדרת מתודות עבור Structsמקבלי Pointer לעומת Valueאתחול Structפונקציות Constructorחזרה – מחשבון פשוט4ממשקים
מבוא לממשקיםמימוש משתמעממשק כחוזהממשק ריק (any)אימות טיפוסמתג טיפוסיםהרכבת ממשקיםממשקי Stringer ו-Errorסיכום – מחשבון צורות7כימוס
שדות מיוצאים לעומת שדות לא מיוצאיםכימוס ברמת החבילהמתודות Getter ו-Setterהסתרת מידע ב-Goסיכום – רשומות תלמידים10Generics (Go 1.18 ומעלה)
מבוא ל־Genericsפרמטרים של טיפוסיםאילוצי טיפוסיםמבנים גנרייםפתרון עוקף למתודות גנריותסיכום – אוסף גנרי13תבניות עיצוב חלק 1
מבוא לתבניות עיצובתבנית Singletonתבנית Factoryתבנית Abstract Factoryתבנית Observerתבנית Strategy2העמקה בסוגים וב-Structs
סוגים בסיסיים ומורכביםהגדרות של סוגים מותאמים אישיתתגיות StructStructs אנונימייםStructs מקונניםערכי אפס וברירות מחדלסיכום – ספר אנשי קשר5קומפוזיציה במקום ירושה
למה ב-Go אין ירושהיסודות הטמעת Structקידום מתודותהטמעת כמה Structsהטמעה לעומת אגרגציההסתרת מתודות מוטמעותסיכום – היררכיית עובדים8טיפול בשגיאות ו-OOP
ממשק errorסוגי שגיאות מותאמים אישיתעטיפת שגיאות (fmt.Errorf)שגיאות Sentinelerrors.Is() ו-errors.As()Panic, Defer ו-Recoverסיכום – מנתח קבצים11ספרייה סטנדרטית ותכנות מונחה עצמים
io.Reader ו-io.Writersort.Interfaceהממשק fmt.Stringerencoding/json עם מבניםהממשק http.Handlerסיכום – מודלים של REST API3מצביעים וזיכרון
יסודות המצביעים ב-Goמצביעים למבניםהעברה לפי ערך לעומת לפי הפניההפונקציה new()איסוף אשפה ב-Goסיכום - בונה רשימה מקושרת6פולימורפיזם ב-Go
פולימורפיזם באמצעות ממשקיםטיפוס ברווז ב-Goכללי התאמה לממשקאוספים פולימורפייםהזרקת תלויותסיכום - מעבד תשלומים9מקביליות ו-OOP
יסודות GoroutinesChannels ותקשורתChannel עם חוצץ לעומת ללא חוצץפקודת Selectsync.Mutex ו-sync.RWMutexsync.WaitGroupתכנון Struct בטוח לשימוש במקבילסיכום - מאגר עובדים12מושגים מתקדמים בתכנות מונחה עצמים
דפוס האפשרויות הפונקציונליותדפוס Builder ב-Goשרשור מתודותכינויי טיפוסים לעומת הגדרותיסודות Reflectionסקירה כללית של יצירת קודתרגלו בעצמכם: קומפיילר Go אונליין