כימוס ברמת החבילה
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 48 מתוך 107.
בעוד שהשיעור הקודם עסק בנראות ברמת השדה, הכימוס ב-Go פועל למעשה ברמת החבילה. כל מה שנמצא באותה חבילה, ללא קשר לקובץ שבו הוא נמצא, יכול לגשת לשדות ולפונקציות שאינם מיוצאים.
נבחן חבילה עם כמה קבצים:
// account/account.go
package account
type Account struct {
balance int // לא מיוצא
}
func (a *Account) Deposit(amount int) {
a.balance += amount
}
// account/helper.go
package account
func ResetAccount(a *Account) {
a.balance = 0 // עובד - אותה חבילה
}
שני הקבצים שייכים לחבילה account, ולכן helper.go יכול לגשת ישירות לשדה הלא מיוצא balance. הדבר נעשה בכוונה. כך אפשר לפצל את הלוגיקה של החבילה בין קבצים, תוך שמירה על גישה פנימית.
עם זאת, קוד מחוץ לחבילה אינו יכול לגשת לחברים שלא יוצאו:
// main.go
package main
import "account"
func main() {
acc := &account.Account{}
acc.Deposit(100) // עובד - מתודה מיוצאת
// acc.balance = 50 // שגיאה - שדה שאינו מיוצא
// account.ResetAccount // היה עובד אילו ResetAccount הייתה מיוצאת
}
גבול ברמת החבילה הוא מנגנון הכימוס העיקרי של Go. מתכננים חבילות כיחידות מלוכדות, שבהן קוד פנימי משתף פעולה בחופשיות, בעוד שקוד חיצוני מתקשר רק דרך ה-API המיוצא. חשבו על כל חבילה כעל מודול עצמאי עם ממשק ציבורי ברור.
אתגר
קלבואו נבנה מערכת לניהול מלאי שמדגימה כיצד קבצים מרובים באותה חבילה יכולים לשתף פעולה באמצעות גישה ישירה לשדות לא מיוצאים, תוך הסתרת הנתונים האלה מקוד חיצוני.
תיצרו שלושה קבצים שפועלים יחד בחבילה main:
product.go: הגדירו מבנהProductעם שדה מיוצאNameושדה לא מיוצאquantity(מסוג int). הוסיפו מתודה מיוצאתInfo() stringשמחזירה את התבנית:[Name]: [quantity] in stockinventory.go: צרו פונקציות עזר שעובדות עם מוצרים באמצעות גישה ישירה לשדה הלא מיוצאquantity(הדבר אפשרי משום שהן נמצאות באותה חבילה). ממשו:Restock(p *Product, amount int)- מוסיפה את הכמות לכמות של המוצרSell(p *Product, amount int) bool- מפחיתה את הכמות מ-quantity אם יש מספיק מלאי, ומחזירהtrueאם הפעולה הצליחה אוfalseאם אין מספיק מלאיNewProduct(name string, initialQty int) *Product- פונקציית יצירה שיוצרת מוצר עם השם והכמות ההתחלתית שסופקו
main.go: קראו את פרטי המוצר מהקלט, צרו מוצר באמצעות פונקציית היצירה, בצעו פעולות על המלאי והציגו את התוצאות. אחרי כל פעולה, הדפיסו את פרטי המוצר כדי להציג את מצבו המעודכן.
הקלטים הבאים יסופקו:
- שורה 1: שם המוצר
- שורה 2: כמות התחלתית (מספר שלם)
- שורה 3: כמות למילוי המלאי (מספר שלם)
- שורה 4: כמות למכירה (מספר שלם)
לאחר יצירת המוצר, הדפיסו את פרטיו ההתחלתיים. לאחר מכן מלאו את המלאי והדפיסו שוב את הפרטים. לבסוף, נסו למכור והדפיסו את הפרטים המעודכנים (אם הפעולה הצליחה) או את Sale failed: insufficient stock (אם אין מספיק כמות).
לדוגמה, עבור Laptop, 10, 5 ו-8, הפלט שלכם צריך להיות:
Laptop: 10 in stock
Laptop: 15 in stock
Laptop: 7 in stockועבור Phone, 3, 2 ו-10, הפלט שלכם צריך להיות:
Phone: 3 in stock
Phone: 5 in stock
Sale failed: insufficient stockהתובנה המרכזית כאן היא ש-inventory.go יכול לקרוא ולשנות ישירות את השדה הלא מיוצא quantity, משום שהוא נמצא באותה חבילה כמו product.go. זוהי כמוסה ברמת החבילה בפעולה: שיתוף פעולה פנימי מתאפשר בחופשיות, ובו בזמן הנתונים מוגנים מפני חבילות חיצוניות.
נסו בעצמכם
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// קרא את שם המוצר
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name)
// קרא את הכמות ההתחלתית
initialQtyStr, _ := reader.ReadString('\n')
initialQty, _ := strconv.Atoi(strings.TrimSpace(initialQtyStr))
// קרא את כמות המלאי להוספה
restockStr, _ := reader.ReadString('\n')
restockAmount, _ := strconv.Atoi(strings.TrimSpace(restockStr))
// קרא את הכמות למכירה
sellStr, _ := reader.ReadString('\n')
sellAmount, _ := strconv.Atoi(strings.TrimSpace(sellStr))
// TODO: צור מוצר חדש באמצעות הבנאי NewProduct
// TODO: הדפס את פרטי המוצר ההתחלתיים
// TODO: הוסף מלאי למוצר והדפס את פרטיו
// TODO: נסה למכור והדפס את התוצאה
// אם המכירה מצליחה, הדפס את הפרטים המעודכנים
// אם המכירה נכשלת, הדפס "Sale failed: insufficient stock"
fmt.Println() // החלף בפלט הרצוי
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
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 אונליין