סיכום קורס: תכנות מונחה עצמים
סיכום מדויק, תמציתי ומקיף של כל 18 פרקי הקורס, העקרונות, קטעי הקוד, שאלות הניתוח והדוגמאות המרכזיות
חלק I: מבוא ומנגנוני הפשטה (פרקים 1–2)
מדוע לומדים Smalltalk כ-OOP טהורה, בעיית הקבועים החוזרים, ו-7 שלבי התפתחות מנגנוני ההפשטה.
1.1–1.11 מטרות הקורס, למה Smalltalk וסביבת העבודה Squeak
-
ספרי הלימוד והמקורות: הקורס מבוסס על שני ספרי מקור מרכזיים:
(1) Squeak by Example (Black et al., 2007) – ללימוד שפת
SmalltalkבסביבתSqueak(מתמקד בפרקים 4–9, 11–12, 17–18), ו-(2) Object-Oriented Programming (Timothy Budd, 2002) – לעקרונות תכנות מונחה עצמים בחתך שפות כגון C, C++, Ada, Modula-2, Eiffel, Java ו-Smalltalk (מתמקד בפרקים 2–3, 10, 13–16, 18). -
מדוע כדאי ללמוד Smalltalk? בדומה ל-
LISPול-LOGO, שפתSmalltalkהיא שפה מונחית עצמים "טהורה" (Pure OOP) המתבססת על מנגנון הפשטה אחד ויחיד, פשוט וקליט: כל דבר הוא עצם, והתכנות מתבצע אך ורק על ידי הגדרת עצמים ושליחת הודעות ביניהם. היא כמעט נטולת תחביר ומכריחה את הסטודנט לחשוב ולתכנת בגישת OOP טהורה. -
סביבת העבודה Squeak: סביבת פיתוח בקוד פתוח תואמת
Smalltalk-80, הממומשת כולה ב-Smalltalk עצמה ובעלת מנשק גרפי עשיר (ששימש אף את אולפני Disney ביצירת סרטים).
2.1 דוגמה לשימוש במנגנוני הפשטה: מניעת חזרה על קבועים
מנגנוני הפשטה מוכרים בשפות תכנות: משתנים, קבועים, טיפוסי נתונים, פרוצדורות/פונקציות, מודולים, טיפוסי נתונים מופשטים (ADT), מחלקות וירושה.
לדוגמה, נניח שב-C++ מוגדר מערך int a[100]; ולולאה for (i = 0; i < 100; i++) cout << a[i];. להלן השוואת 4 מנגנוני הפשטה למניעת חזרה על הקבוע 100 ביתרונותיהם וחסרונותיהם:
| שיטת הפשטה | קוד מרכזי | הערכת מנגנוני ההפשטה (יתרונות וחסרונות) |
|---|---|---|
| ללא הפשטה (קבוע כפול) | int a[100]; ... i < 100 |
הבעיה בגישה זו: הקבוע 100 מופיע פעמיים; אם גודל המערך יעודכן ל-200 ויישכח עדכון תנאי הלולאה, תיווצר חוסר עקביות ושגיאות ריצה. |
| פקודת מאקרו ב-C++ | #define N 100 |
מגדיר את גודל המערך במקום אחד, אך מבוצע ע"י הקדם-מעבד (Preprocessor) כהחלפת טקסט עיוורת לפני ההידור, ללא בדיקת טיפוסים או תחום הכרה (Scope). |
| שימוש ב-enum ב-C++ | enum {N = 100}; int a[N]; |
יתרון לעומת מאקרו: enum הוא חלק משפת התכנות עצמה (כפוף לבדיקות המהדר, ל-Scope ולסימבולים ב-Debugger).חיסרון לעומת קבוע ישיר: דורש שורת קוד נוספת ומפצל את הגדרת המערך וגודלו לשתי שורות נפרדות במקום שורה אחת. |
| מאקרו חישוב גודל | #define SIZEOF(arr) (sizeof(arr)/sizeof(arr[0])) |
משלב את שני היתרונות: הגדרת המערך וגודלו בשורה אחת (int a[100];) והגודל אינו מופיע פעמיים, אך עדיין תלוי במאקרו קדם-מעבד. |
| תמיכה מובנית ב-Java | a.length או for (int k : a) |
ב-Java תמיכה מובנית בתוך השפה: המערך הוא עצם היודע את אורכו (a.length), ובלולאת for-each תחביר הלולאה מופשט עוד יותר ללא מונה אינדקס כלל (for (int x : a))! |
2.2 ★ 7 שלבי התפתחות מנגנוני ההפשטה (סעיפים 2.2.1–2.2.7)
• לדוגמה, מוגדרות המחלקות `EmptyList` (שבה `empty = true; head = error; tail = error`) ו-`NonEmptyList(x, l)` (שבה `empty = false; head = x; tail = l`).
• השוואת מטריצת ADT מול OOP: בייצוג כ-ADT החלוקה היא לפי הפעולות (`empty?`, `head`, `tail` חוצות את העמודות `nil` ו-`cons`), ואילו בייצוג מונחה עצמים (OOP) החלוקה היא לפי טיפוסי העצמים (`nil` / `EmptyList` ו-`cons` / `NonEmptyList`), כשכל מחלקה מאגדת את מימוש כל הפעולות עבורה.
125K שורות קוד מומשה מחדש בשפת C++ (בעזרת ירושה) באמצעות 10K שורות קוד בלבד!
חלק II: עצמים – המושג "עצם" (פרק 3)
סוגי עצמים, 3 מאפייני העצם (זהות, מצב, התנהגות), עצמים בשפת C, זהות עצמית, בדיקת שוויון וסוגי העתקה.
3.1–3.3 סוגי עצמים, 3 מאפייני העצם ודוגמאות
- שאלת מפתח: איזה מושג חשוב יותר ב-OOP – "עצם" או "מחלקה"? פתרון: המושג "עצם" הוא בסיסי יותר כמובן, אך למעשה מושג ה"מחלקה" הוא חשוב יותר (כי הוא מנגנון ההפשטה המגדיר קבוצות של עצמים).
-
סיווג העצמים לשני מרחבים:
1. עצמים השייכים למרחב הבעיה (`problem space`): מייצגים ישויות מעולם הבעיה, כגון עובד, שעון נוכחות, פריט במלאי, פטיש בחנות, מספר מרוכב $e+2i$, כלי רכב נושא לוחית זיהוי19-070-35, או רשימה ממרחב הבעיה (כגון רשימת קניות או רשימת נוכחות תלמידים).
2. עצמים השייכים למרחב התכנית / הפתרון (`program / solution space`): מייצגים מבני עזר תכנותיים, כגון קובץ, או רשימה ממרחב הפתרון (כגון רשימת בלוקים פנויים במנהל זיכרון או שרשרת גיבוב). שימו לב כי רשימה כגון(13, "hello", true)יכולה להשתייך הן למרחב הבעיה והן למרחב הפתרון בהתאם להקשר השימוש בה.
🔨 שלושת מאפייני העצם (זהות, מצב והתנהגות):
3.4 האם בשפת C אפשר להגדיר עצמים? (סעיפים 3.4.1–3.4.3)
int counter(void) { static int n = 0; return ++n; }• זהות: מופע הפונקציה.
• מצב: ערך המשתנה הסטטי
n.• החיסרון: ההתנהגות מנוונת (הודעה אחת בלבד – הפעלת הפונקציה) ומופע יחיד.
stack.h (push, pop) וקובץ מימוש stack.c עם static int stack[100], sp;.• מצב והתנהגות: המשתנים הסטטיים ואוסף הפונקציות בקובץ.
• החיסרון: הזהות מנוונת – יש רק מופע אחד של המחסנית!
• זהות: המצביע לקובץ (ייתכנו מצביעים רבים לקבצים שונים!).
• מצב: תוכן הקובץ.
• התנהגות: הפעולות על המצביע (
fopen, feof, fgetc, סגירת הקובץ).
3.5 ★ זהות עצמית, בדיקת שוויון וסוגי העתקה
-
שם מול זהות והמזהה השמור לעצם הנוכחי: שם העצם נבדל מזהותו (ייתכנו שני עצמים בעלי אותו שם, עצם בעל כמה שמות, או עצם ללא שם כלל, אך לא ייתכנו שני עצמים בעלי אותה זהות). המזהה השמור לעצם הנוכחי: ב-
C++וב-Java:this| ב-Eiffel:Current| ב-Smalltalk:self. - שני סוגי השוואה: (1) השוואת הזהות: שני שמות שווים אמ"ם הזהות זהה (מצביעים לאותו עצם ממש). (2) השוואה מבנית: שני שמות שווים אמ"ם לשני העצמים יש מבנה זהה ומצב זהה.
📋 סיווג סוגי העתקת עצמים ב-Java (העתקה זהותית, רדודה, רב-שכבתית ועמוקה – חובה למבחן!):
בהעתקת רשימה מקושרת של נקודות class PList { public Point head; public PList tail; }:
- 1. העתקה זהותית: המצביע לעצם משוכפל; בסיום ההעתקה יש שני שמות לאותו עצם ממש ונותרת זהות אחת בלבד.
- 2. העתקה מבנית: מצב העצם מועתק לעצם חדש (בסיום יש שני עצמים בעלי זהויות שונות). נחלקת ל:
- העתקה רדודה (`oneLevelCopy`): השורש מועתק בהעתקה מבנית (
PList res = new PList();), אך העצמים הפנימיים מועתקים זהותית (res.head = head; res.tail = tail;). - העתקה רדודה לעומק 2 (`twoLevelCopy`): יוצרת נקודה חדשה בראש (
res.head = new Point(head.x, head.y);) ומעתיקה את הזנב לעומק אחד (res.tail = tail.oneLevelCopy();). - העתקה עמוקה (`deepCopy`): הן השורש והן כל חוליות הזנב והנקודות הפנימיות מועתקים רקורסיבית/איטרטיבית בהעתקה מבנית עמוקה (לכל חוליה נוצרים
new Point(...)ו-new PList()חדשים). - העתקה רקורסיבית (`copy`): ב-
PList.copy()כתוב:res.head = head.copy(); if (tail != null) res.tail = tail.copy();.
ניתוח עומק ההעתקה (האם זו העתקה רדודה או עמוקה?): עומק ההעתקה נקבע כאן ע"י העצמים הפנימיים והאופן שבו הם מממשים אתcopy()! אם המחלקהPointמוגדרת עם שדות קבועיםpublic final int x, y;(כלומרPointהיא עצם בלתי משתנה / Immutable שמצבו אינו יכול להשתנות לעולם),Point.copy()יכולה לממש בחוכמהreturn this;(העתקה זהותית של הנקודה מבלי לשכפלה לשווא בזיכרון), בעוד חוליות ה-PListמשוכפלות מבנית לכל אורך הרשימה!
- העתקה רדודה (`oneLevelCopy`): השורש מועתק בהעתקה מבנית (
3.6 מאפיינים סטטיים ודינמיים של עצמים (סעיפים 3.6.1–3.6.2)
• מבנה (`structure`): אוסף התכונות.
• מצב (`state`): אוסף הערכים במבנה.
• עצם נצחי (`Immutable Object`): עצם שמצבו אינו משתנה לאורך חייו; המבנה שלו מורכב מתכונות נצחיות בלבד.
• שיטה / מתודה (`Method`): הקוד המתאים שהעצם בורר ומריץ בהתאם למהות ההודעה.
• פרוטוקול (`Protocol`): אוסף ההודעות שהעצם מבין.
הבחנה קריטית: עצמים שונים עשויים להבין את אותה הודעה, אבל להגיב עליה באופן שונה (להפעיל עבורה שיטה שונה).
חלק III: מבוא לשפת סמולטוק וסביבת Squeak (פרקים 4–5)
ארכיטקטורת קובצי Squeak, 3 כפתורי העכבר, כלי הפיתוח ופיתוח המשחק המלא Quinto.
4.1–4.4 קובצי סביבת Squeak, 3 כפתורי העכבר ותפריט העולם
SqueakX.image(קובץ התמונה): מכיל את "תמונת" סביבת הריצה ומצב כל האובייקטים (כולל מחלקות ומתודות) כפי שנשמרה לאחרונה.SqueakX.sources: קובץ לקריאה בלבד המכיל את קובצי המקור המתאימים לגרסה X ואילך.SqueakX.changes(קובץ השינויים): מכיל את רשימת כל השינויים שהתבצעו בקובצי המקור (מומלץ לגבות את ה-imageוה-changesיחד!).
- כפתור אדום (שמאלי): בחירת פריטים וסימון טקסט; בלחיצה על רקע ה-
Worldמעלה את תפריט העולם (`World Menu`) (ניתן לקבעו למסך בלחיצה על צלמית הנעץ בכותרתו!). - כפתור צהוב (ימני): מעלה תפריטים תלויי-הקשר (Context Menus).
- כפתור כחול (מרכזי / גלגלת): מקיף את העצם במסגרת צלמיות ה-
morphic halo(להזזה, סיבוב ושינוי גודל). - שמירה ויציאה (4.4):
save and quitבתחתית תפריט העולם שומרת את תמונת המסך בדיוק כפי שהייתה.
4.5–4.10 כלי העבודה ב-Squeak, חיפוש מחלקות/מתודות והמתודה shout
-
חלון עבודה (`Workspace`) מול חלון הודעות (`Transcript` – קיצור `Ctrl+t`):
Transcript show: 'hello world'; cr.מדפיס ל-Transcript ויורד שורה (בזכות ה-Cascade;). -
4 פקודות הביצוע והבחינה ב-Workspace:
1.do it (Ctrl+d): מבצע את הביטוי המסומן (למשל3 + 4מחשב7אך לא מציג דבר).
2.print it (Ctrl+p): מבצע את הביטוי, שולח לעצם המוחזר את ההודעהprintStringומציג את הייצוג הטקסטואלי שלו (למשלTime now).
3.inspect it (Ctrl+i): פותחInspector– חלונית שמאל מציגה אתselfואת משתני המופע, חלונית ימין מציגה את הערך הנבחר, והחלונית התחתונה משמשת כ-Workspace שבוselfקשור לעצם הנבחן (למשלself squaredעל7יחזיר49).
4.explore it (Ctrl+Shift+i / Shift+Alt+i): פותחExplorerהמציג את העצם במבנה עץ היררכי (ניתן לעבור מ-Explorer ל-Inspector ולהיפך דרך הכפתור בחלק הימני התחתון). -
טעינת חבילות עם `SqueakMap` ודפדפן המערכת `System Browser`:
•SqueakMap: כלי לטעינת חבילות תוכנה מרשת האינטרנט.
• 4 חלוניות ה-System Browser(משמאל לימין): (1) קטגוריות (Categories) $\to$ (2) מחלקות (Classes) $\to$ (3) פרוטוקולים (Protocols, או--all--) $\to$ (4) מתודות (Methods). לחיצה על כפתור?במרכז מציגה את תיעוד המחלקה.
• 4 דרכים לחיפוש מחלקה: (1) ניווט בחלונית הקטגוריות, (2) שליחת ההודעהBoolean browse(או סימון שם המחלקה ו-Ctrl+b), (3) פתיחתHierarchy Browser(כפתורhierarchy), (4)find class... (f)בתפריט ההקשר של חלונית הקטגוריות.
• חיפוש מתודות ב-Method Finder: לפי שם/חלק משם (למשלtime) או לפי דוגמת מקבל, ארגומנטים ותוצאה מופרדים בנקודה:4. 16$\implies$4 squared --> 16.
// הגדרת המתודה shout במחלקה String (פרוטוקול converting):
shout
"Answer a String converted to Uppercase followed by an exclamation mark."
^ self asUppercase, '!'
// משולש צהוב בפינה הימנית-עליונה של חלונית העריכה מציין שינויים שטרם נשמרו -> שומרים עם accept (Ctrl+s).
// הרצה ב-Workspace עם print it: 'hello' shout --> 'HELLO!'
5.1–5.7 ★ הרצת תכנית ראשונה: המשחק Quinto, דיבוג הבאג וייצוא fileOut
המשחק Quinto מתנהל על לוח משובץ ($10 \times 10$): לחיצה על כל משבצת הופכת את מצבן (צהוב חיוור $\leftrightarrow$ כחול כהה) של 4 המשבצות השכנות הצמודות אליה (מעליה, מתחתיה, מימינה ומשמאלה – ללא אלכסונים וללא המשבצת שנלחצה עצמה!). התכנית בקטגוריה SBE-Quinto מורכבת משתי מחלקות:
SBECell (סעיפים 5.2–5.4):
יורשת מ-SimpleSwitchMorph (כפתור דו-מצבי) עם משתנה מופע יחיד mouseAction:
SBECell >> initialize
super initialize.
self label: ''.
self borderWidth: 2.
bounds := 0@0 corner: 16@16.
offColor := Color paleYellow.
onColor := Color paleBlue darker darker.
self useSquareCorners.
self turnOff
SBECell >> mouseAction: aBlock "פרוטוקול accessing"
^ mouseAction := aBlock
SBECell >> mouseUp: anEvent "פרוטוקול event handling"
mouseAction value
SBEGame:
יורשת מ-BorderedMorph עם משתנה מופע יחיד cells (מסוג Matrix) ומתודת cellsPerSide ^ 10:
SBEGame >> initialize
| sampleCell width height n |
super initialize.
n := self cellsPerSide.
sampleCell := SBECell new.
width := sampleCell width. height := sampleCell height.
self bounds: (5@5 extent: ((width*n)@(height*n)) + (2 * self borderWidth)).
cells := Matrix new: n tabulate: [:i :j | self newCellAt: i at: j]
SBEGame >> newCellAt: i at: j
| c origin |
c := SBECell new.
origin := self innerBounds origin.
self addMorph: c.
c position: ((i-1) * c width) @ ((j-1) * c height) + origin.
c mouseAction: [self toggleNeighboursOfCellAt: i at: j].
^ c "חובה! בלעדיו מוחזר self שהוא SBEGame!"
newCellAt:at: ושמירת התכנית (`fileOut` / `fileIn`):
• מלכודת ערך החזרה ב-
newCellAt:at:: ללא השורה ^ c בסוף newCellAt:at:, המתודה מחזירה כברירת מחדל את ^ self (עצם ה-SBEGame!). לכן כל איברי המטריצה cells מתמלאים במצביעים ל-SBEGame במקום ל-SBECell, ובלחיצה על תא המתודה toggleNeighboursOfCellAt:at: שולחת (cells at: i-1 at: j) toggleState ל-SBEGame וזורקת MessageNotUnderstood: SBEGame>>toggleState!
• ייצוא וייבוא קוד: ייצוא קטגוריה לקובץ טקסט
SBE-Quinto.st מתבצע באמצעות fileOut מתפריט ההקשר של חלונית הקטגוריות. ייבוא הקובץ לסביבה אחרת מתבצע ע"י גרירת הקובץ אל חלון ה-Squeak ובחירה ב-filein entire file.
חלק IV: פיתוח תוכנה בשפת סמולטוק (פרקים 6–9)
תחביר השפה, פרימיטיבים, קדימויות הודעות, בלוקים ואיטרטורים, מודל העצמים, משתני מופע של מחלקה, Singleton, Traits, כלי הפיתוח וה-Debugger.
6.1–6.2 ליטרלים, מערכים סטטיים/דינמיים, 6 פסאודו-משתנים ופרימיטיבים
| אלמנט תחבירי | דוגמאות מפתח | כללים ודגשים למבחן |
|---|---|---|
| משתנים מקומיים מול משותפים | מקומי: | x y |, myVarמשותף: Transcript, Color |
מקומיים (משתני מופע, פרמטרים של מתודה/בלוק, משתנים זמניים) מתחילים באות קטנה. משותפים (גלובליים, משתני מחלקה, שמות מחלקות, `pool dictionaries`) מתחילים באות גדולה. |
| מספרים, תווים, מחרוזות וסימבולים | 2r101 (=5), 2.4e7, $a, Character tab, 'Mc''Donald', #Hello |
כדי לכלול גרש בתוך מחרוזת רושמים שתי מרכאות בודדות ''. הערות נכתבות במרכאות כפולות "comment". סימבול (#Hello) מובטח להיות ייחודי בזיכרון (עותק יחיד) ובלתי ניתן לשינוי (Immutable). |
| מערך סטטי (זמן הידור) מול דינמי (זמן ריצה) | סטטי: #(1 'ab' $a)דינמי: {1. 2. 1+2} |
מלכודת מבחן: במערך סטטי #(...) האיברים מופרדים ברווחים ונקבעים בזמן הידור ללא שערוך הודעות! לכן #(10@10 100@100) אינו מכיל 2 נקודות אלא 6 איברים: #(10 #@ 10 100 #@ 100)! במערך דינמי {1. 2. 1+2} הביטויים מופרדים בנקודה ומשוערכים בזמן ריצה ל-#(1 2 3) (חובה רווח אחרי הנקודה ב-1. 2 אחרת יתקבל #(1.2 3)!). |
| 6 פסאודו-משתנים (Pseudo-variables) | קבועים: true, false, nilדינמיים: self, super, thisContext |
ערכם נקבע ע"י מערכת זמן הריצה ולא ניתן לבצע להם השמה (:= או _). nil הוא המופע היחיד של UndefinedObject (כל המשתנים מאותחלים ל-nil). thisContext מתייחס למסגרת העליונה של מחסנית זמן הריצה (הקשר הביצוע של המתודה או הבלוק). |
| פרימיטיבים (Primitives) | <primitive: 1> |
פעולות בסיסיות המבוצעות ישירות ע"י ה-VM (הקצאת זיכרון, מניפולציית בתים, חיבור/השוואה ב-SmallInteger, גישה למערך). מתודה עם <primitive: X> כוללת אחריו קוד Smalltalk אלטרנטיבי שמתבצע רק אם הפעולה הפרימיטיבית נכשלה! |
6.3–7.6 ★ 3 סוגי ההודעות, כללי קדימות, מבנה מתודה ושרשור Cascade
סדר הקדימויות בשפת Smalltalk (סעיפים 6.3 ו-7.1–7.4):
- 1. סוגריים עגולים
( ... ): משנים את סדר ההערכה (אך בניגוד לסוגריים מרובעים[...]אינם משפיעים על מספר הפעמים שהביטוי מוערך!). - 2. הודעות אונריות (Unary – הקדימות הגבוהה ביותר!): מילה אחת (ללא נקודתיים וללא ארגומנטים), כגון
3 factorial. - 3. הודעות בינריות (Binary – הקדימות השנייה!): תו אחד או יותר מתוך הסימנים המיוחדים (
+ - * / \ ~ < > = @ % | & ? ,) עם ארגומנט יחיד.
⚠️ כלל זהב: לכל האופרטורים הבינריים יש קדימות זהה והם מוערכים משמאל לימין! (ניתן להעמיס אופרטורים אך לא לשנות את קדימותם או מספר האופרנדים שלהם).
• דוגמאות חישוב:3 + 4 * 5$\implies$35|2 + 5 * 4 + 3$\implies$31|2 raisedTo: 1 + 3 factorial$\implies$ קודם אונרי3 factorial(=6), אז בינרי1 + 6(=7), ולבסוף מילת-מפתח2 raisedTo: 7=128! |2 * 3 factorial raisedTo: 2$\implies$(2 * 6) raisedTo: 2=144! - 4. הודעות מילות-מפתח (Keyword – הקדימות הנמוכה ביותר!): מזהה (Selector) המורכב מרצף המילים הארוך ביותר המלוות בנקודתיים
:וארגומנט.
• חשיבות הסוגריים בהודעות מילת-מפתח: בביטויst := 'hello'. st at: (st indexOf: $e) put: $x(שמחליף את'e'ב-'x'ומחזיר$xכשהמחרוזת הופכת ל-'hxllo'), ללא הסוגריים המערכת הייתה מנסה לשלוח ל-stהודעה אחת שגויה בשםat:indexOf:put:!
LinkedList >> add: link after: otherLink
| savedLink |
lastLink == otherLink ifTrue: [^ self addLast: link].
savedLink := otherLink nextLink.
otherLink nextLink: link.
link nextLink: savedLink.
^ link
; הראשונה, ומחזיר את ערך ההודעה האחרונה:
•
3 + 4; + 5 $\implies$ שתיהן נשלחות ל-3 $\implies$ מחזיר 8.
•
2 raisedTo: 2; * 3 factorial $\implies$ המקבל הוא 2; ההודעה השנייה * 6 נשלחת ל-2 (ולא ל-4!) $\implies$ מחזיר 12!
•
10@10 - 10; corner: 30@30; isZero $\implies$ המקבל הוא 10@10; בסוף (10@10) isZero מחזיר false! אבל בלי ה-; הראשונה: 10@10 - 10 corner: 30@30; isZero $\implies$ המקבל של ה-Cascade הוא התוצאה של 10@10 - 10 שהיא הנקודה 0@0, ולכן (0@0) isZero מחזיר true!
•
(10@10 + 20; corner: 30@30) area $\implies$ מלבן מ-10@10 עד 30@30 ששטחו 400.ניתוח ללא שימוש ב-Cascade (בלי ה-
;): (10@10 + 20 corner: 30@30) area יוצר מלבן מ-30@30 עד 30@30 ששטחו 0!
6.5–6.6 בלוקים, תנאים, מימוש הלולאות ואיטרטורים
-
בלוקים (`BlockContext`): פונקציות אנונימיות
[ :x :y | | n | ... ]. מוערכים ע"יvalue,value:,value:value:(עד 4 פרמטרים), אוvalueWithArguments: anArray(למעל 4 פרמטרים). בלוקים יכולים לגשת למשתנים שהוגדרו מחוץ לבלוק, ופקודת החזרה^בתוך בלוק גורמת ליציאה מיידית מן המתודה שבה נכתב הבלוק. -
מימוש מבני בקרה, תנאים ולולאות ב-Smalltalk:
// 1. מימוש תנאים במחלקות True ו-False: True >> ifTrue: trueAlternativeBlock ifFalse: falseAlternativeBlock ^ trueAlternativeBlock value False >> ifTrue: trueAlternativeBlock ifFalse: falseAlternativeBlock ^ falseAlternativeBlock value // 2. מימוש רקורסיבי של לולאת whileTrue: במחלקה BlockContext: BlockContext >> whileTrue: aBlock ^ [self value] whileTrue: [aBlock value] // 3. מימוש timesRepeat: ב-Integer ו-to:do: ב-Number באמצעות whileTrue: Integer >> timesRepeat: aBlock | count | count := 1. [count <= self] whileTrue: [aBlock value. count := count + 1] Number >> to: stop do: aBlock | nextValue | nextValue := self. [nextValue <= stop] whileTrue: [aBlock value: nextValue. nextValue := nextValue + 1] -
הבחנה בין
1 to: 10 do: [...]לבין(1 to: 10) do: [...]: ללא הסוגריים נשלחת למספר1ההודעהto:do:; עם הסוגריים נוצר תחילה עצם מסוגIntervalואליו נשלחת ההודעהdo:(תוצאת הביצוע זהה:55). וב-Interval>>collect: aBlockנעשה שימוש ב-result := self species new: self sizeכאשרInterval>>speciesמחזירה את המחלקהArray!
8.1–8.3 ★ מודל העצמים, 3 סוגי עצמים ב-Squeak, כימוס, מתודות מחלקה ו-Dog/Hyena
-
סיווג העצמים ב-Squeak לשלושה סוגים לפי ייצוגם בזיכרון:
1. עצמים רגילים עם משתני מופע: מועברים באמצעות מצביע (`reference`).
2. שלמים מסוגSmallInteger: מועברים על פי ערך (`by value`) ונשמרים ישירות בתוך המצביע עצמו (Immediate Value) במקום כעצם נפרד בזיכרון.
3. עצמים מאונדקסים (`Indexed Objects`): כגון מערכים (`Array`), המתייחסים לרצף זיכרון המכיל אוסף של עצמים. -
גבולות הכימוס (Encapsulation) ב-Smalltalk מול Java ו-C++:
• ב-Java ו-C++ גבולות הכימוס מוגדרים בתחום המחלקה (מופע אחד יכול לגשת ישירות למשתנים הפרטיים של מופע אחר מאותה מחלקה).
• ב-Smalltalk גבולות הכימוס מוגדרים בתחום המופע: משתני המופע נגישים ישירות רק למופע עצמו (ולמתודות בתתי-מחלקות, בדומה ל-protectedב-Java), אך לא למופע אחר מאותה מחלקה!
• דוגמת יישום (`Rectangle>>containsRect: aRect`): הביטוי^ aRect origin >= origin and: [aRect corner <= corner]ניגש ישירות למשתני המופעoriginו-cornerשל המקבל (self), אך חייב לקרוא למתודות הגישהaRect originו-aRect cornerעבור הארגומנט! היתרון: ניתן להעביר ל-containsRect:מלבן ממימוש אחר לגמרי (שמיוצג ע"י נקודה, רוחב וגובה) כל עוד הוא מספק מתודות גישהoriginו-corner!
• מתודות ב-Smalltalk: כולן ציבוריות (`public`). שיוך לפרוטוקולprivateאו הוספת קידומתpvtלשם המתודה הם מוסכמה בלבד ואינם חוסמים גישה מחוץ למחלקה.
🎨 ניתוח שליחת הודעה למחלקה (`Color blue`) ושני ההבדלים בין מתודות מחלקה ל-static ב-Java:
• היכן מוגדרת Color blue? המתודה blue מוגדרת ב-Color class (כמתודת מחלקה, ולא ב-Color!). לכן:
1.
Color new blue $\implies$ שגיאה! (`MessageNotUnderstood: Color>>blue`) כי מופע של Color אינו מבין מתודת מחלקה!
2.
Color blue $\implies$ תקין! שולח את ההודעה לעצם המחלקה Color ומחזיר מופע צבע כחול.
3.
Color blue blue $\implies$ שגיאה! כי Color blue מחזיר מופע של Color, ושליחת blue שנייה למופע נכשלת!
• 2 הבדלים מהותיים בין מתודות מחלקה ב-Smalltalk למתודות
static ב-Java:
1. קישור דינמי מול סטטי: ב-Java מתודות
static מקושרות בזמן הידור (קישור סטטי), ואילו ב-Smalltalk מתודות מחלקה מקושרות בזמן ריצה (קישור דינמי) ותומכות בירושה ובדריסה מלאה!
2. הפעלה דרך מופעים: ב-Smalltalk מתודות מחלקה ניתנות להפעלה רק דרך עצם המחלקה ולא דרך מופעים, ואילו ב-Java מתודות
static ניתנות להפעלה גם דרך המחלקה וגם דרך מופעים.
🐕 משתני מופע של מחלקה (`class instance variables`): דוגמת `Dog` ו-`Hyena`:
במטה-מחלקהDog class מוגדר instanceVariableNames: 'count', והמחלקה Hyena יורשת מ-Dog.
• לכל תת-מחלקה נוצר עותק פרטי ונפרד משלה של
count (בניגוד ל-static ב-Java/C++ או ל-classVariables ב-Smalltalk שבהם קיים עותק יחיד משותף!). כמו כן, מחלקה אינה יכולה לגשת ישירות למשתני מופע של עצם, ועצם אינו יכול לגשת ישירות למשתני מופע של מחלקה.
• מדוע
Hyena new מקדם רק את count של Hyena ולא את של Dog? כי Hyena יורשת את Dog class>>new (שמבצעת count := count + 1. ^ super new). בזמן ריצת Hyena new המקבל (self) הוא Hyena ולכן מקודם ה-count של Hyena, ואילו הקריאה ל-super new מתחילה את החיפוש ממחלקת-העל של המחלקה שבה כתובה המתודה (מחלקת-העל של Dog class שהיא Object class!) ולכן מדלגת על Dog class>>new! (כנ"ל ב-Hyena initialize).
8.3–8.4 ★ דפוס Singleton, מחלקות מופשטות ו-Traits
uniqueInstance:
WebServer class >> uniqueInstance
uniqueInstance ifNil: [uniqueInstance := self new].
^ uniqueInstance
מדוע self new ולא WebServer new? כדי שאם תת-מחלקה תירש מ-WebServer ותקבל את ההודעה uniqueInstance, ייווצר מופע מסוג תת-המחלקה (self) ולא מסוג WebServer!
self subclassResponsibility (למשל Number>>* aNumber; שרשרת ההפשטה: Magnitude → Number → Integer → SmallInteger הקונקרטית).•
Traits: שיתוף אוספי מתודות בין מחלקות ללא קשר ירושה:
Trait named: #TAuthor uses: {} category: 'SBE-Quinto'
TAuthor >> author ^ 'on'
// שילוב במחלקה עם מילת המפתח uses:
BorderedMorph subclass: #SBEGame
uses: TAuthor instanceVariableNames: 'cells' ...
8.5–8.7 ★ איתור מתודות, הגדרה מחדש מול הרחבה, self מול super ומשתנים משותפים
-
הגדרה מחדש (Overriding) מול הרחבה (Extension):
• הגדרה מחדש (Overriding):EllipseMorph>>defaultColor ^ Color yellowדורסת לחלוטין אתMorph>>defaultColor ^ Color blue.
• הרחבה (Extension):BorderedMorph>>initializeמרחיבה את מתודת האב ע"י קריאה ל-super initializeואחריה אתחול משתני המופע שלה.
כלל הברזל: שליחה ל-self מול שליחה ל-super ו-doesNotUnderstand::
• self ו-super מתייחסות שתיהן לאותו עצם מקבל בדיוק!• שליחה ל-
self (איתור דינמי): החיפוש מתחיל במחלקה של המקבל (ידועה רק בזמן ריצה).• שליחה ל-
super (איתור סטטי): החיפוש מתחיל במחלקת-העל של המחלקה שבה נמצאת המתודה המבצעת את השליחה ל-super (ידועה מראש בזמן הידור!).• הודעה שאינה מובנת: אם החיפוש מגיע לראש ההיררכיה (
ProtoObject) ללא הצלחה, ה-VM שולחת ל-self את ההודעה doesNotUnderstand: (המוגדרת ב-Object, יוצרת חריג MessageNotUnderstood ופותחת את ה-Debugger; ניתן לדרוס אותה בתת-מחלקה!).
-
3 סוגי המשתנים המשותפים ב-Smalltalk:
1. משתנים גלובליים (`Global Variables`): מוגדרים במרחב השמותSmalltalk(מופע שלSystemDictionary), כגון:Transcript(מופע שלTranscriptStream),Smalltalk,Sensor(מופע שלEventSensor),World(מופע שלPasteUpMorph),ActiveHand(מופע שלHandMorph),Undeclared(מופע שלSystemDictionary) ו-SystemOrganization(מופע שלSystemOrganizer). הוספת גלובלי חדש: השמה ב-Workspace אוSmalltalk at: #Name put: obj(והסרה ב-Smalltalk removeKey: #Name).
2. משתני מחלקה (`Class Variables`): מוגדרים ב-classVariableNames:(למשל ב-Character:'CharacterTable ClassificationTable...'), משותפים לעצם המחלקה, לכל מופעיה ולכל תתי-המחלקות שלה, ונגישים ממתודות מופע וממתודות מחלקה. מאותחלים ב-ClassName class>>initialize(יש להפעילה מפורשות כי היא מופעלת אוטומטית רק כשהמחלקה נטענת לראשונה לזיכרון!) או באתחול עצל במתודת גישה (שמונע גישה ישירה ומוסיף תקורה).
3. מאגרים משותפים (`Pool Dictionaries`): מילוני משתנים המשותפים לקבוצת מחלקות שאינן קשורות בהיררכיית ירושה (כיום מומלץ להגדירם כתת-מחלקות שלSharedPoolולציינם תחתpoolDictionaries:).
9.1–9.10 ★ כלי הפיתוח ב-Squeak, Monticello ודיבוג המתודה suffix ב-Debugger
-
כפתורי ה-
System Browserו-5 צבעי כפתורinheritance:browse (Ctrl+b)(ובחלונית הקטגוריות מציגback...למחלקות שהוצגו לאחרונה),senders,implementors,versions,hierarchy,vars,source, וכפתורinheritanceשצבעו מציין: ורוד = דריסה ללא קריאה ל-super; ירוק = דריסה תוך שימוש ב-super; זהב = מוגדרת מחדש בתת-מחלקה; כתום = ורוד + זהב; סגול = ירוק + זהב.
• הפעלה תכנותית דרךSystemNavigation default:SystemNavigation default browseClass: EllipseMorph,browseHierarchy: EllipseMorph, ו-browseAllCallsOn: #drawOn:. -
ניהול מארזים ב-
Monticello: מארזSqueakLinkכולל: (1) את כל המחלקות בקטגוריהSqueakLinkובקטגוריות המתחילות ב-SqueakLink-(למעט מתודות בפרוטוקולים המתחילים ב-*), ו-(2) כל מתודה בכל מחלקה אחרת (כגוןObject>>asSQL) שנמצאת בפרוטוקול ששמו*squeaklinkאו מתחיל ב-*squeaklink-! כתוצאה מכלל זה (הממומש במחלקהPackageInfo), כל מחלקה וכל מתודה שייכת למארז אחד בלבד. גיבוי מקומי של קובצי.mczנשמר בתיקייהpackage-cache. -
InspectorמולExplorer: ב-Inspectorהפריט השני הואall inst varsו-selfקשור תמיד לעצם הראשי; ב-Explorerהמשתנהselfבחלונית התחתונה מתקשר דינמית לאיבר/המשתנה הספציפי שנבחר כרגע בעץ! -
כלי סביבה נוספים:
Process Browser(תהליכים רצים),Message Names(חיפוש מתודות ללא פתיחת חלון חדש),Change Set Browser(simple change sorter) ו-Change Sorter(dual change sorterלהעברת שינויים בין מקבצי שינויים),File List Browser, ושחזור קוד שהלך לאיבוד מתוך קובץ השינויים (.changes).
🐞 מקרה הבוחן של ה-Debugger: איתור ותיקון 3 הבאגים במתודה `String>>suffix`:
// הגרסה השגויה הראשונית שנכשלת בהרצת 'readme.txt' suffix (זורקת Error: Object is not in the collection):
String >> suffix
| dot dotPosition |
dot := FileDirectory dot. "באג 1: מחזיר את המחרוזת '.' בעוד (self at: i) מחזיר תו $. (ו- $. = '.' הוא false!)"
dotPosition := (self size to: 1 by: -1) detect: [:i | (self at: i) = dot]. "באג 3: קורס אם אין נקודה במחרוזת!"
^ self copyFrom: dotPosition to: self size "באג 2: מחזיר '.txt' כולל הנקודה במקום 'txt'!"
// הגרסה המתוקנת והמלאה לאחר 3 התיקונים ב-Debugger:
String >> suffix
| dot dotPosition |
dot := FileDirectory dot asCharacter. "תיקון 1: המרה לתו $. "
dotPosition := (self size to: 1 by: -1) detect: [:i | (self at: i) = dot] ifNone: [self size]. "תיקון 3"
^ self copyFrom: (dotPosition + 1) to: self size "תיקון 2: יחזיר 'txt', ואם אין נקודה יחזיר מחרוזת ריקה ''"
חלק V: מחלקות, ירושה ואוספים ב-Squeak (פרקים 10–12)
5 מרכיבי המחלקה, מחלקות הבסיס (Object, Number, Character, String, Symbol, Boolean) והיררכיית האוספים (Collections).
10.1–10.6 המושג "מחלקה" (Class): 3 תפקידים, 5 מרכיבים ובבואה (Reflection)
חשיבות המחלקה כמנגנון הפשטה: המחלקה היא מנגנון ההפשטה החשוב ביותר ב-OOP משום שהיא מרכזת במקום אחד את מה שמשותף לכל העצמים מאותה קבוצה: הפרוטוקול (אוסף ההודעות), מימוש ההתנהגות (המתודות) והמבנה (הצהרות משתני המופע). (רק הזהות והמצב הספציפי שונים מעצם לעצם).
תשובה: כן ולא! ככלל, בשפות מונחות עצמים מקובל שכל העצמים מאותה מחלקה מתנהגים בהתאם למוגדר במחלקה, אבל אפשרי בהחלט לתכנן מחלקה שבה כל עצם קובע לעצמו מימוש שונה של המתודות! לדוגמה, ניתן לממש מחסנית
class Stack ב-C++ שבה void (*push)(int element); ו-int (*pop)(void); מוגדרים כמצביעים לפונקציה (Function Pointers) במשתני המופע במקום הגדרת הפונקציה עצמה!
- תבנית ליצירת עצמים חדשים (למשל ביטוי
newהמקבל את שם המחלקה). - הצהרה על הפרוטוקול שהעצמים "מבינים" (משמש שפות עם הטפסה סטטית לאיתור שגיאות בזמן הידור).
- ישות בפני עצמה בזמן ריצה (מיוצגת ע"י עצם ב-Smalltalk וב-Java Reflection).
- 1. כור יצירה / מכרה עצמים (`Forge`): אוסף ההודעות ליצירת עצמים חדשים (חתימות הבנאים).
- 2. בית-חרושת לייצור עצמים (`Mill`): המימוש של הבנאים.
- 3. פרוטוקול (`Protocol`): אוסף ההודעות שהעצמים מבינים (חתימת המתודות).
- 4. התנהגות (`Behavior`): המימוש של המתודות.
- 5. מבנה (`Structure`): אוסף משתני המופע (מבנה הזיכרון של כל עצם).
| דוגמה והמחשה בשפות שונות | אילו מ-5 מרכיבי המחלקה קיימים בה? | יצירת עצמים חדשים |
|---|---|---|
10.5.1 מנשק ב-Java (interface printable) |
מגדיר את הפרוטוקול (`Protocol`) בלבד, אך לא את ההתנהגות ולא את המבנה. | אינו מאפשר ליצור עצמים חדשים |
10.5.2 מחלקה אנונימית ב-Java (new printable { ... }) |
כוללת מבנה (private int s;) והתנהגות (void print()), אך חסרת `Forge` ליצירת מופעים נוספים. |
נוצר מופע יחיד בלבד; אינה מאפשרת ליצור עצמים נוספים |
10.5.3 מחלקה מופשטת ב-++C (virtual void draw() = 0;) |
ניתן להגדיר בה פרוטוקול, מבנה, מתודות ובנאים (עבור תתי-מחלקות). | אינה מאפשרת ליצור עצמים חדשים ישירות |
10.5.4 מחלקה ב-Smalltalk (המחלקה Point) |
מבנה (instanceVariableNames: 'x y'), מתודות-מופע (setX:setY:, x, y) ומתודות-מחלקה ליצירה: Point class>>x: xInteger y: yInteger ^ self basicNew setX: xInteger setY: yInteger. |
כן – המחלקה עצמה היא עצם המגיב להודעות יצירה! |
assignField(Object o, String fieldName, Object value) מבצעת: Class c = o.getClass(); Field f = c.getField(fieldName); f.set(o, value);.
• האם
getClass() מחזירה עצם? המתודה o.getClass() אכן מחזירה עצם לכל דבר (מופע של המחלקה java.lang.Class), ולכן ניתן לקרוא עליו למתודה c.getField(...) וגם לקרוא עליו שוב ושוב ל-getClass()!
• שרשור קריאות
getClass(): מה מחזירה הקריאה o.getClass().getClass().getClass().getClass(); ב-Java? היא מחזירה תמיד את העצם המייצג את המחלקה java.lang.Class! הסיבה לכך היא שב-Java (מערכת בת 3 רמות בלבד, כמפורט בפרק 18) כל עצם-מחלקה הוא מופע של המחלקה Class, והמחלקה Class היא מופע של עצמה (Class.class.getClass() == Class.class)!
11.1 המחלקה Object (הדפסה, שוויון ו-hash, שייכות, העתקה וניפוי שגיאות)
בראש היררכיית המחלקות ב-Squeak נמצאת ProtoObject (המרכזת את ההגדרות המינימליות ביותר), ומתחתיה Object. ל-Object אין משתני מופע ואסור להוסיף לה כאלה, כדי לא לשנות את מבנה הזיכרון של מחלקות שה-VM מסתמכת על מבנן (כגון SmallInteger).
Object>>printOn: aStream נכתב לזרם 'an ' (אם שם המחלקה מתחיל בתנועה title first isVowel) או 'a ' ואחריו שם המחלקה (באמצעות nextPutAll:).
• ב-
Point>>printOn: aStream מוגדר:
Point >> printOn: aStream
x printOn: aStream.
aStream nextPut: $@.
(y isNumber and: [y negative]) ifTrue: [aStream space]. "שורות 3-4"
y printOn: aStream
• שאלת הבנה (מדוע קיימות שורות 3–4 ב-Point>>printOn:?): אם שיעור ה-y הוא מספר שלילי (למשל בנקודה 3 @ -4), ללא הוספת הרווח (aStream space) היה מודפס הרצף 3@-4, והמפענח של Smalltalk היה מפרש את שני התווים הצמודים @- כאופרטור בינרי אחד לא-מוגדר במקום האופרטור @ ואחריו המספר השלילי -4!
• קדימות אופרטורים בהדפסה (השמטת סוגריים ב-
3@4 printString): הביטוי (3@4) printString מחזיר את המחרוזת '3@4', אך ללא סוגריים 3@4 printString מריץ קודם את ההודעה האונארית 4 printString (שמחזירה '4') ואז יוצר עצם Point ששיעור ה-y שלו הוא מחרוזת: 3@'4'!
• עצמים שהם `self evaluating`: המערך הדינמי
{3@4. 4.5} מדפיס {3@4 . 4.5} ולכן הוא self evaluating, ואילו {3@4. 4+5} מדפיס {3@4 . 9} ולכן ביטוי המקור אינו זהה לייצוג המודפס.
Object המתודה = ממומשת כבדיקת זהות: ^ self == anObject (ואי-שוויון ~= מחזיר את שלילת =).
• ב-
Point>>= aPoint נבדק שוויון הסוג והערכים: self species = aPoint species ifTrue: [^ x = aPoint x and: [y = aPoint y]] ifFalse: [^ false].
• חוק ברזל למבחן: אם מגדירים מחדש את
=, חובה להגדיר מחדש גם את hash כך ששני עצמים שווים (a = b) יחזירו תמיד אותו מספר hash!
4 isMemberOf: SmallInteger $\implies$ true, אך 4 isMemberOf: Number $\implies$ false (בודקת אם העצם הוא מופע בדיוק של המחלקה הנתונה).
•
4 isKindOf: Number וגם 4 isKindOf: Object $\implies$ true (בודקת אם העצם הוא מסוג הארגומנט או כל תת-מחלקה שלו).
•
4 respondsTo: #+ וגם 'hello' respondsTo: #size $\implies$ true.
a1 := {{'a'. 'b'}. {'c'. 'd'}}: ב-a2 := a1 shallowCopy שינוי תת-מערך (a1 at: 1) at: 1 put: 'z' ישנה גם את a2 כי תתי-המערכים משותפים! ב-a2 := a1 copyTwoLevel שינוי במערך דו-ממדי לא ישפיע על a2, אך במערך תלת-ממדי {{{'a'}}...} שינוי ברמה השלישית עדיין ישפיע על שניהם (ולכך נדרשת deepCopy).
• המתודה
Object>>copy מבצעת shallowCopy ואז שולחת לעותק את ההודעה postCopy (תתי-מחלקות דורסות את postCopy להשלמת השכפול מעבר לרדוד).
self halt (פתיחת ה-Debugger), self assert: [aBlock] (תכנות לפי חוזה Design by Contract – זורק AssertionFailure אם התנאי false), self deprecated: 'use ifNotNilDo:' (כמו ב-doIfNotNil:), doesNotUnderstand:, self error: 'msg', self subclassResponsibility, ו-self shouldNotImplement (לביטול מתודה שנורשה אך אינה מתאימה לתת-המחלקה).
• מתודות בדיקה כגון
isArray, isBoolean, isBlock אינן מומלצות לשימוש כי הן מפרות את עקרון הכימוס, למעט ProtoObject>>isNil ו-Object>>notNil השימושיות. מתודת האתחול initialize מוגדרת ב-ProtoObject ונקראת אוטומטית מתוך new.
11.2–11.5 מספרים, תווים, מחרוזות, Symbol ובוליאניים
Magnitude (מופשטת) האופרטורים < ו-= מוגדרים כמתודות מופשטות ומשמשים למימוש כל השאר (<=, >, >=, between:and:, min:, max:).
• ב-
Number האופרטורים +, -, *, / הם מופשטים. // מבצע חלוקת שלמים מעוגלת כלפי מטה, ו-\\ מחזיר את השארית.
• תחת
Number יורשות: Float (עם מתודות מחלקה לקבועים Float pi ו-Float e), Fraction (נוצר ב-Fraction numerator:denominator: או ע"י / בין שלמים ונשמר תמיד בצורה מצומצמת: 6/8 → (3/4)), ו-Integer (מופשטת, עם factorial, atRandom, isPrime, gcd: ו-3 תתי-המחלקות SmallInteger, LargePositiveInteger, LargeNegativeInteger המומרות אוטומטית זו לזו!).
•
Character (יורשת מ-Magnitude): נוצרת עם $a, Character tab / cr / space / backspace / escape / enter או Character value: asciiCode.
Object → Collection → SequenceableCollection → ArrayedCollection → String (מופשטת) → ByteString / Symbol.
• השוואת
ByteString מול Symbol: עבור s1 := 'hello'. s2 := 'hello': המחרוזות שוות (s1 = s2 הוא true), אך הן מופעים נפרדים בזיכרון (s1 == s2 הוא false), וביצוע s1 at: 1 put: $H או s1 := s1, ' world' משנה רק את s1 מבלי להשפיע על s2! לעומת זאת, שני Symbol זהים (#hello) הם תמיד אותו עצם בזיכרון (==) ואינם ניתנים לשינוי (`Immutable`)!
• מתודות
String חשובות: שרשור עם פסיק ,, replaceAll:with:, replaceFrom:to:with: (משנות במקום), copyReplaceAll:with: (יוצרת עותק חדש), findString:, ו-'123+12' asNumber → 123!
•
Boolean → True / False: false ifTrue: [...] מחזיר nil, ו-true ifFalse: [...] מחזיר nil. ב-and: ו-or: כאשר אין צורך להעריך את הבלוק מוחזר self.
12.1–12.4 ★ אוספים ב-Squeak: היררכיית Collection, 8 סיווגי יכולות ו-5 דרכי מימוש
-
7 הפרוטוקולים המשותפים ב-`Collection`:
(1) גישה:
size, capacity, at:, at:put:| (2) בדיקה:isEmpty, includes:, contains:, occurrencesOf:| (3) הוספה:add:, addAll:| (4) הסרה:remove:, remove:ifAbsent:, removeAll:| (5) אנומרציה:do:, collect:, select:, reject:, detect:, detect:ifNone:, inject:into:, count:, anySatisfy:| (6) הסבה:asBag, asSet, asOrderedCollection, asSortedCollection, asSortedCollection:, asArray| (7) יצירה (מתודות מחלקה):new, new:, with:, with:with: (עד 4 איברים), withAll:. -
5 קטגוריות המימוש של אוספים:
(1) מימושי מערך (
Array– מאחסן איברים במשתנה מופע מאונדקס של האוסף ולכן גודלו קבוע), (2) מימושים סדורים (OrderedCollection, SortedCollection– מאחסנים איברים במערך פנימי השמור באחד ממשתני המופע של האוסף, כך שניתן להקצות מערך גדול יותר לפי הצורך!), (3) מימושי Hash (Set, Dictionary– משתמשים במערך כטבלת גיבוב; ו-Bagשמשתמש בפנים ב-Dictionaryשבו האלמנטים הם המפתחות ומספר המופעים הוא הערכים!), (4) מימושים מקושרים (LinkedList– רשימה מקושרת סטנדרטית), (5) מימושי אינטרוול (Interval– ממומש באמצעות 3 שלמים בלבד:start, stop, step).
| מחלקת אוסף | סדור / גישה | דינמי (Growable)? | כפילויות? | דרכי יצירה ומתודות ייחודיות |
|---|---|---|---|---|
| Array | אינדקס 1..n |
לא (גודל קבוע) | כן (הטרוגני; יש גם הומוגניים ByteArray, IntegerArray, FloatArray) |
Array new: 5 ($\to$ 5 איברי nil), Array with: 1 with: 2, #(...), {...}. גישה ב-at: ו-at:put:. |
| OrderedCollection | לפי סדר הכנסה | כן | כן | add: (מוסיף בסוף), addFirst:, addLast:, insert:before:, at:, at:put:, remove: (מסיר את המופע הראשון שמתאים!). |
| SortedCollection | ממוין (<= או sortBlock:) |
כן | כן | SortedCollection sortBlock: [:s1 :s2 | s1 area <= s2 area]. |
| Interval | אינדקס at: (אך Immutable!) |
לא | לא | Interval from: 1 to: 10 by: 2 או 1 to: 10 by: 2. תומך ב-at: אך אסור לבצע עליו at:put:! |
| Dictionary / IdentityDictionary | לפי מפתח (Keyed) | כן (Hash) | מפתחות ייחודיים, ערכים חופשיים | זוגות Association (#one->1). יצירה עם Dictionary newFrom: {#one->1} או Dictionary withAll: #(7 3 1 3) (מצמיד מפתחות סידוריים 1..4!). מתודות: at:put:, at:ifAbsent:, removeKey:, keys (מחזיר Set!), values (מחזיר Array!), associations. IdentityDictionary משווה מפתחות ב-== (יעיל ל-Symbol). |
| Set | לא (אין at:!) |
כן (Hash) | לא (מנפה כפילויות: add: 3; add: 9/3 שומר איבר אחד!) |
Set newFrom: #(1 2 2 3), union:, intersection:, includes:. |
| Bag | לא | כן | כן (קבוצה עם חזרות) | ממומש ע"י Dictionary (איבר $\to$ מונה מופעים occurrencesOf:). |
12.5–12.6 ★ מימוש כל האיטרטורים במחלקה Collection ו-4 כללי הזהב
// מימוש איטרטורים מרכזיים באוספים (do:, collect:, select:, reject:, detect:, inject:into:):
SequenceableCollection >> do: aBlock
1 to: self size do: [:index | aBlock value: (self at: index)]
// גרסאות נוספות של do: -> doWithout:, withIndexDo:, reverseDo:, ובמילון: keysDo:, valuesDo:, associationsDo: (במילון do: סורקת את הערכים values!)
Collection >> collect: aBlock
| newCollection |
newCollection := self species new.
self do: [:each | newCollection add: (aBlock value: each)].
^ newCollection
// זהירות: collect: על String תחזיר String ולכן הבלוק חייב להחזיר תווים (Character), אחרת תיזרק שגיאה!
Collection >> select: aBlock
| newCollection |
newCollection := self species new.
self do: [:each | (aBlock value: each) ifTrue: [newCollection add: each]].
^ newCollection
Collection >> reject: aBlock
^ self select: [:element | (aBlock value: element) == false]
Collection >> detect: aBlock
^ self detect: aBlock ifNone: [self errorNotFound: aBlock]
Collection >> detect: aBlock ifNone: exceptionBlock
self do: [:each | (aBlock value: each) ifTrue: [^ each]].
^ exceptionBlock value
Collection >> inject: thisValue into: binaryBlock
| nextValue |
nextValue := thisValue.
self do: [:each | nextValue := binaryBlock value: nextValue value: each].
^ nextValue
// הודעות איטרציה נוספות: count: [:each | ...] ו-anySatisfy: [:each | ...]
⚠️ 4 הערות שימושיות וכללי מפתח לעבודה עם אוספים:
- המתודה
add:מחזירה את האיבר שהתקבל להוספה ולא את האוסף! (OrderedCollection new add: 1מחזיר1). לקבלת האוסף שולחים; yourself. - אסור להסיר איבר מאוסף בזמן שמבצעים עליו איטרציה! במקום זאת משכפלים את האוסף באמצעות
copyומבצעים את האיטרציה על העותק המשוכפל. - הגדרת
=מחייבת הגדרתhash: אוספים משתמשים ב-=וב-hashלבדיקת שוויון בין איברים. - עדיף להשתמש בעצמים `Immutable` כמפתחות במילונים וכאיברים בקבוצות (`Set`): כי ערך ה-
hashשל עצמים `Mutable` עלול להשתנות במהלך חייהם, ואז שני עצמים שווים עלולים להיחשב כשונים.
חלק VI: סימולציה – חידת המלכות והסביבה הגרפית Morphic (פרקים 13–17)
ייצוגים מתמטיים וגרפיים לחידת N המלכות, הפתרון המבוזר ב-Smalltalk, הכללה ל-N×N×N וממשק Morphic.
13–14.4 ★ רקע, ייצוג פרמוטציה ו-4 אופני הייצוג של חידת N המלכות
-
הגדרת בעיית 8 המלכות ועקרונות הסימולציה:
• הגדרת חידת 8 המלכות: הצב 8 מלכות על לוח שחמט כך שאף מלכה אינה מאיימת על אף מלכה אחרת (אין שתי מלכות באותה שורה, באותה עמודה או באותו אלכסון).
• רקע היסטורי: הוצגה ב-1848 ע"י שחקן השחמט מקס בזל (Max Bezzel), ונחקרה ע"י גאוס (C.F. Gauss, 1865), פוליה (G. Polya, 1918) ולוקאס (E. Lucas, 1960). במדעי המחשב היא דוגמה קלאסית לאלגוריתמיBacktrackingולסימולציה בעזרת עצמים (הגישה שהולידה את שפתSimula).
• ייצוג פתרון כפרמוטציה: מכיוון שבכל עמודה ובכל שורה יש בדיוק מלכה אחת, ניתן לייצג פתרון כפרמוטציה מעל $\{0, 1, 2, 3, 4, 5, 6, 7\}$, למשל הפתרון החוקי: $$\begin{pmatrix} 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 \\ 7 & 3 & 0 & 2 & 5 & 1 & 6 & 4 \end{pmatrix}$$
1. לא באותה עמודה: $i_k \ne i_l$
2. לא באותה שורה: $j_k \ne j_l$
3. לא באותו אלכסון: $i_k + j_k \ne i_l + j_l$
4. לא באותו אלכסון נגדי: $i_k - j_k \ne i_l - j_l$
1. לא באותה עמודה: $x \not\equiv y \pmod N$
2. לא באותה שורה: $\lfloor x/N \rfloor \ne \lfloor y/N \rfloor$
3. לא באותו אלכסון: $(x \bmod N) + \lfloor x/N \rfloor \ne (y \bmod N) + \lfloor y/N \rfloor$
4. לא באותו אלכסון נגדי: $|(x \bmod N) - \lfloor x/N \rfloor| \ne |(y \bmod N) - \lfloor y/N \rfloor|$
• נייצג כל משבצת בלוח כצומת ב-$V$ ($|V|=N^2$), ותהא ב-$E$ קשת בין כל שני צמתים שנמצאים על אותה עמודה, אותה שורה או אותו אלכסון (יחס איום).
• פתרון: כל קבוצה יציבה $V' \subseteq V$ בגודל $\sqrt{|V|} = N$.
• נייצג כל משבצת כצומת ב-$V$, ותהא ב-$E$ קשת בין כל שני צמתים שאינם נמצאים על אותה עמודה, אותה שורה או אותו אלכסון (יחס אי-איום, הגרף המשלים ל-14.3).
• פתרון: כל קליק $V' \subseteq V$ בגודל $\sqrt{|V|} = N$ ($\binom{N}{2}$ קשתות).
14.5–14.6 ★ 8 סימטריות הלוח ופתרונות זרים ממש
-
8 סיבובי הסימטריה היוצרים פתרונות שקולים איזומורפית:
• 4 סיבובים דו-ממדיים נגד כיוון השעון: ב-$90^\circ$, ב-$180^\circ$, ב-$270^\circ$, וב-$360^\circ$ (זהות עצמית).
• 4 סיבובים תלת-ממדיים של $180^\circ$ במרחב (שיקופים): סביב הציר האופקי, סביב הציר האנכי, סביב הציר האלכסוני ($i \ne j$), וסביב הציר האלכסוני הנגדי ($i = j$).
| N (Board Size) | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| סך כל הפתרונות עבור N | 1 | 0 | 0 | 2 | 10 | 4 | 40 | 92 | 352 | 724 |
| פתרונות זרים ממש (עצמאיים) | 1 | 0 | 0 | 1 | 2 | 1 | 6 | 12 | 46 | 92 |
* מספר הפתרונות הזרים ממש: עבור $N = 1, 2, 3, 4, 5, 6, 7, 8$ מספר הפתרונות הזרים ממש (שאינם שקולים איזומורפית תחת 8 הסימטריות) הוא 1, 0, 0, 1, 2, 1, 6, 12 בהתאמה.
15.1 ★ פתרון מונחה עצמים לחידת 8 המלכות: המחלקה Board ושרשרת המלכות
בפתרון מונחה העצמים (פרק 15), לכל מלכה מוקצית עמודה כנחלתה הבלעדית בלוח, והמלכות דנות ביניהן באמצעות שליחת הודעות רק לגבי השורה (row) שכל אחת תתפוס. המחלקה Board מחזיקה משתנה מופע יחיד בלבד (lastQueen) המצביע לראש רשימה מקושרת של 8 מלכות שבסופה מלכה מנוונת (QueenTerminator):
// הגדרת המחלקה Board, אתחול שרשרת המלכות והמתודה solve:
Object subclass: #Board
instanceVariableNames: 'lastQueen'
classVariableNames: '' poolDictionaries: '' category: 'OOD-8Queens'
Board >> initialize
lastQueen := QueenTerminator new.
(1 to: 8) do: [:i |
lastQueen := Queen new setColumn: i neighbor: lastQueen]
Board >> solve
Transcript clear.
lastQueen firstSolution
ifTrue: [
Transcript show: lastQueen result printString; cr.
[lastQueen nextSolution] whileTrue: [
Transcript show: lastQueen result printString; cr].
Transcript show: 'End of Solutions.'; cr]
ifFalse: [
Transcript show: 'No Solution.'; cr]
// הרצה ופלט:
// board := Board new initialize. board solve
// OrderedCollection (1 5 8 6 3 7 2 4) ... (92 פתרונות)
15.2–15.4 ★ המחלקה Queen: שלושת משתני המופע ו-7 מתודות הפרוטוקול
למחלקה Queen יש 3 משתני מופע: row (השורה הנוכחית 1..8), column (מספר העמודה הקבוע 1..8), ו-neighbor (המצביע הקבוע למלכה השכנה משמאל).
Object subclass: #Queen
instanceVariableNames: 'row column neighbor'
classVariableNames: '' poolDictionaries: '' category: 'OOD-8Queens'
// --- 15.3 ארבע מתודות הפרוטוקול החיצוני ---
Queen >> setColumn: aColumn neighbor: aQueen
column := aColumn.
neighbor := aQueen
Queen >> firstSolution
neighbor firstSolution.
row := 1.
^ self moveToNextNonAttackedRow
Queen >> nextSolution
^ self advance and: [self moveToNextNonAttackedRow]
Queen >> result
"add row to the list"
^ neighbor result addLast: row; yourself
// --- 15.4 שלוש מתודות התקשורת הפנימית ("פרטיות") בין המלכות ---
Queen >> advance
row = 8
ifTrue: [
neighbor nextSolution ifFalse: [^ false].
row := 0].
row := row + 1.
^ true
Queen >> moveToNextNonAttackedRow
[neighbor canAttack: row x: column]
whileTrue: [self advance ifFalse: [^ false]].
^ true
Queen >> canAttack: testRow x: testColumn
| columnDifference |
columnDifference := testColumn - column.
((row = testRow
or: [row + columnDifference = testRow])
or: [row - columnDifference = testRow])
ifTrue: [^ true].
^ neighbor canAttack: testRow x: testColumn
🔍 4 דגשים קריטיים על מתודות Queen למבחן:
- ב-
canAttack:x:: לא בודקיםcolumn = testColumnכי לכל מלכה הוקצתה עמודה ייחודית באתחול! נבדקים רק 3 תנאים: אותה שורה (row = testRow), אלכסון עולה (row + columnDifference = testRow) ואלכסון יורד (row - columnDifference = testRow). - ב-
advance: כאשרrow = 8המלכה מבקשת משכנתהneighbor nextSolution(Backtracking!), מאפסתrow := 0ואז מקדמתrow := row + 1כך שהיא מתמקמת בשורה1מול הסידור החדש של שכנותיה. - ב-
result: חובה לסיים ב-; yourselfכיaddLast: rowמחזירה אתrowולא את ה-OrderedCollection! - ב-
nextSolution: השימוש ב-and: [...]העצל מבטיח שאםself advanceהחזירfalse(אין יותר פתרונות), החיפוש ייעצר מיד.
15.5 ★ הגדרת מלכה מנוונת: המחלקה QueenTerminator (תבנית Null Object)
במקום לסרבל את המימוש בתנאי עצירה (בדיקות neighbor isNil), מגדירים את QueenTerminator המשמשת כשכנה של המלכה הראשונה (בעמודה 1) ובעלת אותו פרוטוקול:
Object subclass: #QueenTerminator
instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'OOD-8Queens'
QueenTerminator >> canAttack: testRow x: testColumn
^ false "תמיד מחזיר false (אינה מאיימת על אף משבצת)"
QueenTerminator >> firstSolution
^ true "תמיד מחזיר true"
QueenTerminator >> nextSolution
^ false "תמיד מחזיר false (עוצר את החיפוש כשמלכה 1 מיצתה את שורה 8)"
QueenTerminator >> result
^ OrderedCollection new "מחזיר רשימה ריקה חדשה"
// המתודות setColumn:neighbour:, advance ו-moveToNextNonAttackedRow אינן בשימוש במלכה המנוונת!
16.1–16.2 ★ הכללה ללוח N×N (מלכודת משתנה המחלקה N!) וללוח תלת-ממדי N×N×N
הכללה ל-$N$ מלכות על לוח $N \times N$: הצב $N$ מלכות על לוח שחמט $N \times N$ כך שאף מלכה אינה מאיימת על מלכה אחרת. בפתרון מונחה העצמים הקבוע 8 מופיע ב-2 מקומות בלבד: בלולאת 1 to: 8 ב-Board>>initialize ובבדיקה row = 8 ב-Queen>>advance.
🚨 שאלת הבחינה הקלאסית: מלכודת משתנה המחלקה N בהכללת הלוח ל-$N \times N$!
אם נגדיר ב-Board משתנה מחלקה משותף classVariableNames: 'N' ומתודת מחלקה Board class>>N ^ N, וב-Queen>>advance נבדוק row = Board N, מה יקרה בהרצת הקוד הבא?
board1 := Board new.
board2 := Board new.
board1 initialize: 5. "קובע N := 5 ויוצר ב-board1 שרשרת של 5 מלכות"
board2 initialize: 1. "דורס את משתנה המחלקה המשותף וקובע N := 1!"
board1 solve. "ידפיס: No Solution. !"
הסבר: משתנה מחלקה N משותף לכל מופעי Board! הקריאה board2 initialize: 1 דרסה את N ל-1, ולכן כשמריצים board1 solve, 5 המלכות של board1 בודקות row = Board N מול 1, לא מתקדמות מעבר לשורה 1, ואי אפשר להציב 5 מלכות בשורה אחת בלבד!
הפתרון התקני: לשמור את גודל הלוח כמשתנה מופע פרטי (boardSize) ב-Board ובכל Queen!
🧊 חידת המלכות המרחבית – לוח שחמט תלת-ממדי $N \times N \times N$:
הגדרת הבעיה התלת-ממדית: הצב $N^2$ מלכות על לוח שחמט תלת-ממדי $N \times N \times N$ כך שבכל אחד מהמישורים אף מלכה אינה מאיימת על מלכה אחרת. עבור שתי מלכות $Q_1(i, j, k)$ ו-$Q_2(\text{row}, \text{col}, \text{lev})$, הקוד ב-Smalltalk המזהה איום מרחבי:
dx := (row - i) abs.
dy := (col - j) abs.
dz := (lev - k) abs.
((dx = 0) and: [ dy = 0 ]) ifTrue: [ ^ true ]. "1. איום לאורך ציר המפלסים (אותה שורה ואותה עמודה)"
((dx = 0) and: [ dy = dz ]) ifTrue: [ ^ true ]. "2. איום אלכסוני במישור המקביל לצירים y-z (אותה שורה)"
((dx = dz) and: [ dy = 0 ]) ifTrue: [ ^ true ]. "3. איום אלכסוני במישור המקביל לצירים x-z (אותה עמודה)"
((dx = dy) and: [ dx = dz ]) ifTrue: [ ^ true ] "4. איום באלכסון מרחבי תלת-ממדי או במישור x-y (כאשר dz=0)"
* מימוש פתרון לחידה התלת-ממדית ב-Smalltalk: מכיוון שיש להציב $N^2$ מלכות על לוח $N \times N \times N$, מקצים לכל מלכה זוג קבוע של (row, column) (סה"כ $N^2$ מלכות בשרשרת מקושרת מהמלכה $(N,N)$ ועד QueenTerminator), והמלכות מנהלות משא ומתן על בחירת המפלס lev ($1 \dots N$) תוך בדיקת אי-איום באמצעות 4 תנאי האיום המרחביים שהוגדרו לעיל!
17.1–17.3 הסביבה הגרפית Morphic: ישירות, חיות, מערכת הצירים והרכבת צורות
-
שני עקרונות היסוד של סביבת Morphic:
1. ישירוּת (`directness`): הצורות על המסך הן עצמים שאפשר לבחון ולשנותם בצורה ישירה. הדגמת ניתוק פריט מתפריט העולם: לוחצים בכפתור הכחול (האמצעי) מעל תפריט העולם לקבלת ה-morphic haloשל התפריט $\to$ לוחצים שוב בכפתור הכחול מעל אחד מפריטי התפריט להקפתו ב-Halo משלו $\to$ גוררים אותו החוצה באמצעות צלמית ה-pickup(באמצע השורה העליונה), והפריט מתפקד ככפתור עצמאי!
2. חיוּת (`liveness`): הסביבה מגיבה אל המשתמש והמידע על המסך מתעדכן בהתאם בזמן אמת. -
הפיכת עצמים וצבעים ל-Morph וחלון ה-Viewer:
•s := 'Hello' asMorph openInWorld. s openViewerForArgument.פותח בצד ימין את חלון ה-Viewer המאפשר להזיז ולסובב את העצם בלחיצה על סימני הקריאה!(ולגרום לו "לצחוק" באמצעות שליחת הפקודה הקולית ב-Viewer!).
•Color>>asMorphממומשת עם Cascade ו-yourself:^ (RectangleMorph new) fillStyle: self; borderWidth: 0; yourself. -
מערכת הצירים, מתודות בסיסיות ובניית צורות מורכבות:
ראשית הצירים
(0@0)נמצאת בפינה השמאלית העליונה של המסך; צירxמתקדם ימינה וצירyמתקדם כלפי מטה. מתודות:position/position:(פינה שמאלית-עליונה של המלבן החוסם),extent/extent:(רוחב@גובה),color:.// יצירת צורה צהובה וצורה ירוקה מסוג Morph: mYellow := Morph new color: Color yellow; openInWorld. mGreen := Morph new color: Color green; openInWorld. 10 timesRepeat: [mYellow position: mYellow position + (10@0)]. "הזזה ימינה 10 פיקסלים 10 פעמים" 10 timesRepeat: [mGreen position: mGreen position + (10@10)]. "הזזה באלכסון למטה וימינה 10 פעמים" mGreen extent: mYellow extent. "השוואת גודל הירוקה לצהובה" mYellow color: Color blue. "שינוי צבע הצהובה לכחול" -
הרכבת צורות (`Composition`):
• בקוד:rect addMorph: starמכניסה את הכוכב לתוך המלבן; הזזת המלבן (rect position: ...) מזיזה את שניהם יחדיו, והזזת הכוכב ישירות מזיזה אותו ביחס למלבן אך הוא ממשיך לזוז עם המלבן!star center: rect centerממרכזת אותו (בפרוטוקולgeometry), ו-rect removeMorph: starמנתקת אותו (נעלם מהמסך אך קיים בזיכרון ומוצג שוב עםstar openInWorld).
• בממשק החזותי: מתפריט העולםobjects (o) → Graphicsגוררים שתי צורות, מניחים זו על זו, לוחצים בכפתור הצהוב (ימני) על העליונה ובוחריםembed into. לניתוק: שתי לחיצות בכפתור הכחול (הראשונה מעלה Halo למכילה, השנייה למוכלת) וגרירתה החוצה.
17.4 ★ יצירת צורות גרפיות: CrossMorph, drawOn:, containsPoint: ושני השכתובים
כדי ליצור את המחלקה CrossMorph (היורשת מ-Morph), משכתבים את drawOn: aCanvas (הנעזרת ב-self bounds insetBy: להסטת פינות המלבן החוסם פנימה) ואת containsPoint: aPoint (כדי שרק נקודות על הצלב עצמו יגיבו לעכבר ולא 4 הפינות הריקות של המלבן החוסם):
CrossMorph >> drawOn: aCanvas
| crossHeight crossWidth horizontalBar verticalBar |
crossHeight := self height / 3.0.
crossWidth := self width / 3.0.
horizontalBar := self bounds insetBy: 0 @ crossHeight.
verticalBar := self bounds insetBy: crossWidth @ 0.
aCanvas fillRectangle: horizontalBar color: self color.
aCanvas fillRectangle: verticalBar color: self color
CrossMorph >> containsPoint: aPoint
| crossHeight crossWidth horizontalBar verticalBar |
crossHeight := self height / 3.0.
crossWidth := self width / 3.0.
horizontalBar := self bounds insetBy: 0 @ crossHeight.
verticalBar := self bounds insetBy: crossWidth @ 0.
^ (horizontalBar containsPoint: aPoint) or: [verticalBar containsPoint: aPoint]
שני עקרונות תכנון וארגון קוד ב-CrossMorph: (1) ריכוז חישובי horizontalBar ו-verticalBar במתודות עזר פרטיות למניעת כפילות הקוד בין drawOn: ל-containsPoint:; (2) פיצול המלבן האנכי לחלק עליון וחלק תחתון (מעל ומתחת ל-horizontalBar) כך שהריבוע המרכזי המשותף יצויר פעם אחת בלבד (חשוב במיוחד כשמציירים בצבע שקוף למחצה!).
17.5 ★ אירועי עכבר (2 שלבים), מקלדת (3 שלבים) ואנימציית Stepping
handlesMouseDown: (ללחיצה/שחרור/תזוזה), handlesMouseOver: (לכניסה/יציאה) או handlesMouseOverDragging: כך שיחזירו ^ true.• שלב 2 (תגובה + `self changed`):
// תגובה ללחיצה (הקטנה בלחיצה על הכפתור הצהוב):
CrossMorph >> mouseDown: anEvent
anEvent redButtonPressed
ifTrue: [self extent: (self extent * 1.1)].
anEvent yellowButtonPressed
ifTrue: [self extent: (self extent * 0.9)].
self changed "חובה כדי לתזמן קריאה ל-drawOn:!"
// תגובה לתנועת סמן (אדום בכניסת עכבר, ירוק ביציאה):
CrossMorph >> handlesMouseOver: anEvent ^ true
CrossMorph >> mouseEnter: anEvent self color: Color red
CrossMorph >> mouseLeave: anEvent self color: Color green
mouseEnter: anEvent מעבירים פוקוס: anEvent hand newKeyboardFocus: self (ובו בזמן יש לדרוס handlesMouseOver: ^ true).• שלב 2: ב-
handleKeystroke: anEvent בודקים את anEvent keyValue:
30 (חץ למעלה: -(0@1)), 31 (חץ למטה: +(0@1)), 29 (חץ ימינה: +(1@0)), 28 (חץ שמאלה: -(1@0)).• שלב 3: ב-
mouseLeave: anEvent משחררים פוקוס: anEvent hand newKeyboardFocus: nil.• אנימציות (`stepping and presenter`):
stepTime (מספר המילישניות בין צעדים), step (מבצעת את השינוי בצורה בכל פעימה), startStepping, stopStepping ו-isStepping.
17.6–17.10 ★ דו-שיח, Drag & Drop, DieMorph ו-Canvas/fork
-
תיבת דו-שיח (`FillInTheBlank`) ותפריט קופץ (`PopUpMenu`):
•FillInTheBlank request: 'enter your name:' initialAnswer: '??????'$\implies$ מחזירה את המחרוזת שהוקלדה (או'??????'אם אושר ללא שינוי, או''ב-Cancel).
•menu := PopUpMenu labelArray: #('x' 'y' 'z') lines: #(1 2 3). menu startUpWithCaption: 'Choose an item'$\implies$lines:מציין אחרי אילו אינדקסים יופיע קו מפריד, ו-startUpWithCaption:מחזירה את אינדקס הפריט שנבחר (1..3, או0אם לא נבחר דבר). -
גרירה והשמטה (`Drag and Drop`) ומנגנון תיאום בין צורות:
• במקבל (`ReceiverMorph`):wantsDroppedMorph: aMorph event: anEvent(האם לקבל את העצם הנגרר) ו-repelsMorph: aMorph event: ev(אם מחזירהtrue, עצם שנדחה חוזר למקומו המקורי במקום להישאר מונח בנקודת העזיבה!).
• בנגרר (`DroppedMorph`):wantsToBeDroppedInto: aMorphו-rejectDropMorphEvent: anEvent(מגדירה את התנהגות הנגרר במקרה של דחייה).// מנגנון Drag & Drop (ReceiverMorph מקבל רק צבע שונה משלו ומחזיר דחויים; DroppedMorph משנה צבע בדחייה): ReceiverMorph >> wantsDroppedMorph: aMorph event: anEvent ^ self color ~= aMorph color ReceiverMorph >> repelsMorph: aMorph event: ev ^ (self wantsDroppedMorph: aMorph event: ev) not DroppedMorph >> rejectDropMorphEvent: anEvent self color: Color random. "משנה את צבעו כך שבגרירה הבאה יתקבל!" super rejectDropMorphEvent: anEvent
🎲 קוביית המשחק `DieMorph` (`perform:`) ומחלקת `Canvas`
BorderedMorph subclass: #DieMorph
instanceVariableNames: 'faces dieValue isStopped'
classVariableNames: '' poolDictionaries: '' category: 'SBE-Morphic'
DieMorph class >> faces: aNumber ^ self new faces: aNumber
DieMorph >> initialize
super initialize. self extent: 50@50. self color: Color green.
dieValue := 1. faces := 6. isStopped := false
DieMorph >> faces: aNumber
(aNumber isInteger and: [aNumber > 0] and: [aNumber <= 9]) ifTrue: [faces := aNumber]
DieMorph >> face1 ^ {0.5@0.5}
DieMorph >> face2 ^ {0.25@0.25 . 0.75@0.75}
DieMorph >> face3 ^ {0.25@0.25 . 0.75@0.75 . 0.5@0.5} "... עד face9 (קואורדינטות יחסיות 0..1)"
DieMorph >> drawOn: aCanvas
super drawOn: aCanvas.
(self perform: ('face', dieValue asString) asSymbol)
do: [:aPoint | self drawDotOn: aCanvas at: aPoint]
DieMorph >> drawDotOn: aCanvas at: aPoint
aCanvas fillOval: (Rectangle center: self position + (self extent * aPoint) extent: self extent / 6) color: Color black
DieMorph >> dieValue: aNumber
(aNumber isInteger and: [aNumber > 0] and: [aNumber <= faces])
ifTrue: [dieValue := aNumber. self changed]
DieMorph >> stepTime ^ 100
DieMorph >> step isStopped ifFalse: [self dieValue: (1 to: faces) atRandom]
DieMorph >> handlesMouseDown: anEvent ^ true
DieMorph >> mouseDown: anEvent anEvent redButtonPressed ifTrue: [isStopped := isStopped not]
• שימוש ב-Canvas, תמונות Form ותהליכי fork: המחלקה Canvas מספקת מתודות לציור צורות, טקסט ותמונות (כולל שקיפות ב-AlphaBlendingCanvas, רוטציה ושינוי קנה-מידה). לטעינת תמונה מקובץ והקטנתה: f := (Form fromFileNamed: 'myPic.bmp') magnifyBy: 0.5. f asMorph openInWorld. להרצת אנימציה בתהליך רקע עצמאי (Thread): שולחים לבלוק את ההודעה fork (למשל [100 timesRepeat: [(Delay forMilliseconds: 20) wait. m position: m position + (2@1)]] fork).
חלק VII: מטה-מחלקות – המושג "מטה-מחלקה" (פרק 18)
שרשרת true class..., סיווג למטה-רמות, אבולוציית 5 הרמות, 10 חוקי המודל ב-Squeak, איתור מתודת new והמעגל הסגור של Metaclass.
18.1–18.7 ★ ייצוג מחלקה כעצם, שרשרת class, סיווג למטה-רמות ו-5 רמות עצמים
מכיוון שב-Smalltalk כל דבר הוא עצם וכל עצם הוא מופע של מחלקה, גם מחלקה היא עצם — והיא מופע של מטה-מחלקה (`Metaclass`): מחלקה שהמופעים שלה הם מחלקות!
- • שלב 1: true class →
True - • שלב 2: true class class →
True class - • שלב 3: true class class class → Metaclass
- • שלב 4: true class class class class → Metaclass class
- • שלב 5 (5 פעמים class): → Metaclass
- • שלב 6 (6 פעמים class): → Metaclass class
- • שלב 7 (7 פעמים class): → Metaclass
Play) והרומנים ("1984", "אוליבר טוויסט", "מלחמה ושלום" $\in$ המחלקה Novel), מבחינים בשלוש שכבות מהותיות של ישויות במערכת:
1. מופעים סופיים (`Terminal Instances` / `Non-instantiable objects`): עצמים פשוטים שאינם יכולים לייצר עצמים נוספים (כגון "אוליבר טוויסט").
2. מחלקות (`Classes` / `Instantiable objects`): עצמים המייצרים מופעים פשוטים (כגון
Novel ו-Play).
3. מטה-מחלקות (`Metaclasses` / `Instances are classes`): עצמים המייצרים מחלקות (כגון המחלקה
Class).
| מספר הרמות במערכת | שפות מייצגות | מאפייני הארכיטקטורה והמגבלה שהובילה לרמה הבאה |
|---|---|---|
| רמה 1: מערכת של עצמים בלבד | Self | מערכת "שטוחה" ללא מחלקות כלל. כל עצם מתאר את עצמו (טבלה המתאימה שמות לערכים) ומופעים חדשים נוצרים באמצעות שכפול (Cloning) של עצם קיים (אב-טיפוס `Novel Prototype`). |
| רמה 2: מערכת עצמים בת 2 רמות | Eiffel | יש עצמים ויש מחלקות המתארות אותם, אך אין מטה-מחלקות: המחלקות אינן מיוצגות בזמן ריצה, אינן עצמים ואינן מגיבות להודעות. |
| רמה 3: מערכת עצמים בת 3 רמות | Smalltalk-76, Little Smalltalk | הדרישה המינימלית למערכת סמולטוק: גם העצמים וגם המחלקות הם עצמים, ויש רק מטה-מחלקה אחת בלבד (היורשת מ"עצם" והיא מופע של עצמה). המגבלה: לכל המחלקות יש מבנה והתנהגות זהים (למשל מימוש משותף של new) ואי אפשר להגדיר מתודות-מחלקה או משתני-מחלקה פרטניים לכל מחלקה! |
| רמה 4: מערכת עצמים בת 4 רמות | LOOPS, ObjVlisp (הרחבות Lisp) | מאפשרת לכל מחלקה להגדיר מטה-מחלקה פרטית משלה (כגון Meta Novel עם n_novels, או לשתף מטה-מחלקה), וכל המטה-מחלקות הן מופעים של "המטה-מחלקה הגנרית" היחידה (שהיא מופע של עצמה). |
| רמה 5: מערכת בת 5 רמות – Explicit Metaclass | Squeak, Smalltalk-80 | לכל מחלקה (לרבות Metaclass עצמה!) יש מטה-מחלקה שמתארת רק אותה; כל המטה-מחלקות (לרבות Metaclass class) הן מופעים של Metaclass, ונוצר גרף ירושה של מטה-מחלקות החופף את גרף הירושה של המחלקות הרגילות! |
18.8–18.11 ★ 10 חוקי המודל ב-Squeak, היררכיה מקבילה ואיתור blue ו-new
📜 10 החוקים של מודל העצמים והמטה-מחלקות ב-Squeak (סעיפים 18.8–18.14):
2. כל עצם הוא מופע של מחלקה.
3. לכל מחלקה יש מחלקת-על.
4. הכל קורה באמצעות שליחת הודעות.
5. איתור מתודות נעשה במעלה היררכיית הירושה (למשל שליחת
class ל-translucentBlue מחפשת ב-TranslucentColor → Color → Object).
Color class).7. היררכיית מטה-מחלקות מקבילה להיררכיית המחלקות!
8. כל מטה-מחלקה יורשת מהמחלקה
Class ומהמחלקה Behavior.9. כל מטה-מחלקה היא מופע של המחלקה
Metaclass.10. המטה-מחלקה של
Metaclass (Metaclass class) היא מופע של Metaclass!
-
הודעות תשאול למחלקה כעצם ובחינה ב-Inspector:
• הודעות למחלקה:subclasses(מערך תתי-מחלקות),allSuperclasses(אוסף כל מחלקות-העל),instVarNames(מערך משתני המופע של המחלקה),allInstVarNames(מערך כל משתני המופע כולל אלו שנורשו ממחלקות-העל).
• מעקב שרשרת superclass של מטה-מחלקה: מה מחזיר הביטויTranslucentColor class superclass superclass? הוא מחזיר אתObject class(המטה-מחלקה שלObject).
• שליחת מתודת מחלקה בירושה (TranslucentColor blue): כששולחיםTranslucentColor blue, החיפוש מתחיל ב-TranslucentColor class, עולה ל-Color class, מוצא שם אתblueומחזיר צבע כחול אטום רגיל (Color blue). מה אפשר לעשות כדי ש-TranslucentColor blueתחזיר צבע כחול שקוף במקום כחול רגיל? להגדיר מחדש (Override) את המתודהblueבמטה-מחלקהTranslucentColor class(למשל^ super blue alpha: 0.5)!
• כותרת חלון ה-Inspector: בכותרת ה-Inspector רשום תמיד שם המחלקה של העצם הנבחן (ולא שם העצם): כשבוחנים אתColor blueהכותרת היאColor, וכשבוחנים את המחלקהColorהכותרת היאColor class!
🏆 שאלת הבחינה הקלאסית: מסלול איתור המתודה TranslucentColor new:
- המקבל (
self): עצם המחלקהTranslucentColor. - התחלת החיפוש במטה-מחלקה של המקבל: החיפוש מתחיל ב-
TranslucentColor class(ולא במחלקת-העלColor!). - טיפוס במעלה היררכיית המטה-מחלקות:
TranslucentColor class → Color class → Object class → Class → ClassDescription → Behavior. - מציאת
newב-Behavior: המתודהnewממומשת ב-Behavior, ומכיוון שהיא יוצרת ומחזירה מופע שלself(שהוא המקבל המקוריTranslucentColor), העצם המוחזר הוא מופע שלTranslucentColor(ולא שלBehavior)!
18.12–18.15 ★ מחלקות הליבה (Behavior, ClassDescription, Class, Metaclass), נוסחאות ודבר סוף
Behavior (יורשת מ-Object)
יוצרת את המינימום הנדרש להידור מתודות וליצירת מופעים ולהרצתם (שומרת מצביע ל-superclass, מילון מתודות, פורמט מופעים, ומתודות היצירה new, basicNew, new:, basicNew:).
ClassDescription (יורשת מ-Behavior)
מחלקה מופשטת המוסיפה פעולות כגון הגדרת פרוטוקולים, שמות למשתני מופע, תיעוד ומעקב אחרי שינויים. ממנה יורשות שתי אחיות: Class ו-Metaclass!
Class (יורשת מ-ClassDescription)
יוצרת את ההתנהגות המשותפת לכל המחלקות הרגילות: מטפלת בשם המחלקה, משתני מחלקה, מאגרים משותפים ויצירת תתי-מחלקות. ממנה יורשת Object class (וב-Squeak דרך ProtoObject class)!
Metaclass (יורשת מ-ClassDescription)
מגדירה את התנהגותן המשותפת של מטה-מחלקות (יצירה ואתחול של מופע יחיד שהוא עצם המחלקה, אתחול משתני מחלקה וכו'). כל המטה-מחלקות (כולל Metaclass class!) הן מופעים של Metaclass!
🎓 נוסחאות זהב לפיצוח שאלות שרשור class ו-superclass במבחן (סעיפים 18.14–18.15):
- • C superclass class == C class superclass →
true(עבור כל מחלקה C שאינה שורש העץ, לפי חוק 7 של ההיררכיה המקבילה!) - • Object superclass →
ProtoObject(אוnilבשורש) | Object class superclass →Class(דרךProtoObject class) - • Metaclass superclass →
ClassDescription| Class superclass →ClassDescription - • Metaclass class →
Metaclass class| Metaclass class class →Metaclass(חוק 10: קשר מופע מעגלי!) - • Metaclass class superclass →
ClassDescription class(לפי חוק 7, כי מחלקת-העל של Metaclass היא ClassDescription!)
* דבר סיום והשפעות בעולם התוכנה: שפת Objective-C (ששימשה את חברת Apple למערכות OS X ו-iOS) היא שפת תכנות מונחית עצמים המוסיפה לשפת C הודעות בסגנון שפת Smalltalk!
בהצלחה רבה במבחן בתכנות מונחה עצמים (20906)! 🎓🚀
הסיכום מקיף, מעמיק ונאמן לחומר הלימוד בכל 18 הפרקים של הקורס.