הזרקת תלויות
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 45 מתוך 107.
הזרקת תלויות היא טכניקה שבה מבנה מקבל את התלויות שלו מבחוץ במקום ליצור אותן בעצמו. ב-Go, ממשקים הופכים את התבנית הזו לטבעית ועוצמתית.
במקום לקבע בקוד מימוש ספציפי בתוך struct, מקבלים ממשק. כך אפשר להחליף מימושים בלי לשנות את הקוד של ה־struct:
type Notifier interface {
Send(message string) string
}
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
עכשיו צרו מבנה שתלוי בממשק, ולא בטיפוס קונקרטי:
type OrderService struct {
notifier Notifier // תלות המוזרקת באמצעות ממשק
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
ל־OrderService לא ידוע ולא אכפת אם הוא משתמש בדוא״ל או ב־SMS. מזריקים את התלות בעת יצירת השירות:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: ההזמנה בוצעה: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: ההזמנה בוצעה: Phone
}
הגישה הזו הופכת את הקוד שלך לגמיש יותר וקל יותר לבדיקה. במהלך הבדיקות, אפשר להזריק notifier מדומה שלא שולח הודעות בפועל. בסביבת הייצור מזריקים את המימוש האמיתי. המבנה נשאר ללא שינוי בשני המקרים.
אתגר
קלבואו נבנה מערכת רישום (logging) שמדגימה את העוצמה של הזרקת תלויות. תיצרו שירות שיכול לכתוב יומנים ליעדים שונים, בלי שהשירות ידע או יתעניין לאן היומנים האלה נשלחים בפועל.
תארגנו את הקוד שלכם בשלושה קבצים:
logger.go: הגדירו ממשקLoggerשדורש מתודהLog(message string) string. לאחר מכן צרו שתי מימושים שונים של רושמי יומנים:ConsoleLoggerעם שדהPrefix. המתודהLogשלו מחזירה[CONSOLE] [Prefix]: [message]FileLoggerעם שדהFilename. המתודהLogשלו מחזירה[FILE:[Filename]] [message]
service.go: צרו מבנהAppServiceשתלוי בממשקLogger(ולא בטיפוס קונקרטי). כללו פונקציית בנאיNewAppServiceשמקבלתLoggerומחזירה מצביע ל-AppService. הוסיפו ל-AppServiceמתודה בשםDoWork(task string) stringשמשתמשת ברושם היומנים שהוזרק כדי לרשום את ההודעהProcessing: [task]ומחזירה את מה שרושם היומנים מחזיר.main.go: קראו את התצורה מהקלט, צרו את שני סוגי רושמי היומנים, הזריקו כל אחד מהם למופעAppServiceנפרד, וקראו ל-DoWorkבכל שירות עם המשימה שסופקה. הדפיסו את התוצאה של כל קריאה לשירות.
הקלטים הבאים יסופקו:
- שורה 1: הקידומת של רושם היומנים למסוף
- שורה 2: שם הקובץ של רושם היומנים לקובץ
- שורה 3: המשימה לעיבוד
לדוגמה, עבור INFO, app.log ו-user authentication, הפלט שלכם אמור להיות:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationשימו לב ש-AppService לא יודע אם הוא רושם יומנים למסוף או לקובץ. הוא פשוט קורא ל-Log בכל רושם יומנים שהוזרק אליו. הגמישות הזאת היא המהות של הזרקת תלויות: אותו קוד שירות עובד עם מימושים שונים לחלוטין של רישום יומנים.
נסו בעצמכם
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// קרא את הקידומת של רושם הלוגים למסוף
prefix, _ := reader.ReadString('\n')
prefix = prefix[:len(prefix)-1]
// קרא את שם הקובץ של רושם הלוגים לקובץ
filename, _ := reader.ReadString('\n')
filename = filename[:len(filename)-1]
// קרא את המשימה לעיבוד
task, _ := reader.ReadString('\n')
if len(task) > 0 && task[len(task)-1] == '\n' {
task = task[:len(task)-1]
}
// TODO: צור ConsoleLogger עם הקידומת
// TODO: צור FileLogger עם שם הקובץ
// TODO: צור AppService והזרק אליו את ConsoleLogger
// TODO: צור AppService נוסף והזרק אליו את FileLogger
// TODO: קרא ל-DoWork בכל אחד מהשירותים עם המשימה והדפס את התוצאות
fmt.Println("result1")
fmt.Println("result2")
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות OOP ב-Go
קבצים חיצונייםסביבת העבודה והמודולים של Goחבילות וייבואשמות מיוצאים לעומת שמות לא מיוצאיםמבוא ל-OOP ב-GoStructs כמחלקותהגדרת מתודות עבור Structsמקבלי Pointer לעומת Valueאתחול Structפונקציות Constructorחזרה – מחשבון פשוט4ממשקים
מבוא לממשקיםמימוש משתמעממשק כחוזהממשק ריק (any)אימות טיפוסמתג טיפוסיםהרכבת ממשקיםממשקי Stringer ו-Errorסיכום – מחשבון צורות7כימוס
שדות מיוצאים לעומת שדות לא מיוצאיםכימוס ברמת החבילהמתודות Getter ו-Setterהסתרת מידע ב-Goסיכום – רשומות תלמידים10Generics (Go 1.18 ומעלה)
מבוא ל־Genericsפרמטרים של טיפוסיםאילוצי טיפוסיםמבנים גנרייםפתרון עוקף למתודות גנריותסיכום – אוסף גנרי2העמקה בסוגים וב-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 אונליין