חמשת עקרונות מודל העצמים, כללים למשלוח הודעות וכמוסות מבוססת-עצם
איך פועל מנגנון שליחת ההודעות שלב אחר שלב, כיצד המהדר מאתר עצמים חסרים, ומהו ההבדל המכריע בין כמוסות מבוססת-עצם ב-Smalltalk לבין כמוסות מבוססת-מחלקה בשפות כמו Java ו-++C.
מודל מנטלי ועיקרון מנחה: הערכה מוקדמת מדויקת וכמוסות ברמת המופע הבודד
לשלוח הודעה ב-Smalltalk זה כמו לשלוח שליח עם חבילה: קודם מוודאים מי בדיוק הנמען (הערכת המקבל), אחר כך אורזים את כל המצרכים משמאל לימין (הערכת הארגומנטים), ורק אז השליח יוצא לדרך וחוזר עם התוצאה!
ב-Smalltalk הכמוסות היא מבוססת-עצם (Object-based) ולא מבוססת-מחלקה: עצם רשאי לגשת ישירות אך ורק למשתני המופע של עצמו (self). אפילו שני עצמים מאותה מחלקה אינם רשאים לגשת ישירות לשדות זה של זה!
במתודה Rectangle>>containsRect:, המלבן הנוכחי ניגש ישירות לשדות origin ו-corner של עצמו, אך חייב לשלוח הודעות גישה (aRect origin) כדי לתשאל את המלבן השכן aRect!
1. חמשת עקרונות היסוד של מודל העצמים בשפת Smalltalk
חמשת החוקים הפשוטים שמהם נגזרת כל ההתנהגות של המערכת הטהורה
ברוך הבא לפרק שבו אנחנו מפרקים לגורמים את המנוע הפנימי של שפת Smalltalk! כדי להבין איך עובד עולם תכנות מונחה עצמים טהור לחלוטין, אני רוצה שנפתח בחמשת כללי היסוד שעליהם מושתת המודל של Smalltalk — חמישה חוקים שמהם נגזרת כל התנהגות במערכת:
אין טיפוסים פרימיטיביים נפרדים! מספרים, מחרוזות, בלוקים, תהליכים, סביבות ריצה ואפילו המחלקות עצמן — כולם מופעים של מחלקות ומתנהגים כעצמים שלמים.
כל ישות בעולם התוכנה נוצרת ומוגדרת על ידי מחלקה יחידה הקובעת את מבנה שדותיה ואת המתודות שתופעלנה בתגובה להודעות שהיא מקבלת.
מבנה המחלקות מאורגן בעץ ירושה יחיד (Single Inheritance). בראש ההיררכיה ב-Squeak ניצבת המחלקה ProtoObject, שתחתיה יורשת מחלקת הבסיס Object.
אין קריאות לפונקציות גלובליות, פקודות הסתעפות קשיחות או גישה ישירה לזיכרון של עצם אחר; כל פעולה מתרחשת כשלקוח שולח הודעה לעצם מקבל.
כשנשלחת הודעה לעצם, החיפוש מתחיל במחלקת המופע של העצם, ואם המתודה אינה שם — מטפס שלב אחר שלב במעלה עץ הירושה עד למחלקת השורש.
2. כללים למשלוח הודעות ואיתור עצמים חסרים במהדר
ארבעת השלבים הכרונולוגיים של כל שליחת הודעה ובדיקות התקינות של המהדר
מכיוון ששפת Smalltalk מבוססת באופן בלעדי על העברת הודעות בין עצמים, משלוח ההודעות בתוכנית כפוף לארבעה כללי יסוד קפדניים המבוצעים תמיד באותו הסדר בדיוק:
הערכת מקבל ההודעה (Receiver Evaluation)
לפני שבכלל מתייחסים להודעה עצמה או לארגומנטים שלה, המערכת מפענחת ומעריכה תמיד את העצם שאליו מיועדת ההודעה (המקבל / Receiver).
סדר הערכת הארגומנטים (Left-to-Right Arguments)
רק לאחר שהמקבל הוערך, הארגומנטים הנלווים להודעה מוערכים לפי הסדר הופעתם בקוד, משמאל לימין.
תזמון שליחת ההודעה (Message Dispatch Timing)
ההודעה נשלחת אל העצם המקבל אך ורק לאחר שהושלמה במלואה ההערכה של מקבל ההודעה ושל כל הארגומנטים שלה.
החלפת הביטוי בערך המוחזר (Expression Replacement)
לאחר שהמתודה הרלוונטית סיימה את ביצועה, הערך שהיא מחזירה (התוצאה) מחליף את הביטוי המקורי של משלוח ההודעה בקוד התוכנית, כך שניתן להמשיך בשרשור הפעולות.
🛡️ עצמים חסרים: איך המהדר (Compiler) שומר עלינו משגיאות מבנה?
כאשר אנחנו כותבים פקודת משלוח הודעה, המהדר (Compiler) של השפה מסייע לנו באיתור שגיאות תחביר בסיסיות ומתריע כאשר חסרים עצמים הנדרשים לביצוע התקין של ההודעה:
• במקרה של הודעה אונרית (Unary Message): המהדר יבדוק שקיים עצם מקבל ושם להודעה.
• במקרה של הודעת מילות-מפתח (Keyword Message): המהדר יוודא בנוסף למקבל, שלא חסרים ארגומנטים כלשהם הנדרשים למילות המפתח.
3. משתני מופע, מתודות וכמוסות מבוססת-עצם (Encapsulation)
למה ב-Smalltalk מופע אינו יכול לגשת ישירות לשדות של מופע אחר מאותה מחלקה? ניתוח Rectangle>>containsRect:
כעת בוא נבחן כיצד מחלקה מגדירה את המבנה (משתני מופע – Instance Variables) ואת ההתנהגות (מתודות – Methods) של העצמים שלה, ונגלה הבדל מרתק ומחמיר בין הכמוסות של Smalltalk לבין שפות כמו Java או ++C:
☕ ב-Java ו-++C: כמוסות מבוססת-מחלקה (Class-based)
בשפות כמו Java ו-++C, הכמוסות מוגדרת ברמת המחלקה: מופע אחד של מחלקה (למשל מלבן א') רשאי לגשת ישירות לשדות הפרטיים (private) של מופע אחר מאותה המחלקה (מלבן ב', כגון otherRect.origin).
💎 ב-Smalltalk: כמוסות מבוססת-עצם בלבד (Object-based)
ב-Smalltalk, הכמוסות היא ברמת העצם בלבד: משתני המופע שייכים למופע הספציפי ונשמרים בזיכרון המוקצה לו. עצם רשאי לגשת ישירות אך ורק למשתני המופע של עצמו (self) ושל מחלקות-העל שלו. אין שום אפשרות לעצם לגשת ישירות לשדותיו של מופע אחר — אפילו אם שניהם נוצרו בדיוק מאותה המחלקה!
בוא נראה את דוגמת הבוחן המאלפת מתוך המחלקה Rectangle — המתודה containsRect: הבודקת האם המלבן הזר aRect מוכל בתוך המלבן הנוכחי:
"Answer whether aRect is within the receiver (OK to coincide)."
^ aRect origin >= origin and: [ aRect corner <= corner ]
המשתנים origin ו-corner מימין לאופרטורים הם משתני המופע של המלבן הנוכחי (self), והגישה אליהם ישירה. לעומת זאת, כדי לקבל את הנקודות של המלבן הזר aRect, המלבן הנוכחי חייב לשלוח הודעות גישה מפורשות: aRect origin ו-aRect corner (מתודות גישה – Accessor methods).
הדרך המומלצת לחשוף מידע החוצה היא כתיבת מתודות גישה (Accessors). למרות שגישה ישירה מתוך המחלקה למשתנים של עצמה (כמו הגישה הישירה ל-origin ול-corner של self) היא מהירה יותר, שימוש במתודות גישה מקטין את התלות הפנימית ומקל על שינויים עתידיים במבנה הנתונים.
• משתני מופע בירושה: בניגוד ל-Java/++C, לא ניתן להסתיר משתני מופע ממחלקות יורשות — הם תמיד נגישים למחלקות-הבנות (בדומה ל-protected).
• מתודות ופרוטוקולים: כל המתודות הן פומביות (public). נהוג לחלקן לקטגוריות לוגיות הנקראות פרוטוקולים (כגון initialization או accessing). לסימון מתודה כפרטית נהוג לשייכה לפרוטוקול private ולהוסיף קידומת pvt לשמה.
ארבעת שלבי משלוח הודעה וגבול הכמוסות ב-Rectangle>>containsRect:
מיני-סימולציה 8.1: בודק המהדר (עצמים חסרים) ומוסכמות פרוטוקולים
מעבדת ציר הזמן של משלוח הודעות ובוחן הכמוסות של Rectangle>>containsRect:
▶️ סימולטור צעד-אחר-צעד: הערכת ביטוי משלוח הודעה
שלב 1 מתוך 5🔬 מעבדת הכמוסות ב-Rectangle>>containsRect:
בחר כיצד לגשת לשדות של המלבן הנוכחי (self) ושל המלבן השכן (aRect) וראה מה מותר ומה נחסם:
הגשר הנרטיבי: אם כל דבר ב-Smalltalk הוא עצם – אז מהי בעצם המחלקה עצמה, ואיך יוצרים ממנה מופעים?
למדנו את הכלל הראשון: "כל דבר הוא עצם". אבל רגע — אם כל דבר הוא עצם, זה אומר שגם המחלקה עצמה (כמו Point, Color או Dog) היא עצם חי בזמן ריצה!
אז מי המחלקה שיצרה את עצם המחלקה? מהי מטה-מחלקה (Metaclass)? ומהו ההבדל המהפכני של משתני מופע של מחלקה (Class Instance Variables) שמאפשר לכלב (Dog) ולצבוע (Hyena) להחזיק מונים נפרדים לחלוטין ולממש את תבנית היחידן (Singleton)? בוא נעבור ליחידה 8.3!
מחלקות כעצמים, מטה-מחלקות (Metaclasses), משתני מופע של מחלקה ותבנית יחידן (Singleton)
תשאול עצמים באמצעות ההודעה class, המטה-מחלקה שמאחורי הקלעים, מתודות מחלקה כבתי חרושת דינמיים, עקרון העותק הנפרד בדוגמת הכלב והצבוע (Dog ו-Hyena), ומימוש אלגנטי של Singleton עם self new.
מודל מנטלי ועיקרון מנחה: המחלקה היא עצם חי שלכל תת-מחלקה שלו יש עותק פרטי של משתני המופע של המחלקה!
חשוב על מחלקה כמו על "בית חרושת" שמייצר מכוניות. ב-Smalltalk, גם בית החרושת עצמו הוא עצם חי שיש לו מנהל משלו (המטה-מחלקה) ומונה ייצור פרטי על הקיר שלו!
כאשר מחלקה מגדירה משתנה מופע של מחלקה (Class Instance Variable), כל תת-מחלקה שיורשת ממנה מקבלת עותק נפרד, פרטי ועצמאי משלה של אותו משתנה (כמו המונה count ב-Dog וב-Hyena).
במימוש תבנית היחידן (WebServer class>>uniqueInstance), חובה להשתמש בביטוי הדינמי self new (ולא ב-WebServer new), כדי שגם תת-מחלקות יוכלו לרשת את התבנית וליצור יחידן משל עצמן!
1. תשאול עצמים (class), מטה-מחלקות (Metaclasses) ומתודות מחלקה
איך שואלים עצם מי המחלקה שלו, ואיך מתודות מחלקה פועלות כבתי חרושת דינמיים בזמן ריצה
המחלקה היא הלב הפועם המגדיר את המבנה ואת ההתנהגות של קבוצת עצמים. מאחר שכל עצם ב-Smalltalk יודע בכל רגע מאיזו מחלקה הוא נוצר, ניתן לתשאל כל עצם בזמן ריצה לגבי המחלקה שיצרה אותו על ידי שליחת ההודעה האונרית class אליו:
• משלוח הביטוי 1 class יחזיר את עצם המחלקה SmallInteger.
• משלוח הביטוי (4@5) class יחזיר את עצם המחלקה Point.
אבל כאן מגיע הקסם האמיתי: כל מחלקה בסמולטוק היא בעצמה עצם חי ופעיל לכל דבר! ניתן לבחון מחלקה ב-Inspector בדיוק כמו כל עצם אחר, והיא מהווה מופע של מחלקה ייעודית שנוצרה עבורה מאחורי הקלעים על ידי המערכת — הנקראת מטה-מחלקה (Metaclass).
🏛️ תפקיד המטה-מחלקה וכפתור ה-class ב-Browser
המטה-מחלקה מגדירה את מבנה המחלקה (משתני מופע של מחלקה) ואת התנהגות המחלקה (מתודות מחלקה).
בסביבת הפיתוח Squeak, ניתן לעבור לבחינה ולעריכה של המטה-מחלקה ומתודות המחלקה על ידי לחיצה על כפתור הבורר class בדפדפן המערכת (System Browser).
🏭 מתודות מחלקה (Class Methods) מול static ב-Java/++C
תפקידן העיקרי של מתודות מחלקה הוא לשמש כתבניות ("בתי חרושת" – Factories) ליצירת מופעים, כגון ההודעה new, או מתודות ליצירת מופעים עם אתחול ספציפי כמו Color blue (שליחת ההודעה blue למחלקה Color המחזירה מופע מוכן בצבע כחול) או Point x: 10 y: 20.
בשונה ממתודות סטטיות (static) ב-Java או ++C המנותקות ממודל העצמים, ב-Smalltalk אלו הודעות אמיתיות הנשלחות אל עצם המחלקה בזמן ריצה, עוברות קישור דינמי מלא ויכולות לעבור ירושה ודריסה!
2. משתני מופע של מחלקה (Class Instance Variables): דוגמת הכלב והצבוע (Dog ו-Hyena)
עקרון העותק הנפרד והמבודד לכל מחלקה בעץ הירושה
מעבר למתודות מחלקה, המטה-מחלקה מאפשרת להגדיר גם משתנים השייכים לעצם המחלקה עצמו — הנקראים משתני מופע של מחלקה (Class Instance Variables). הם מוגדרים בצד המחלקה באמצעות השדה instanceVariableNames: של המטה-מחלקה (למשל Dog class instanceVariableNames: 'count'), ושמם מתחיל באות קטנה.
תכונתם הייחודית והחשובה מכל היא הדרך שבה הם מורשים במורד עץ הירושה: כאשר מחלקה מגדירה משתנה מופע של מחלקה, כל תת-מחלקה שיורשת ממנה מקבלת עותק פרטי, נפרד ועצמאי משלה של אותו המשתנה!
Dog class instanceVariableNames: 'count'
Dog class >> initialize
count := 0.
Dog class >> new
count := count + 1.
^ super new
Dog subclass: #Hyena ...
Dog initialize. "מאפס רק את count של Dog"
Hyena initialize. "מאפס בנפרד את count של Hyena"
Dog new. "מקדם רק את מונה הכלבים!"
Hyena new. "מקדם רק את מונה הצבועים!"
שים לב: אפילו ש-Hyena new מפעילה את מתודת המחלקה new שהיא ירשה מ-Dog, הגישה למשתנה count בתוך המתודה פונה אל העותק הפנימי והמבודד של Hyena! המונים של הכלבים והצבועים מבודדים לחלוטין ואינם משפיעים זה על זה.
3. תבנית עיצוב יחידן (Singleton Pattern): הקסם של self new
איך מבטיחים מופע יחיד למחלקה (כגון WebServer או שעון) ומדוע אסור לקבע את שם המחלקה בקוד?
יישום הנדסי קלאסי ואלגנטי למשתנה מופע של מחלקה הוא מימוש תבנית היחידן (Singleton Pattern) — כאשר אנו מעוניינים להבטיח שלמחלקה מסוימת (למשל שרת אינטרנט WebServer או שעון מערכת) ייווצר אך ורק מופע אחד ויחיד בכל זמן הריצה של המערכת.
כיצד זה פועל מבעד למנוע?
1. מגדירים משתנה מופע של מחלקה בשם uniqueInstance במחלקה WebServer.
2. כותבים מתודת מחלקה באותו השם המבצעת בדיקת קיום ואתחול עצל (Lazy Initialization): אם המשתנה ריק (nil), מאתחלים אותו ע"י פנייה ל-self new, ולבסוף מחזירים אותו:
uniqueInstance ifNil: [ uniqueInstance := self new ].
^ uniqueInstance
💎 שאלת מיליון הדולר: למה כתבנו self new ולא WebServer new?
זוהי תובנה קריטית שמחברת בין הקישור הדינמי לבין עקרון העותק הנפרד של משתנה מופע של מחלקה!
המשתנה self בתוך מתודת מחלקה מתייחס תמיד לעצם המחלקה הדינמי שקיבל את ההודעה בזמן ריצה.
אם מחר ניצור מחלקה-בת של WebServer (למשל SecureWebServer), היא תירש אוטומטית עותק עצמאי משלה של המשתנה uniqueInstance ואת מתודת המחלקה uniqueInstance.
כשנשלח SecureWebServer uniqueInstance, הביטוי self new ייצור מופע מטיפוס המחלקה-הבת (SecureWebServer) וישמור אותו בעותק ה-uniqueInstance הפרטי שלה! אילו היינו מקבעים בקוד WebServer new, היינו פוגעים ביכולת הירושה ומחזירים תמיד שרת רגיל.
היררכיית המופע, עצם המחלקה והמטה-מחלקה + בידוד המונה count ב-Dog וב-Hyena
מיני-סימולציה 8.2: למי נשלחת ההודעה? (עצם המחלקה מול מופע קונקרטי)
מעבדת העותק הנפרד (Dog מול Hyena) וסימולטור ה-Singleton בירושה
🐾 ניסוי 1: הרץ הודעות על עצמי המחלקות Dog ו-Hyena
🌐 ניסוי 2: תבנית היחידן (WebServer uniqueInstance)
הגשר הנרטיבי: הבנו את כוחן של המחלקות והמטה-מחלקות – אבל איך בנוי עץ הירושה כולו ואיך משתפים קוד ללא ירושה מרובה?
ראינו שכל מחלקה יורשת ממחלקת-על. אבל מי ניצבת בראש הפירמידה כולה ב-Squeak (Object ו-ProtoObject)? איך מגדירים מחלקות ומתודות מופשטות בשפה שבה אין מילת מפתח abstract (רמז: self subclassResponsibility)? ואיך פותרים את מגבלת הירושה היחידה באמצעות מנגנון ה-Traits? בוא נמשיך ליחידה 8.4!
מחלקות-על (Superclasses), מחלקות ומתודות מופשטות (subclassResponsibility), תבנית Template Method ו-Traits
ירושה יחידה משורש העץ (ProtoObject ו-Object), אכיפת מימוש במתודות מופשטות בשרשרת המספרים (Magnitude → Number → Integer → SmallInteger), מתודות היברידיות (max:), ושיתוף קוד באמצעות Traits (TAuthor ב-SBEGame).
מודל מנטלי ועיקרון מנחה: ירושה יחידה אנכית (Is-A) + שאיבת התנהגות אופקית באמצעות Traits
מחלקה מופשטת היא כמו חוזה אדריכלי: היא מספקת לוגיקה שלמה (כמו max: ב-Magnitude), אך משאירה סעיפים שבהם כתוב self subclassResponsibility — "על הקבלן המבצע (תת-המחלקה) למלא סעיף זה!".
ב-Smalltalk אין מילת מפתח שמורה abstract! מתודה הופכת למופשטת על ידי שליחת ההודעה self subclassResponsibility בגופה, שתזרוק שגיאת ריצה אם תת-המחלקה לא תדרוס אותה.
כדי להתגבר על מגבלת הירושה היחידה מבלי להסתבך בירושה מרובה, מנגנון ה-Traits מאפשר "לשאוב" חבילות של מתודות (ללא משתני מופע) לתוך כל מחלקה בעזרת uses:!
1. מבנה עץ הירושה היחידה: Object ו-ProtoObject
למה לכל מחלקה יש הורה אחד בלבד, ומי ניצבת בראש הפירמידה בסביבת Squeak?
הארכיטקטורה של Smalltalk תומכת בירושה יחידה (Single Inheritance) בלבד — כלומר, לכל מחלקה יש מחלקת-הורה (Superclass) אחת בלבד שממנה היא יורשת את המבנה (משתני המופע) ואת ההתנהגות (המתודות). עץ ההיררכיה כולו מתנקז למחלקת שורש אחת:
- בגרסאות קלאסיות של השפה, מחלקת האם שבראש ההיררכיה נקראת
Object, ורובן המוחלט של המחלקות יורשות ממנה את השירותים המתקדמים שכל עצם צריך. - בסביבת Squeak הוכנסה מעל
Objectמחלקת-על עליונה בשםProtoObject, הממוקמת בשורש הקיצוני של העץ ומספקת את הסט המינימלי והבסיסי ביותר של פעולות והודעות שכל עצם חייב להבין. - כל מחלקה חדשה נוצרת על ידי שליחת הודעה אל מחלקת-העל שלה:
subclass:instanceVariableNames:classVariableNames:poolDictionaries:category:. - לחיצה על כפתור ה-
hierarchyבדפדפן המערכת (System Browser) פותחת את ה-Hierarchy Browser המציג את העץ ההיררכי המלא ואת הורשת משתני המופע והמתודות מטה.
2. מחלקות ומתודות מופשטות (subclassResponsibility) ותבנית Template Method
איך מגדירים מתודה מופשטת ללא מילה שמורה abstract, ואיך Magnitude מעניקה מתודות שלמות בחינם?
מחלקה מופשטת (Abstract Class) מספקת שלד עיצובי המאגד פונקציונליות ופרוטוקול משותפים להיררכיה שלמה, אך היא אינה מיועדת ליצירה ישירה של מופעים ממנה. במחלקות אלו אנו פוגשים מתודות מופשטות המגדירות "מה" העצם אמור לדעת לעשות, אך משאירות את ה"איך" למחלקות היורשות.
ב-Smalltalk אין מילת מפתח תחבירית שמורה כגון abstract! כיצד אם כן מסמנים מתודה כמופשטת? פשוט על ידי שליחת ההודעה המיוחדת self subclassResponsibility בתוך גוף המתודה!
הצהרה זו מעבירה מפורשות את חובת המימוש למחלקה-הבת. אם תיווצר מחלקה-בת שלא תדרוס (Override) את המתודה ותנסה להריץ אותה, הפקודה subclassResponsibility תזרוק שגיאת ריצה קריטית המעידה כי חובת המימוש חלה על תת-המחלקה.
Number >> * aNumber
"Answer the result of multiplying the receiver by aNumber."
self subclassResponsibility
Magnitude >> max: anotherMag
self < anotherMag
ifTrue: [ ^ anotherMag ]
ifFalse: [ ^ self ]
🔢 שרשרת ההפשטה של המספרים
סדר הירושה מהשורש: Magnitude (מופשטת – גדלים ברי-השוואה) מורישה ל-Number (מופשטת – מגדירה בין היתר את הכפל * כמתודה מופשטת), המורישה ל-Integer (מופשטת בעצמה), המורישה ל-SmallInteger שהיא כבר מחלקה קונקרטית בתחתית העץ הדורסת ומממשת בפועל את פעולות הכפל בחומרה!
🧩 מתודות היברידיות / תבנית Template Method
במחלקה המופשטת Magnitude, המתודה max: ממומשת במלואה! היא נשענת על קריאה למתודה המופשטת <. ברגע שתת-מחלקה קונקרטית (כגון Date, Time או Number) מממשת רק את המתודה הבסיסית <, היא מקבלת בחינם את המימוש השלם של max:, min:, >=, <= ו->!
3. מאפיינים שיתופיים (Traits): שיתוף התנהגות ללא ירושה מרובה
איך שואבים מקבצי מתודות (כגון TAuthor) לתוך מחלקה (כגון SBEGame) מבלי לשנות את עץ הירושה שלה?
בשל מגבלת הירושה היחידה, מחלקה אינה יכולה לרשת משתי מחלקות-על שונות במקביל. כדי לאפשר שיתוף והרכבה של יחידות התנהגות (מקבצי מתודות, ללא משתני מופע) בין מחלקות שונות שאין ביניהן בהכרח שום קשר היררכי, קיים בסביבת Squeak מנגנון ה-Traits (תכונות / מאפיינים שיתופיים).
uses: { }
category: 'SBE-Quinto'
"Returns author initials"
^ 'on'
uses: TAuthor
instanceVariableNames: 'cells'
classVariableNames: ''
poolDictionaries: ''
category: 'SBE-Quinto'
ברגע שהוספנו את מילת המפתח uses: TAuthor בהכרזת המחלקה SBEGame (שיורשת מ-BorderedMorph), כל מופע של SBEGame תומך ישירות במתודה author (הביטוי SBEGame new author מחזיר 'on'), וזאת מבלי לשנות כלל את עץ הירושה המקורי שלה!
היררכיית המספרים (Magnitude עד SmallInteger) ושאיבת TAuthor לתוך SBEGame
מיני-סימולציה 8.3: חוקר מתודות מופשטות (subclassResponsibility) ומתודות היברידיות
מעבדת מחלקות מופשטות והרכבת Traits (ניסוי כפל ב-Number מול SmallInteger ושאיבת TAuthor)
🔢 ניסוי 1: מה קורה כשמפעילים * או max: בהיררכיית Magnitude?
🧩 ניסוי 2: הרכבת ה-Trait TAuthor במחלקה SBEGame
הגשר הנרטיבי: ראינו איך העץ ההיררכי בנוי – אבל איך מערכת זמן הריצה מטפסת בו כדי לאתר מתודה, ומה ההבדל בין self ל-super?
כעת אנחנו מוכנים לצלול אל הלב הדינמי של השפה (סעיפים 8.5–8.6): איך פועל אלגוריתם איתור המתודות (Method Lookup) במעלה עץ הירושה? מה קורה כשהחיפוש מגיע עד ל-ProtoObject ולא מוצא את המתודה (רמז: doesNotUnderstand:)? ואיך טעות של מילה אחת בין self ל-super בתוך מתודת initialize מכניסה את המערכת ללולאה אינסופית? בוא נעבור ליחידה 8.5–8.6!
הקישור הדינמי, איתור מתודות (Method Lookup), דריסה והרחבה, self מול super ו-doesNotUnderstand:
אלגוריתם החיפוש במילון המתודות (methodDict) במעלה עץ הירושה עד ProtoObject, מנגנון doesNotUnderstand:, ערך החזרה ממתודה (^ מול ברירת המחדל self), דריסה (defaultColor) מול הרחבה (super initialize), ו-5 צבעי כפתור ה-inheritance.
מודל מנטלי ועיקרון מנחה: גם self וגם super מצביעים על אותו עצם בדיוק — ההבדל הוא רק מאיפה מתחיל חיפוש המתודה!
דמיין בניין קומות משפחתי: בקריאה ל-self החיפוש תמיד מתחיל בקומת הקרקע של הדייר עצמו (מחלקת המופע הדינמית). בקריאה ל-super אנחנו לוקחים מעלית ישירות לקומה שמעל הקומה שבה כתוב הקוד הנוכחי!
במערכת זמן הריצה מתקיים תמיד self == super (שניהם מייצגים את העצם המקורי שקיבל את ההודעה). אולם self מתחיל את החיפוש במחלקת המופע הדינמית, ואילו super מתחיל במחלקת-העל של המחלקה שבה נכתב הקוד!
אם מתודה מסתיימת ללא חץ החזרה ^, היא מחזירה אוטומטית את self (ולכן כתיבת ^ self בסוף מתודה היא כפילות מיותרת). ואם מתודה לא נמצאה עד ל-ProtoObject, נשלחת למקבל ההודעה doesNotUnderstand:!
1. הקישור הדינמי (8.5) ואלגוריתם איתור המתודות במעלה העץ (8.6)
איך מערכת זמן הריצה מוצאת את המתודה הנכונה, ומה קורה כשהיא מגיעה ל-ProtoObject בידיים ריקות?
משלוח הודעה לעצם אינו שקול לקריאת פונקציה מסורתית. עוצמתה של Smalltalk נשענת על מנגנון הקישור הדינמי (Late Binding / Dynamic Dispatch): תהליך איתור המתודה המדויקת שתבוצע נדחה עד לרגע הביצוע בזמן הריצה, ונגזר בלעדית מזהות המחלקה הדינמית של העצם שקיבל את ההודעה באותו חלקיק שנייה (ללא קשר לאופן הכרזת המשתנה, מה גם ש-Smalltalk היא ממילא שפה נטולת טיפוסים סטטיים).
כאשר נשלחת הודעה לעצם, מערכת זמן הריצה פותחת במסע חיפוש שיטתי במעלה עץ הירושה — אלגוריתם איתור המתודות (Method Lookup):
המערכת בוחנת את העצם המקבל ומזהה את המחלקה הישירה והמדויקת שממנה הוא נוצר.
methodDict
המערכת מחפשת את סלקטור ההודעה בטבלת/מילון המתודות (methodDict) של אותה מחלקה. אם נמצאה מתודה מתאימה — היא מבוצעת מיד!
אם המתודה אינה נמצאת שם, המערכת מטפסת רמה אחת למעלה אל מחלקת-העל (superclass) ובודקת את מילון המתודות שלה, וכך הלאה דרך Object ועד לקצה העץ ב-ProtoObject.
doesNotUnderstand:
אם החיפוש הגיע עד ל-ProtoObject והמתודה לא אותרה, המערכת אורזת את הסלקטור והארגומנטים במופע של Message ושולחת למקבל המקורי את ההודעה doesNotUnderstand:!
🚨 הכוח של doesNotUnderstand:
ברירת המחדל לטיפול ב-doesNotUnderstand: במחלקת Object היא זריקת חריגה (Exception) מסוג MessageNotUnderstood, עצירת התוכנית ופתיחת חלון מנפה השגיאות (Debugger).
אולם, מכיוון שזוהי הודעה לכל דבר, מתכנתים יכולים לדרוס (Override) את doesNotUnderstand: במחלקות מותאמות כדי לתפוס הודעות דינמיות שלא הוגדרו מראש ולממש מנגנוני מעטפת (Proxies), תיעוד קריאות דינמי (Logging), או ניתוב הודעות מרוחק (Forwarding)!
↩️ החזרת ערך ממתודה (^ מול ברירת המחדל self)
כל מתודה בסמולטוק מסתיימת בהחזרת ערך אל הלקוח ששלח את ההודעה:
• החזרה מפורשת: מתבצעת בעזרת סימן החץ הפונה כלפי מעלה (^).
• ברירת מחדל אוטומטית: אם מתודה מגיעה לסוף שורותיה ללא ציון מפורש של ^, המערכת מחזירה באופן אוטומטי ובשתיקה את self (העצם המקורי שקיבל את ההודעה). לכן, המוסכמה היא לא לכתוב ^ self בשורה האחרונה של מתודה (זוהי כפילות מיותרת), כדי לשמור על קוד נקי ותמציתי.
2. דריסה מלאה (Overriding) מול הרחבה (Extension) וההבחנה בין self ל-super
דוגמאות הבוחן defaultColor ו-initialize, לולאת הקסם האינסופית, ו-5 צבעי כפתור ה-inheritance
כאשר תת-מחלקה יורשת ממחלקת-על, היא יכולה לעדכן את ההתנהגות שירשה באחת משתי דרכים מרכזיות:
"answer the default color/fill style for the receiver"
^ Color blue
"answer the default color/fill style for the receiver"
^ Color yellow
super initialize.
borderWidth := 1.
borderColor := Color black
הקריאה super initialize בתחילת המתודה מבטיחה שמשתני המופע של מחלקות-העל יאותחלו כהלכה לפני אתחול השדות המקומיים של הבן!
⚡ הבנת המנגנון של self מול super (ולמה טעות ביניהם גורמת ללולאה אינסופית!)
שני משתני הפסאודו (Pseudo-variables) self ו-super מצביעים בזמן ריצה על אותו עצם מקבל בדיוק בזיכרון (self == super)!
ההבדל היחיד והקריטי ביניהם מתבטא בנקודת ההתחלה שממנה יחל מסע חיפוש המתודה במעלה עץ הירושה:
• קריאה דרך self (חיפוש דינמי מלא): מנתבת את מערכת זמן הריצה להתחיל את חיפוש המתודה במחלקת המופע המקורית (הטיפוס הדינמי) של העצם שקיבל את ההודעה בזמן הריצה.
• קריאה דרך super (עוגן סטטי): מאלצת את המערכת לעקוף את המימוש המקומי ולהתחיל את חיפוש המתודה ישירות במחלקת-העל (Superclass) של המחלקה שבה נכתב הקוד המבצע את הפניה בפועל (נקבע מראש בשעת הקומפילציה).
💥 סכנת לולאת הקסם (Infinite Recursion): אילו במתודה BorderedMorph>>initialize היינו כותבים בטעות self initialize במקום super initialize, מנוע החיפוש היה מתחיל שוב במחלקת המופע BorderedMorph, מוצא שוב את אותה מתודה initialize עצמה, קורא לה שוב ושוב, ונתקע בלולאה אינסופית שתקריס את המערכת!
🎨 חיווי 5 הצבעים של כפתור ה-inheritance ב-System Browser
כאשר בוחרים מתודה ב-System Browser, צבע הכפתור inheritance מספק אבחנה חזותית מיידית לגבי מעמד המתודה בשרשרת הירושה:
super.
super.
super) ובנוסף נדרסת בתת-מחלקה.
super) ובנוסף נדרסת בתת-מחלקה.
אלגוריתם הטיפוס במעלה עץ הירושה עד ProtoObject ונקודת ההתחלה של self לעומת super
מיני-סימולציה 8.4: מפענח צבעי כפתור inheritance ב-System Browser
מעבדת איתור המתודות (Method Lookup), לולאת הקסם של self מול super ו-doesNotUnderstand:
בחר תרחיש הרצה על מופע של EllipseMorph או BorderedMorph ועקוב אחר מסלול החיפוש במעלה העץ (EllipseMorph → BorderedMorph → Morph → Object → ProtoObject):
הגשר הנרטיבי: הבנו איך עצמים שולחים הודעות ומאתרים מתודות – אבל איך משתפים מידע בין עצמים רבים ללא העברת פרמטרים מסורבלת?
עד עכשיו ראינו משתני מופע (ששייכים למופע בודד) ומשתני מופע של מחלקה (ששייכים לעצם מחלקה בודד). אבל מה קורה כשעולה הצורך לשתף נתונים בין מספר רב של עצמים ומחלקות במקביל?
ביחידה המסכמת הבאה נכיר את שלוש הקטגוריות של משתנים משותפים (Shared Variables) — משתנים גלובליים במילון Smalltalk, משתני מחלקה (Class Variables) ומילוני מאגר משותפים (Pool Dictionaries) — ונחתום את פרק 8 עם כרטיסיות הזיכרון התלת-ממדיות!