למה ב-Go אין ירושה
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 34 מתוך 107.
שפות מסורתיות מונחות עצמים כמו Java ו-C++ משתמשות בירושה כדי לשתף קוד בין טיפוסים. מחלקת בת מרחיבה מחלקת אב ויורשת את כל השדות והמתודות שלה. Go משמיטה תכונה זו במכוון.
ירושה יוצרת צימוד הדוק בין טיפוסים. כשמחלקת אב משתנה, כל מחלקות הבת מושפעות.
היררכיות ירושה עמוקות נעשות קשות להבנה ולתחזוקה. ״בעיית מחלקת הבסיס השברירית״ מתרחשת כאשר שינויים במחלקת בסיס שוברים באופן בלתי צפוי מחלקות נגזרות.
המעצבים של Go בחרו בדרך אחרת: קומפוזיציה במקום הורשה. במקום לומר "כלב הוא חיה", Go מעודדת אותך לומר "לכלב יש התנהגויות דמויות־חיה." השינוי העדין הזה מוביל לקוד גמיש יותר וקל יותר לתחזוקה.
Go משיגה שימוש חוזר בקוד באמצעות שני מנגנונים שכבר למדת:
- ממשקים מגדירים חוזי התנהגות ללא פרטי מימוש
- הטמעת מבנים מאפשרת לטיפוסים לכלול טיפוסים אחרים ולעשות שימוש חוזר במתודות שלהם
חשבו על ההשוואה הזאת. ב-OOP מסורתי, ייתכן שתכתבו class Dog extends Animal. ב-Go, משלבים מבנה Animal בתוך Dog ומממשים ממשקים משותפים. התוצאה היא פונקציונליות דומה עם צימוד רופף יותר בין טיפוסים.
בפרק הזה נבחן לעומק הטמעת מבנים, ונראה כיצד Go משיגה את היתרונות של ירושה בלי החסרונות שלה.
אתגר
קלבואו נבנה מערכת התראות שמדגימה את גישת ההרכבה של Go במקום ירושה. תיצרו טיפוסים שמשתפים התנהגות באמצעות ממשקים והטמעת מבנים, ולא באמצעות היררכיות של מחלקות.
תארגנו את הקוד שלכם בשלושה קבצים:
notifier.go: הגדירו ממשקNotifierעם מתודה אחת,Notify(message string) string. צרו גם מבנהBaseNotifierעם שדהName, שיוטמע בטיפוסים אחרים. הוסיפו ל-BaseNotifierמתודה בשםFormat(message string) string, שמחזירה את ההודעה כששם מודיע ההתראות מופיע לפניה בסוגריים מרובעים.channels.go: צרו שני טיפוסים של ערוצי התראות שמטמיעים אתBaseNotifierומממשים את הממשקNotifier:EmailNotifierעם שדה נוסף בשםAddressSMSNotifierעם שדה נוסף בשםPhone
Notifyשל כל טיפוס צריכה להשתמש במתודתFormatהמוטמעת ולכלול בפלט את המידע הספציפי לערוץ שלו.main.go: צרו פונקציה בשםSendAlertשמקבלת כלNotifierוהודעה, ואז מחזירה את התוצאה של הקריאה ל-Notify. קראו את פרטי ההתראות מהקלט, צרו את שני טיפוסי מודיעי ההתראות והדגימו כיצד אפשר להשתמש בהם לסירוגין באמצעות הממשק.
הקלט שיסופק יהיה בפורמט הבא:
- שורה 1: שם מודיע ההתראות עבור אימייל
- שורה 2: כתובת אימייל
- שורה 3: שם מודיע ההתראות עבור SMS
- שורה 4: מספר טלפון
- שורה 5: הודעת התראה
מתודת Format שלכם ב-BaseNotifier צריכה להחזיר:
[Name] messageמתודות Notify שלכם צריכות להחזיר:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
לדוגמה, בהינתן Alerts, user@mail.com, Urgent, 555-1234 ו-Server down, הפלט שלכם צריך להיות:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downשימו לב ששני טיפוסי מודיעי ההתראות משתמשים מחדש במתודת Format מתוך BaseNotifier באמצעות הטמעה, בעוד שכל אחד מהם מספק מימוש משלו של Notify. הפונקציה SendAlert עובדת עם כל Notifier בלי לדעת מהו הטיפוס הקונקרטי. כך נראית הרכבה במקום ירושה בפעולה.
נסו בעצמכם
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert מקבלת כל Notifier והודעה, ומחזירה את תוצאת הקריאה ל-Notify
// TODO: ממשו את הפונקציה SendAlert
func main() {
scanner := bufio.NewScanner(os.Stdin)
// קראו את שם שולח ההתראות בדוא"ל
scanner.Scan()
emailName := scanner.Text()
// קראו את כתובת הדוא"ל
scanner.Scan()
emailAddress := scanner.Text()
// קראו את שם שולח ההתראות ב-SMS
scanner.Scan()
smsName := scanner.Text()
// קראו את מספר הטלפון
scanner.Scan()
phoneNumber := scanner.Text()
// קראו את הודעת ההתראה
scanner.Scan()
alertMessage := scanner.Text()
// TODO: צרו EmailNotifier עם emailName ו-emailAddress
// TODO: צרו SMSNotifier עם smsName ו-phoneNumber
// TODO: השתמשו ב-SendAlert כדי לשלוח את alertMessage דרך שני שולחי ההתראות
// והדפיסו את התוצאות
// מנעו אזהרות על משתנים שאינם בשימוש (הסירו את השורות האלה כשתשתמשו במשתנים)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
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 אונליין