תבנית Singleton
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 86 מתוך 107.
תבנית Singleton מבטיחה שלטיפוס תהיה רק מופע אחד לאורך כל התוכנית שלך, ומספקת נקודת גישה גלובלית אליו. היא שימושית למשאבים משותפים כמו מנהלי תצורה, חיבורי מסד נתונים או רושמי יומן.
ב-Go, אנחנו מממשים Singleton באמצעות משתנה ברמת החבילה בשילוב עם sync.Once כדי להבטיח אתחול בטוח לשרשורים:
package config
import "sync"
type Config struct {
DatabaseURL string
MaxRetries int
}
var (
instance *Config
once sync.Once
)
func GetInstance() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: "localhost:5432",
MaxRetries: 3,
}
})
return instance
}ה־sync.Once מבטיח שפונקציית האתחול תרוץ בדיוק פעם אחת, גם כשכמה goroutines קוראות ל־GetInstance() בו־זמנית. כל קריאה נוספת מחזירה את אותו מופע בלי להריץ מחדש את קוד האתחול.
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 ו-cfg2 מצביעים על אותו מופעהשתמשו ב-Singleton במשורה. אף שהוא נוח, הוא מוסיף מצב גלובלי שעלול להקשות על בדיקות ולהסתיר תלויות. שקלו אם הזרקת תלויות עשויה להיות חלופה טובה יותר עבור המקרה שלכם.
אתגר
קלבואו נבנה Logger יחידני שמבטיח שקיימת רק מופע אחד של רכיב הרישום באפליקציה שלכם! זהו מקרה שימוש קלאסי לדפוס Singleton. רוצים שכל חלקי התוכנית שלכם ישתפו את אותה תצורה ואת אותו מצב של רכיב הרישום.
תארגנו את הקוד שלכם בשני קבצים:
logger.go: ממשו רכיב רישום יחידני בטוח לשימוש במקביל.צרו מבנה
Loggerעם שני שדות:Prefix(מחרוזת) עבור קידומות של הודעות הרישום, ו-MessageCount(int) כדי לעקוב אחר מספר ההודעות שנרשמו.השתמשו במשתנים ברמת החבילה עם
sync.Onceכדי להבטיח שנוצר רק מופע אחד. ממשו פונקצייתGetLogger()שמאתחלת את רכיב הרישום עם קידומת ברירת המחדל"[LOG]"ומחזירה את המופע היחידני.הוסיפו את המתודות הבאות ל-Logger:
SetPrefix(prefix string)- משנה את הקידומת של רכיב הרישוםLog(message string) string- מגדילה את מספר ההודעות ומחזירה מחרוזת בפורמט:[prefix] #[count]: [message]GetCount() int- מחזירה את המספר הכולל של ההודעות שנרשמו
main.go: הדגימו שקריאות מרובות ל-GetLogger()מחזירות את אותו המופע.קראו ערך חדש של קידומת, ואז קראו את מספר ההודעות ולאחריו את כל אחת מההודעות לרישום.
קבלו את מופע רכיב הרישום, הגדירו את הקידומת המותאמת אישית, ואז רשמו כל הודעה והדפיסו את התוצאה. לאחר רישום כל ההודעות, קבלו שוב את מופע רכיב הרישום (כדי להוכיח שזה אותו מופע) והדפיסו את המספר הכולל של ההודעות.
הקלטים הבאים יסופקו:
- שורה 1: הקידומת המותאמת אישית להגדרה
- שורה 2: מספר ההודעות
- השורות הבאות: כל הודעה לרישום
לדוגמה, עבור:
[APP]
3
Server started
User connected
Request processedהפלט שלכם אמור להיות:
[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3ועבור:
[DEBUG]
2
Initializing cache
Cache readyהפלט שלכם אמור להיות:
[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2ועבור:
[ERROR]
1
Connection failedהפלט שלכם אמור להיות:
[ERROR] #1: Connection failed
Total messages logged: 1התובנה המרכזית היא שלא משנה כמה פעמים תקראו ל-GetLogger(), תמיד תקבלו את אותו המופע עם המצב המשותף שלו. מספר ההודעות נשמר כי קיים רק Logger אחד!
נסו בעצמכם
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// קראו את הקידומת המותאמת אישית
prefix, _ := reader.ReadString('\n')
prefix = strings.TrimSpace(prefix)
// קראו את מספר ההודעות
countStr, _ := reader.ReadString('\n')
countStr = strings.TrimSpace(countStr)
count, _ := strconv.Atoi(countStr)
// קראו כל הודעה
messages := make([]string, count)
for i := 0; i < count; i++ {
msg, _ := reader.ReadString('\n')
messages[i] = strings.TrimSpace(msg)
}
// TODO: קבלו את מופע ה-logger באמצעות GetLogger()
// TODO: הגדירו את הקידומת המותאמת אישית באמצעות SetPrefix()
// TODO: רשמו כל הודעה ביומן והדפיסו את התוצאה
// TODO: קבלו שוב את מופע ה-logger (כדי להוכיח שזה אותו מופע)
// והדפיסו את מספר ההודעות הכולל בפורמט: "Total messages logged: X"
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
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 אונליין