כללי התאמה לממשק
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 43 מתוך 107.
אף ש-Go משתמשת בטיפוסיות ברווז כדי לממש ממשקים באופן מרומז, יש כללים מסוימים שקובעים אם טיפוס אכן מממש ממשק. הבנת הכללים האלה עוזרת לך להימנע מבאגים חמקמקים.
הכלל החשוב ביותר נוגע למקבלי מצביע. אם מתודה מוגדרת עם מקבל מצביע, רק מצביע לטיפוס הזה מממש את הממשק, ולא הערך עצמו:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // מקבל מצביע
return "Saved: " + d.Name
}
func Process(s Saver) {
fmt.Println(s.Save())
}
מכיוון של־Save() יש מקבל מצביע, רק *Document מקיים את Saver:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // עובד - המצביע מממש את הממשק
// Process(doc) // שגיאת קומפילציה - הערך אינו מממש את הממשק
}
עם זאת, הכיוון ההפוך גמיש יותר. אם למתודה יש מקבל ערך, גם טיפוסי הערך וגם טיפוסי המצביע עומדים בדרישות הממשק. Go מבצעת באופן אוטומטי ביטול הפניה למצביעים בעת קריאה למתודות עם מקבל ערך:
func (d Document) Info() string { // מקבל ערך
return d.Name
}
// גם Document וגם *Document מממשים ממשק שדורש Info()
חוסר הסימטריה הזה קיים משום ש-Go תמיד יכולה לקבל ערך ממצביע (באמצעות ביטול הפניה), אך היא לא תמיד יכולה לקבל מצביע מערך (ייתכן שאי אפשר לקבל את הכתובת של הערך). זכירת הכלל הזה מונעת שגיאות מהדר מבלבלות בעבודה עם ממשקים.
אתגר
קלנבנה מערכת תצורה שמדגימה כיצד מקבלי מצביע ומקבלי ערך משפיעים על התאמה לממשק. תיצרו טיפוסים שבהם בחירת המקבל קובעת אם אפשר להשתמש בערכים, במצביעים או בשניהם עם ממשק.
תארגנו את הקוד שלכם בשלושה קבצים:
config.go: הגדירו ממשקConfigurableשדורש שתי מתודות:GetValue() stringו-SetValue(string). לאחר מכן צרו שני טיפוסי תצורה:ReadOnlyConfigעם שדהValue: השתמשו במקבל ערך עבורGetValue()(מחזירה את Value) ובמקבל מצביע עבורSetValue()(מעדכנת את Value)Settingעם שדהData: השתמשו במקבלי ערך עבור שתי המתודות (GetValue מחזירה את Data, ואילו SetValue רק מדפיסה "Cannot modify" בלי לשנות דבר)
processor.go: צרו פונקציה בשםProcessConfigשמקבלתConfigurableומחרוזת של ערך חדש. עליה להדפיס את הערך הנוכחי באמצעותGetValue(), לקרוא ל-SetValue()עם הערך החדש, ואז להדפיס שוב את הערך כדי להציג שינויים כלשהם.main.go: קראו את פרטי התצורה מהקלט והדגימו את כללי ההתאמה לממשק:- צרו
ReadOnlyConfigוהעבירו מצביע ל-ProcessConfig(נדרש משום של-SetValueיש מקבל מצביע) - צרו
Settingוהעבירו את הערך ישירות ל-ProcessConfig(עובד משום שלשתי המתודות יש מקבלי ערך)
- צרו
הקלט הבא יסופק:
- שורה 1: הערך ההתחלתי של ReadOnlyConfig
- שורה 2: הערך החדש שיש להגדיר עבור ReadOnlyConfig
- שורה 3: הערך ההתחלתי של Setting
- שורה 4: הערך החדש שיש לנסות להגדיר עבור Setting
הפונקציה ProcessConfig שלכם צריכה להדפיס בפורמט הזה:
Current: [value]
Current: [value after SetValue]לדוגמה, עבור debug, production, localhost ו-remote, הפלט שלכם צריך להיות:
Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhostשימו לב ש-ReadOnlyConfig אכן משנה את הערך שלו (משום שהעברנו מצביע), ואילו Setting נשאר ללא שינוי (ה-SetValue שלו, עם מקבל ערך, לא יכול לשנות את המקור). התובנה המרכזית היא שערכי ReadOnlyConfig לבדם לא מתאימים לממשק Configurable; רק מצביעים מתאימים לו. לעומת זאת, ערכי Setting עובדים ישירות, משום שכל המתודות שלו משתמשות במקבלי ערך.
נסו בעצמכם
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// קראו את הקלטים
scanner.Scan()
rocInitial := scanner.Text()
scanner.Scan()
rocNew := scanner.Text()
scanner.Scan()
settingInitial := scanner.Text()
scanner.Scan()
settingNew := scanner.Text()
// TODO: צרו ReadOnlyConfig עם הערך של rocInitial
// TODO: העבירו מצביע ל-ProcessConfig (נדרש כי ל-SetValue יש מקבל מסוג מצביע)
// TODO: צרו Setting עם הערך של settingInitial
// TODO: העבירו את הערך ישירות ל-ProcessConfig (עובד כי לשתי המתודות יש מקבלים מסוג ערך)
// השתמשו במשתנים האלה כדי להימנע משגיאות על משתנים שלא נעשה בהם שימוש
_ = rocInitial
_ = rocNew
_ = settingInitial
_ = settingNew
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
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 אונליין