מהו בעצם "עצם"? מרחב הבעיה (Problem Space) מול מרחב הפתרון (Program Space)
הגדרת המושג הבסיסי והשורשי ביותר בפרדיגמה, השאלה מה חשוב יותר – "עצם" או "מחלקה", והאבחנה בין עצמים מהעולם האמיתי לבין עצמים טכניים שנולדו למימוש הפתרון התוכנתי.
מודל מנטלי ועיקרון מנחה: הבמה (מרחב הבעיה) ומאחורי הקלעים (מרחב הפתרון)
תחשוב על הצגת תיאטרון: על הבמה נמצאים השחקנים והאביזרים של הסיפור עצמו (עובד, שעון נוכחות, לקוח, מכונית) — זהו מרחב הבעיה. מאחורי הקלעים פועלים מנופים, כבלים ומערכות תאורה שמאפשרים להצגה לרוץ (קובץ, מחסנית, עץ חיפוש) — זהו מרחב הפתרון/התוכנית!
המושג "עצם" (Object) הוא מנגנון ההפשטה הבסיסי והטבעי ביותר בפרדיגמה (והחישוב מבוצע על ידו), אך בארכיטקטורה של התוכנית המושג "מחלקה" (Class) נחשב למנגנון ההפשטה המרכזי, המורכב והחשוב יותר!
ישנם עצמים שיכולים להשתייך לשני המרחבים בהתאם להקשר! רשימה (List) של לקוחות או משימות שייכת למרחב הבעיה, בעוד שרשימה מקושרת גנרית לאחסון מצביעי זיכרון שייכת למרחב הפתרון.
1. מבוא: מהו בעצם "עצם", ומה חשוב יותר – "עצם" או "מחלקה"?
הבנת מושג היסוד שעל שמו נקראת פרדיגמת התכנות מונחה העצמים
בפרקים הקודמים ראינו כיצד התפתחו מנגנוני ההפשטה לאורך ההיסטוריה עד שהגענו לתכנות מונחה עצמים. עכשיו אנחנו פותחים את חלק II (עצמים) וצוללים אל לב המושג שעל שמו נקראת הפרדיגמה כולה: המושג "עצם" (Object).
המושג "עצם" הוא מנגנון ההפשטה הבסיסי והשורשי ביותר בתכנות מונחה עצמים. אם ננסה להגדיר עצם באופן אינטואיטיבי ראשוני, נאמר שעצם הוא ישות עצמאית המייצגת דבר-מה מוחשי או מופשט, המחזיקה מידע פנימי משלה ומסוגלת לבצע פעולות ולהגיב להודעות.
💡
פינת חשיבה ותרגול: איזה מושג חשוב יותר בתכנות מונחה עצמים – "עצם" (Object) או "מחלקה" (Class)? (לחץ לחשיפת התשובה שלי)
▼
התשובה המפתיעה והמדויקת: אף על פי שהמושג "עצם" (Object) הוא המושג הטבעי והבסיסי יותר — שכן הפרדיגמה כולה נקראת על שמו ("תכנות מונחה עצמים") וכל החישוב בזמן ריצה מבוצע בפועל על ידי עצמים — הלכה למעשה המושג "מחלקה" (Class) נחשב לחשוב יותר בארכיטקטורה של התוכנית, והוא מנגנון ההפשטה המרכזי והמורכב יותר שאנו כמתכנתים מעצבים וכותבים!
2. סעיף 3.1: סוגי עצמים – מרחב הבעיה מול מרחב התוכנית/הפתרון
חלוקת העצמים הקיימים בתוכנה לפי מרחב הקיום והייעוד שלהם
כאשר אנחנו כותבים תוכנה מונחית עצמים, העצמים שאנו יוצרים אינם עשויים מקשה אחת, אלא נחלקים לשני סוגים מהותיים בהתאם למרחב הקיומי ולייעוד שלהם:
עצמים המייצגים ישויות מעולם הבעיה האמיתי
עצמים אלו מייצגים ישויות הלקוחות ישירות מתוך העולם האמיתי או מעולם התוכן של הבעיה שהתוכנית מנסה למדל או לפתור.
• שעון נוכחות (Attendance Clock)
• פריט במלאי (Inventory Item), חשבון בנק (Bank Account)
• כלי רכב ספציפי / מכונית, מספר מרוכב ($e+2i$)
עצמים פנימיים וטכניים למימוש הפתרון
עצמים וכלים טכניים שהמתכנת יוצר מאחורי הקלעים כדי לפתור את הבעיה באופן אלגוריתמי או מבני, ואינם קיימים בהכרח בעולם הבעיה המקורי.
• רשימה מקושרת (Linked List), מערך (Array)
• מחסנית (Stack), עץ חיפוש (Search Tree)
• מנפה שגיאות (Debugger)
🤔
פינת חשיבה ותרגול: לאיזה מרחב שייכים "כלי רכב", "מספר מרוכב" ו"רשימה" (List)? (לחץ לפתרון)
▼
1. כלי רכב ספציפי או מספר מרוכב: שייכים ככלל למרחב הבעיה (Problem Space), משום שהם מייצגים ישויות מעולם התוכן (עולם התחבורה או עולם המתמטיקה) שהתוכנה ממדלת.
2. רשימה (List) – הדואליות המרתקת: רשימה היא דוגמה מופתית לעצם שיכול להשתייך לשני המרחבים בהתאם להקשר שבו הוא מופיע!
• אם מדובר ברשימת לקוחות של חנות, רשימת משימות או רשימת קניות מוגדרת — היא שייכת ישירות למרחב הבעיה.
• לעומת זאת, אם מדובר ברשימה מקושרת גנרית המשמשת למעקב ואחסון מצביעי זיכרון פנימיים — היא שייכת למרחב הפתרון/התוכנית!
מרחב הבעיה (Problem Space) מול מרחב הפתרון/התוכנית (Program Space)
מיני-סימולציה 3.1: בוחן ההקשר – איך הקשר התוכנית קובע את המרחב של העצם?
מעבדת מיון העצמים (Drag & Drop + לחיצה מהירה): מרחב הבעיה מול מרחב הפתרון
גרור כל עצם אל העמודה המתאימה (או לחץ על כפתורי השיוך שעל הכרטיסייה) כדי למיין את כל 10 העצמים בין מרחב הבעיה למרחב הפתרון/התוכנית:
מוינו נכון: 0 / 10הגשר הנרטיבי: הבנו אילו סוגי עצמים קיימים בתוכנית – אבל ממה מורכב כל עצם מבפנים?
בין אם העצם שלנו שייך למרחב הבעיה (כמו כלי רכב או עובד) ובין אם הוא שייך למרחב הפתרון (כמו מחסנית או קובץ), כל עצם באשר הוא עומד על שלושה עמודי תווך אוניברסליים: זהות (Identity), מצב (State) והתנהגות (Behavior). בלשונית הבאה נכיר את שלושת הממדים האלה לעומק דרך איור הפטיש המפורסם ודרך ארבע דוגמאות מוחשיות!
שלושת מאפייני העצם: זהות (Identity), מצב (State) והתנהגות (Behavior)
ניתוח שלושת עמודי התווך המגדירים כל עצם כישות עצמאית, והמחשתם המלאה באמצעות הפטיש על הכן, המספר המרוכב $e+2i$, הרשימה `(13, 'hello', true)` וכלי הרכב בעל לוחית הרישוי `19-070-35` (`35-070-19`).
מודל מנטלי ועיקרון מנחה: משולש הקיום של כל עצם – מי אני, מה יש בי, ומה אני יודע לעשות
תחשוב על פטיש בחנות כלי עבודה: היותו בעל ראש פלדה וידית עץ ו"מוצג למכירה" הוא המצב שלו; פעולת "נעיצת מסמרים" היא ההתנהגות שלו; והעובדה שזהו הפטיש הפיזי הספציפי הזה שבחרנו מתוך עשרות פטישים זהים על המדף היא הזהות שלו!
הזהות (Identity) היא קבועה, ייחודית לעולם ואינה משתנה (לרוב מיוצגת ע"י כתובת הזיכרון); המצב (State) הוא המאפיין הסטטי (התכונות ברגע נתון) שיכול להשתנות; וההתנהגות (Behavior) היא המאפיין הדינמי בתגובה להודעות.
שני עצמים יכולים להיות בעלי מצב זהה לחלוטין ולהתנהג באותו אופן בדיוק, ועדיין להיות שני עצמים נפרדים בזכות הזהות השונה שלהם!
1. סעיף 3.2: שלושת עמודי התווך של כל עצם
זהות (Identity), מצב (State – המאפיין הסטטי) והתנהגות (Behavior – המאפיין הדינמי)
כל עצם באשר הוא מוגדר כישות העומדת בפני עצמה באמצעות שלושה ממדי יסוד בלתי נפרדים:
מה שמבדיל את העצם מכל עצם אחר
תכונה מהותית החותכת ומבדילה עצם ספציפי מכל שאר העצמים בעולם. באמצעות הזהות ניתן לקבוע האם שני שמות/משתנים בתוכנית מצביעים למעשה על אותו העצם בדיוק. זהותו של עצם היא יחידה לעולם ואינה משתנה. במימושים רבים של שפות תכנות, כתובת הזיכרון הפיזית היא המשמשת כזהות העצם.
המאפיין הסטטי של העצם
אוסף כל התכונות והערכים הפנימיים שהעצם מכיל בנקודת זמן נתונה. זהו המאפיין הסטטי של העצם. בניגוד לזהות שאינה משתנה לעולם, המצב של העצם יכול להשתנות לאורך חייו.
המאפיין הדינמי של העצם
אוסף כל הפעולות שהעצם מסוגל לבצע בתגובה להודעות שהוא מקבל מלקוחותיו. זהו המאפיין הדינמי של העצם.
2. סעיף 3.3: ארבע דוגמאות מוחשיות להמחשת המודל
פטיש, המספר המרוכב $e+2i$, הרשימה `(13, 'hello', true)` וכלי הרכב `19-070-35` (`35-070-19`)
🔨 1. פטיש פיזי
- מצב (State): "מוצג למכירה", מאפייני הייצור והחומרים שלו (בעל ראש פלדה וידית עץ).
- התנהגות (Behavior): הכאה ופעולת נעיצת מסמרים.
- זהות (Identity): הפטיש הספציפי והייחודי הזה שמוצב על כַּן / שנבחר לקנייה מתוך מגוון פטישים זהים על המדף בחנות.
🔢 2. המספר המרוכב e + 2i
- מצב (State): הערכים של החלק הממשי (e), החלק המדומה/הדמיוני (2), והערך המוחלט.
- התנהגות (Behavior): פעולות אריתמטיות כמו חיבור, חיסור, כפל וחילוק.
- זהות (Identity): מופע המספר המסוים בזיכרון.
📋 3. רשימה (13, 'hello', true)
- מצב (State): אורכה של הרשימה והאלמנטים המוכלים בתוכה (13, 'hello', true).
- התנהגות (Behavior): הוספת איבר, מחיקת איבר, הדפסה, מעבר על איברים ומיון.
- זהות (Identity): מופע הרשימה הספציפי בזיכרון.
🚗 4. כלי רכב (לוחית רישוי 19-070-35 / 35-070-19)
- מצב (State): מהירות נוכחית, מיקום גיאוגרפי, צבע וכמות דלק.
- התנהגות (Behavior): נסיעה, האצה, האטה, עצירה ופנייה.
- זהות (Identity): הייחודיות של הרכב הספציפי המיוצגת ע"י מספר לוחית הרישוי הייחודית שלו.
רחף עם העכבר או לחץ על חלקי הפטיש כדי לחשוף את שלוש בועיות המידע (זהות, מצב, התנהגות)
מעבדת 4 העצמים החיים: שלח הודעות, הפעל התנהגות וראה כיצד המצב משתנה אך הזהות נשארת קבועה!
הגשר הנרטיבי: הבנו שזהות, מצב והתנהגות מגדירים עצם – אבל האם אפשר לבנות עצמים כאלה בשפה פרוצדורלית כמו C?
כדי להבין עד הסוף את המכניקה של עצמים מתחת לפני השטח, הדרך הטובה ביותר היא לשאול: האם אנחנו יכולים לדמות "עצם" (עם זהות, מצב והתנהגות) בשפה שאינה מונחית עצמים כמו שפת C?
בלשונית הבאה ננתח שלושה ניסיונות מרתקים ב-C — פונקציית מונה עם משתנה סטטי, מחסנית כמודול (stack.h ו-stack.c), ומצביע לקובץ (FILE *) — ונראה איפה נוצר "עצם מנוון" ואיפה מתקבל מודל שקרוב מאוד לעצם שלם!
האם בשפת C אפשר להגדיר עצמים? משני עצמים מנוונים ועד למצביע לקובץ (FILE*)
בחינת שלושה מקרי בוחן בשפה פרוצדורלית: פונקציית מונה עם משתנה סטטי (3.4.1), מחסנית כמודול בקובצי stack.h ו-stack.c (3.4.2), ומצביע לקובץ FILE* (3.4.3) כמודל הקרוב ביותר לעצם שלם.
מודל מנטלי ועיקרון מנחה: מתי ישות ב-C היא "עצם מנוון" ומתי היא מתקרבת לעצם אמיתי?
פונקציית מונה ב-C היא כמו מכונה עם כפתור אחד בלבד שקיימת רק אחת כמוה בעולם (גם ההתנהגות וגם הזהות מנוונות). מודול מחסנית מוסיף כפתורים (push, pop), אך עדיין יש רק מכונה אחת כזו!
עצם שלם דורש גם התנהגות עשירה (ריבוי פעולות/הודעות) וגם זהות מלאה המאפשרת ריבוי מופעים עצמאיים. כשחסר אחד מאלה, אנו מקבלים "עצם מנוון".
טיפוס המצביע לקובץ (FILE *) ב-C קרוב מאוד לעצם שלם: הזהות היא המצביע (כתובת ייחודית לכל קובץ), המצב הוא תוכן הקובץ ודגליו, וההתנהגות היא הפונקציות הפועלות עליו (fopen, fgetc, fclose).
1. מקרי בוחן 3.4.1 ו-3.4.2: עצמים מנוונים בשפת C (פונקציית מונה ומודול מחסנית)
כיצד משתנים סטטיים (static) מאפשרים שמירת מצב בין קריאות, ואיפה בדיוק מתרחש הניוון?
כדי להבין באמת מה נותנת לנו שפה מונחית עצמים, בוא ננסה לאתגר את עצמנו: האם אנחנו יכולים ליצור ישויות שעונות להגדרת "עצם" (בעלות זהות, מצב והתנהגות) בשפה פרוצדורלית קלאסית כמו שפת C? התשובה היא שניתן לדמות עצמים ב-C, אך בשני הניסיונות הראשונים שלנו נקבל "עצם מנוון":
עצם מנוון בזהות ובהתנהגות
נתבונן בפונקציית מונה השומרת על ערכה בין קריאות באמצעות משתנה static:
- • מצב (State): נשמר במשתנה הסטטי
static int n = 0. - • זהות (Identity): מופע הפונקציה בזיכרון התוכנית.
- • התנהגות (Behavior): קידום המונה והחזרת הערך (
++n). - ❌ הפגם הכפול: ההתנהגות מנוונת להודעה אחת בלבד (קריאה לפונקציה), והזהות מנוונת כי קיים רק מופע אחד של המונה!
static int n = 0;
return ++n;
}
התנהגות עשירה, אך זהות מנוונת (Singleton)
נפריד בין קובץ מנשק (stack.h) לקובץ מימוש (stack.c) עם משתנים סטטיים גלובליים ברמת הקובץ:
- • מצב (State): נשמר במשתנים הסטטיים ברמת הקובץ (
stack,sp). - • התנהגות (Behavior): עשירה יותר! אוסף פונקציות מובחנות (
push,pop). - • זהות (Identity): מופע הקובץ המהודר בזיכרון.
- ⚠️ הפגם שנותר: הזהות עדיין מנוונת לחלוטין — קיים רק מופע אחד ויחיד של המחסנית במערכת!
extern void push(int val);
extern int pop(void);
#include "stack.h"
static int stack, sp;
void push(int val) { stack[sp++] = val; }
int pop(void) { return stack[--sp]; }
2. מקרה בוחן 3.4.3: מצביע לקובץ (FILE *) – המודל הקרוב ביותר לעצם ב-C!
כיצד מצביע ייחודי לכל קובץ שנפתח פותר את בעיית הזהות ומאפשר ריבוי מופעים עצמאיים
כעת נתבונן בישות הקרובה ביותר למושג ה"עצם" המלא בשפת C: טיפוס המצביע לקובץ FILE * מספריית הקלט/פלט הסטנדרטית (<stdio.h>):
main(int ac, char **av) {
FILE *fin;
for (++av; *av != (char *)0; av++) {
if ((fin = fopen(*av, "r")) == (FILE *)0)
continue;
while (!feof(fin))
putc(fgetc(fin));
}
return 0;
}
מדוע FILE * מתקרב כל כך לעצם אמיתי? משום שכל שלושת עמודי התווך מתקיימים בו באופן מלא וללא ניוון:
- זהות (Identity): המצביע עצמו (הכתובת הייחודית בזיכרון שמוחזרת מ-
fopen). מצביעים שונים לקבצים שונים מייצגים זהויות שונות לחלוטין! - מצב (State): תוכן הקובץ, מיקום המצביע הפנימי הנוכחי בתוכו, ודגלי שגיאה וסוף קובץ (
feof). - התנהגות (Behavior): אוסף הפעולות שניתן לבצע על המצביע — פתיחה (
fopen), קריאת תו (fgetc), בדיקת סוף קובץ (feof) או סגירת קובץ (fclose). - ריבוי מופעים: ניתן לפתוח מספר רב של קבצים במקביל, כשלכל אחד מהם זהות ומצב עצמאיים!
כיצד המצביע FILE * מתפקד כזהות, מצביע על גוש זיכרון (מצב) ומופעל ע"י פונקציות (התנהגות)
מיני-סימולציה 3.3: מאבחן שלושת המודלים בשפת C – איפה נמצא הניוון?
מעבדת הרצה ב-C: השווה בין counter(), מודול stack.c וריבוי מופעים של FILE *
counter()
static n = 0
הודעה אחת בלבד ועותק סטטי אחד בלבד:
stack.c
sp = 1 [10]
ריבוי פעולות (push/pop), אך מופע יחיד:
FILE *
2 קבצים פתוחים
f1(0x10): pos=0 | f2(0x20): pos=0
הגשר הנרטיבי: ראינו שהמצביע העניק ל-FILE* זהות ייחודית – אבל מה ההבדל העמוק בין "זהות" לבין "שם" ו"שוויון"?
הדוגמה של FILE * הראתה לנו שללא זהות עצמאית אי אפשר להבדיל בין מופעים שונים.
אבל כאן מסתתרת אחת המלכודות הכי חשובות במדעי המחשב: האם שם של משתנה הוא הזהות של העצם? איך עצם פונה אל עצמו בשפות שונות (self, this, Current)? וכשאנחנו שואלים "האם שני עצמים שווים?", האם אנחנו מתכוונים לכך שהם אותו עצם בדיוק בזיכרון (השוואת זהות ==) או לכך שיש להם תוכן זהה (השוואה מבנית =)? בוא נעבור ליחידה 3.5!
זהות עצמית (Identity) מול שם ומצב, והשוואת זהות (==) מול השוואה מבנית (=)
ההפרדה החדה בין שם העצם לזהותו (כינויים / Aliases ועצמים ללא שם), המזהה השמור לעצם הנוכחי (self, this, Current), ושני מנגנוני בדיקת השוויון מתחת לפני השטח.
מודל מנטלי ועיקרון מנחה: אדם אחד עם שני כינויים מול תאומים זהים!
אם לאותו אדם קוראים בבית "יוסי" ובצבא "יוסף", אלו שני שמות שונים המצביעים על אותה זהות בדיוק (השוואת זהות ==). לעומת זאת, שני תאומים שלובשים חולצה זהה הם שתי זהויות נפרדות בעלות מצב ומבנה זהים (השוואה מבנית =)!
שם העצם אינו זהותו! ייתכן עצם ללא שם כלל, או עצם יחיד בעל שמות רבים (כינויים / Aliases), והמצב שלו יכול להשתנות עם הזמן — אך הזהות של העצם אינה משתנה לעולם, ולא ייתכנו שני עצמים בעלי אותה זהות!
ב-Smalltalk השוואת זהות מתבצעת ב-== (במחלקת ProtoObject), והשוואה מבנית ב-= (הדורשת גם התאמה של מתודת ה-hash).
1. סעיף 3.5: זהות מול שם, זהות מול מצב, והמזהה השמור לעצם הנוכחי
איך עצם מתייחס לעצמו ב-Smalltalk (`self`), ב-Java/C++ (`this`) וב-Eiffel (`Current`)?
כדי לפתח מודל מנטלי חד של תכנות מונחה עצמים, אנחנו חייבים להפריד באופן מוחלט בין הזהות (Identity) של העצם לבין השם שלו והמצב שלו:
🏷️ זהות לעומת שם (Name vs. Identity)
שם העצם אינו זהותו! ייתכן שלעצם מסוים אין שם כלל (עצם אנונימי שנוצר בתוך ביטוי), ייתכנו שני עצמים בעלי אותו שם בהקשרים שונים, וייתכן עצם יחיד שיש לו מספר שמות שונים המצביעים עליו (כינויים / Aliases).
כלל בל יעבור: לא ייתכנו שני עצמים שונים (שתי זהויות נפרדות) בעלי אותה זהות!
🔄 זהות לעומת מצב (State vs. Identity)
המצב הפנימי של העצם (ערכי התכונות שלו) יכול להשתנות פעמים רבות לאורך מחזור חייו עם הזמן, אך הזהות של העצם אינה משתנה לעולם מרגע יצירתו ועד סוף חייו.
כדי לאפשר לעצם "לדעת" מי הוא ולהתייחס לעצמו מתוך הקוד שלו, כל שפה מונחית עצמים מקצה מזהה שמור המייצג את העצם הנוכחי:
2. סעיף 3.5.1: בדיקת שוויון – השוואת זהות מול השוואה מבנית
שני המנגנונים השונים לחלוטין המסתתרים מאחורי השאלה "האם שני עצמים שווים?"
האם שני השמות מצביעים לאותו העצם בדיוק?
שני שמות של עצמים שווים בהשוואת זהות אם ורק אם הם חולקים את אותה זהות פיזית (מצביעים בדיוק לאותה כתובת זיכרון).
• ב-Smalltalk בדיקה זו מתבצעת באמצעות האופרטור == (ובדיקת אי-זהות באמצעות ~~).
• מתודה זו ממומשת במחלקת השורש הבסיסית ProtoObject כפרימיטיב של השוואת כתובות.
האם לשני העצמים יש מבנה זהה ומצב (תוכן) זהה?
שני עצמים ייחשבו שווים בהשוואה מבנית אם ורק אם יש להם בדיוק אותו מבנה ואותו מצב פנימי (שדותיהם שווים בערכם), אפילו אם זהויותיהם נפרדות לחלוטין!
• ב-Smalltalk מתבצעת באמצעות האופרטור = (ואי-שוויון ב-~=).
• ברירת המחדל במחלקה Object מפנה אל ==, ולכן מחלקות הדורשות שוויון ערכי (כמו Point, String, Magnitude) דורסות את = ומתאימות עמה גם את דריסת המתודה hash!
כינוי לאותו עצם (p1 == p2) מול שני עצמים נפרדים בעלי תוכן זהה (p1 = p3 אך p1 ~~ p3)
מיני-סימולציה 3.4: סייר התייחסות עצמית ואופרטורי השוואה ב-Smalltalk
מעבדת הזיכרון החיה: בדוק את == מול = ושנה מצב של עצם כדי לראות איך הזהות נשארת קבועה!
במעבדה שלפניך קיימים שלושה משתנים ב-Smalltalk: a ו-b מצביעים שניהם לעצם בכתובת @0x100, ואילו c מצביע לעצם נפרד בכתובת @0x200. בצע פעולות וראה כיצד תוצאות == ו-= מגיבות בזמן אמת!
הגשר הנרטיבי: הבנו את ההבדל בין השוואת זהות להשוואה מבנית – ומה קורה כשאנחנו רוצים להעתיק עצם?
בדיוק כפי שבבדיקת שוויון גילינו הבחנה עמוקה בין זהות למבנה, כך בדיוק קורה גם כשאנחנו מבקשים להעתיק או לשכפל עצם!
האם אנחנו משכפלים רק את המצביע (העתקה זהותית)? ואם אנחנו יוצרים עצם חדש (העתקה מבנית) שמכיל בתוכו עצמים פנימיים, עד איזה עומק אנחנו משכפלים אותם — העתקה רדודה (shallowCopy), העתקה לעומק 2 (copyTwoLevel) או העתקה עמוקה (deepCopy)? בוא נעבור ליחידה 3.5.2 ונגלה!
האנטומיה המלאה של העתקה: מהעתקה זהותית ועד להעתקה רדודה, לעומק 2 ועמוקה
ההבחנה בין העתקה זהותית להעתקה מבנית, רמות העומק בשכפול עצם מורכב (shallowCopy, copyTwoLevel, deepCopy, ו-copy / postCopy ב-Squeak), וניתוח ההעתקה הרקורסיבית המותאמת של PList ו-Point ב-Java.
מודל מנטלי ועיקרון מנחה: לשכפל רק את התיקייה החיצונית או גם את המסמכים שבתוכה?
בהעתקה רדודה (Shallow Copy) אנו יוצרים קלסר חדש, אך שמים בו קישורים לאותם דפים בדיוק כמו בקלסר המקורי — אם נצייר על דף בקלסר החדש, גם הקלסר המקורי ישתנה! בהעתקה עמוקה (Deep Copy) אנו מצלמים מחדש את הקלסר ואת כל הדפים בכל הרמות.
בהעתקה זהותית משוכפל רק המצביע (נותרת זהות אחת בלבד). בהעתקה מבנית נוצר עצם חדש בעל זהות נפרדת, ורמת העומק קובעת אילו עצמים פנימיים ישוכפלו מבנית ואילו יועתקו זהותית.
עומק ההעתקה הרקורסיבית נקבע על פי האופן שבו מוגדרת פעולת ההעתקה עבור כל עצם פנימי! אם עצם פנימי הוא בלתי-משתנה (כמו Point עם שדות final), החזרת this היא בטוחה לחלוטין!
1. שתי אפשרויות היסוד להעתקה: העתקה זהותית מול העתקה מבנית
האם בסיום ההעתקה נותרה זהות אחת בזיכרון או שנוצרו שתי זהויות נפרדות?
בדיוק כפי שראינו בשוויון, גם פעולת ההעתקה של עצם מעמידה בפנינו שתי אפשרויות שונות מהותית לניהול הזיכרון ומבנה הנתונים:
1. העתקה זהותית (Identity Copy / Aliasing)
רק המצביע משוכפל (למשל השמה פשוטה b := a). בסיום הפעולה יש לנו שני שמות שונים המצביעים בדיוק על אותו עצם ממש, ולכן קיימת בזיכרון זהות אחת בלבד.
2. העתקה מבנית (Structural Copy)
מצב העצם עצמו מועתק אל תוך תבנית של עצם חדש, כך שבסיום הפעולה נוצרות בזיכרון שתי זהויות נפרדות ועצמאיות.
2. רמות העומק בהעתקה מבנית של עצם מורכב (גרף עצמים)
העתקה רדודה (`shallowCopy`), העתקה לעומק 2 (`copyTwoLevel`), העתקה עמוקה (`deepCopy`) והעתקה מותאמת (`copy` ו-`postCopy`)
כאשר העצם שלנו הוא עצם מורכב (עצם המכיל בתוכו מצביעים לעצמים פנימיים אחרים, כגון רשימה המכילה נקודות או תת-רשימות), ההעתקה המבנית מתחלקת למספר רמות עומק קריטיות:
רק עצם השורש (המעטפת החיצונית) מועתק מבנית לזהות חדשה, בעוד שכל העצמים הפנימיים המוכלים בו אינם משוכפלים אלא מועתקים בהעתקה זהותית (העצם החדש והישן מצביעים שניהם על אותם עצמים פנימיים בדיוק!).
• ב-Smalltalk ממומשת כ-shallowCopy המבוססת על פרימיטיבים (basicSize, basicAt).
• המשמעות: שינוי פנימי בתוך איבר של העותק משפיע ישירות גם על המקור!
שכפול מבני של עצם השורש, ובנוסף שכפול רדוד של כל אחד מהאיברים המוכלים בו ברמה הראשונה (ואילו איברים ברמה השנייה ומטה עדיין משותפים זהותית).
• ב-Smalltalk ממומשת באמצעות המתודה copyTwoLevel.
עץ העצמים מועתק מבנית באופן רקורסיבי מלא: גם השורש וגם כל העצמים הפנימיים שלו בכל הרמות (עד התחתית) מועתקים לזהויות חדשות לחלוטין, ליצירת עותק מבודד ועצמאי ב-100%.
• ב-Smalltalk ממומשת באמצעות המתודה deepCopy.
השיטה המומלצת ב-Squeak היא שליחת ההודעה copy, אשר מממשת self shallowCopy postCopy: תחילה מתבצעת העתקה רדודה של השורש, ומיד אחריה נקראת postCopy. מחלקות שבהן יש שדות פנימיים שאסור לשתף דורסות את postCopy כדי לשכפל ספציפית אותם!
💡
פינת חשיבה ותרגול: נתבונן במחלקות Point ו-PList ב-Java – האם copy() כאן מבצעת העתקה רדודה או עמוקה? (לחץ לניתוח הקוד והפתרון)
▼
Point ו-PList ב-Java – האם copy() כאן מבצעת העתקה רדודה או עמוקה? (לחץ לניתוח הקוד והפתרון)
public final int x, y;
public Point(int a, int b) { x = a; y = b; }
public Point copy() { return this; } // מחזירה את עצמה!
}
public class PList {
public Point head;
public PList tail;
public PList copy() {
PList res = new PList();
res.head = head.copy();
if (tail != null)
res.tail = tail.copy();
return res;
}
}
הפתרון והתובנה העמוקה: עומק ההעתקה תלוי לחלוטין בעצמים הפנימיים ונקבע על ידי האופן שבו פעולת ההעתקה (copy) מוגדרת פנימית עבור כל עצם ועצם שמועתק!
• עבור חוליות הרשימה (PList) מתבצעת העתקה מבנית עמוקה היוצרת חוליות new PList() חדשות בכל הרמות.
• אך עבור עצמי הנקודות (Point), המתודה head.copy() מחזירה את this — כלומר מבצעת העתקה זהותית (שיתוף מצביע)! מדוע זה חכם ובטוח לחלוטין? משום שהשדות של Point מוגדרים כ-final (Point הוא עצם נצחי / בלתי-משתנה), ולכן שיתוף הנקודות חוסך זיכרון מבלי לסכן כלל את עצמאות הרשימה המועתקת!
העתקה רדודה (shallowCopy) מול העתקה לעומק 2 (copyTwoLevel) והעתקה עמוקה (deepCopy)
מיני-סימולציה 3.5: סייר מתודות ההעתקה ב-Smalltalk וב-Java
מעבדת ההעתקה החיה: בחר רמת העתקה ולחץ על "שנה ערך באלמנט הפנימי" כדי לראות מה קורה למקור!
בחר את סוג ההעתקה ליצירת listCopy מתוך listOrig, ואז לחץ על "שנה ערך באלמנט הפנימי של העותק" וראה האם הרשימה המקורית נפגעת (בגלל מצביעים משותפים) או נשארת מוגנת ועצמאית!
הגשר הנרטיבי: הבנו את כל סודות הזהות, השוויון וההעתקה – כעת נארגן את המילון הטכני המלא של המאפיינים הסטטיים והדינמיים!
כעת, אחרי שראינו איך עצמים בנויים ואיך הם מתנהגים בזיכרון, הגיע הזמן לסכם את פרק 3 באופן פורמלי ומדויק: מה ההבדל המדויק בין תכונה (Attribute), מבנה (Structure), מצב (State) ועצם נצחי (Immutable Object) בצד הסטטי? ומה ההבדל הקריטי בין הודעה (Message), שיטה (Method) ופרוטוקול (Protocol) בצד הדינמי? נעבור ליחידה 3.6!
מאפיינים סטטיים מול מאפיינים דינמיים, וסיכום אינטראקטיבי של פרק 3
הגדרות פורמליות של הצד הסטטי (תכונה, מבנה, מצב ועצם נצחי) והצד הדינמי (לקוח, הודעה, שיטה/מתודה ופרוטוקול), ההבחנה המכרעת בין הודעה לשיטה, ו-15 כרטיסיות תלת-ממד (3D Flashcards) לסיכום הפרק.
מודל מנטלי ועיקרון מנחה: הלקוח שולח "הודעה", אך רק העצם קובע איזו "שיטה" תרוץ!
תכונה (Attribute) היא תא הזיכרון הבודד; מבנה (Structure) הוא אוסף כל התכונות (התבנית); מצב (State) הוא הערכים היושבים במבנה כרגע; ועצם נצחי הוא עצם שמצבו לעולם אינו משתנה!
הלקוח שולח הודעה (Message) סימבולית, והעצם בורר ומריץ שיטה (Method) בתגובה. שני עצמים שונים יכולים לקבל את אותה ההודעה בדיוק, אך להגיב בהפעלת שיטה שונה לחלוטין!
הפרוטוקול (Protocol) של העצם הוא המילון השלם של כל ההודעות השונות שהעצם "מבין" ומסוגל להגיב אליהן.
1. סעיף 3.6: חלוקת עולם המונחים של העצם – סטטי לעומת דינמי
הבחנה מדויקת בין המונחים המתארים את שמירת המצב לבין המונחים המתארים את ההתנהגות והתקשורת
- • תכונה (Attribute): המקום הפיזי הספציפי בזיכרון שבו נשמר המצב הפנימי של העצם. מוכרת בשפות שונות גם בשמות הנרדפים: שדה (Field), מאפיין (Property), איבר נתונים (Data Member), ומשתנה מופע (Instance Variable).
- • מבנה (Structure): האוסף השלם של כל תכונות העצם וההגדרות שלו (התבנית המגדירה אילו תכונות קיימות בעצם).
- • מצב (State): אוסף הערכים הספציפיים הנמצאים בתוך המבנה ברגע נתון.
- • עצם נצחי (Immutable Object): עצם שמצבו הפנימי אינו משתנה לעולם לאורך כל מחזור חייו מרגע יצירתו. לפיכך, המבנה של עצם נצחי מורכב מ"תכונות נצחיות" בלבד (לדוגמה: מספרים, תווים, סימבולים ב-Smalltalk, או מחרוזות ב-Java).
- • תקשורת עם לקוח (Client): לקוח המעוניין לתשאל את העצם על אודות מצבו או לבקש ממנו פעולה עושה זאת אך ורק על ידי שליחת הודעות.
- • הודעה (Message): שם סימבולי מופשט (לעיתים בליווי ארגומנטים נלווים) שהלקוח שולח אל העצם.
- • שיטה / מתודה (Method): התהליך הפנימי (קטע הקוד הממשי) שהעצם בורר ומריץ בתגובה להודעה שקיבל.
- • ההבחנה המכרעת בין הודעה לשיטה: הלקוח שולח הודעה, אך העצם המקבל הוא שקובע איזו שיטה תופעל! עצמים שונים לחלוטין עשויים "להבין" את אותה הודעה בדיוק, אך להגיב אליה באופן שונה על ידי הפעלת שיטה שונה לחלוטין (בסיס הפולימורפיזם).
- • פרוטוקול (Protocol): המילון השלם של כל ההודעות השונות שהעצם מסוגל "להבין" ולהגיב אליהן.
הצד הסטטי (תכונה, מבנה, מצב) והצד הדינמי (לקוח, הודעה, פרוטוקול ובחירת שיטה)
מיני-סימולציה 3.6: שלח את אותה ההודעה לעצמים שונים וראה כיצד כל עצם מפעיל שיטה (Method) שונה!
כרטיסיות זיכרון תלת-ממדיות (3D Flashcards) לכל 15 מושגי פרק 3
לחץ על כל כרטיסייה כדי להפוך אותה ולבחון את עצמך על כל אחד מ-15 מושגי המפתח של פרק 3:
נחשפו: 0 / 15סיימנו את פרק 3 ואת חלק II (עצמים)! מה מחכה לנו בחלק III?
בפרק 3 בנינו יחד מודל מנטלי שלם של המושג "עצם": החל מהחלוקה למרחב הבעיה ולמרחב הפתרון, דרך שלושת עמודי התווך (זהות, מצב והתנהגות), הבנת עצמים מנוונים ו-FILE* בשפת C, ההבחנה החדה בין השוואת זהות (==) להשוואה מבנית (=), רמות העומק של העתקה (shallowCopy, copyTwoLevel, deepCopy, ו-copy/postCopy), ועד למילון המלא של המאפיינים הסטטיים והדינמיים.
image, changes, sources), את תפריט העולם (World Menu), את חלונות העבודה ואת ה-Inspector שמאפשר להציץ לתוך כל עצם חי!