פרק 2: מנגנוני הפשטה • סעיף 2.1 מהבעיה הקונקרטית אל ההפשטה

דוגמה לשימוש במנגנוני הפשטה: ממספרי קסם ללולאת for-each

איך משימה בסיסית של הדפסת איברי מערך חושפת את הסכנה שבחזרה על קבועים ("מספרי קסם"), ואיך שפות התכנות פיתחו 6 רמות הולכות וגבוהות של הפשטה כדי להגן עלינו משגיאות ריצה.

🧩
🧠

מודל מנטלי ועיקרון מנחה: כל כפילות של מידע בקוד היא פצצת זמן של חוסר עקביות!

💡 האנלוגיה המוחשית

לכתוב את המספר 100 פעמיים בקוד — גם בהגדרת גודל המערך וגם בתנאי העצירה של הלולאה — זה כמו להחזיק שני שעונים נפרדים על הקיר. ברגע שתכוון רק אחד מהם, לעולם לא תדע מה השעה הנכונה!

⚖️ חוק הברזל הפדגוגי

יש להימנע מחזרה על קבועים! כאשר קבוע מופיע יותר מפעם אחת בקוד, קיים חשש ממשי שבעת עדכון אחד המופעים יישכח — דבר שיוביל לחוסר עקביות, לגלישה מגבולות המערך ולשגיאות ריצה חמורות.

🎯 תובנת המפתח

מנגנוני הפשטה מתקדמים (כמו שדה .length ולולאת for-each) לא רק מרכזים את הקבוע במקום אחד, אלא מעבירים את האחריות על ניהול הגודל והאינדקסים אל המערך עצמו!

1. נקודת המוצא שלנו: מדוע אנחנו זקוקים למנגנוני הפשטה?

הסכנה הסמויה שמסתתרת אפילו בקטע קוד קצר של 3 שורות

בפרק הקודם אמרתי לך שתכנות מונחה עצמים הוא קודם כל פרדיגמת תכנות שמעניקה לנו מנגנוני הפשטה ייחודיים. אבל לפני שנראה איך נולדו המחלקות והירושה, בוא נשאל את עצמנו שאלה פשוטה: אילו מנגנוני הפשטה אנחנו כבר מכירים משפות תכנות אחרות, ולמה אנחנו בכלל זקוקים להם?

כדי להרגיש את הצורך בהפשטה בידיים, בוא נתבונן במשימה הכי בסיסית שיש: אנחנו רוצים להגדיר מערך בגודל 100 בשפת ++C ולהדפיס את כל איבריו באמצעות לולאת for:

// שלב 1: הקוד הבסיסי ב-++C ללא הפשטה
int a[100];
for (i = 0; i < 100; i++)
    cout << a[i];

על פניו, הקוד הזה עובד. אבל תסתכל טוב על המספר 100: הוא מופיע כאן ישירות פעמיים — פעם אחת בהגדרת גודל המערך (int a[100]), ופעם שנייה בתנאי הסיום של הלולאה (i < 100).

מה הסכנה כאן? נניח שמחר דרישות המערכת משתנות, ואנחנו מתבקשים לעדכן את גודל המערך מ-100 ל-200. בתוכנית אמיתית, הגדרת המערך והלולאה שעוברת עליו עשויות להופיע במרחק של מאות שורות קוד זו מזו (ואולי ישנן 15 לולאות שונות שעוברות על המערך!). קיים חשש ממשי שנעדכן מופע אחד ונשכח לעדכן את המופע השני. חוסר העקביות הזה יוביל לשגיאות ריצה חמורות: או שנתעלם מחצי מהנתונים במערך, או שהלולאה תפלוש לזיכרון שאינו שייך למערך (גלישת חוצץ / Buffer Overflow)! לכן כלל הזהב שלנו הוא: יש להימנע מחזרה על קבועים!

2. מסע ב-6 שלבים: איך מעלים את רמת ההפשטה של המערך והלולאה?

ממאקרו של קדם-מעבד ו-enum ועד למאקרו חישובי SIZEOF, שדה .length ולולאת for-each ב-Java

שלב 2 · מאקרו קדם-מעבד #define N 100

הפשטה באמצעות הוראת מאקרו (#define)

כדי לרכז את גודל המערך במקום אחד בלבד, נוכל להשתמש בהוראת קדם-מעבד (Preprocessor):

#define N 100
int a[N];
for (i = 0; i < N; i++)
    cout << a[i];
שלב 3 · מנגנון השפה enum enum {N = 100};

הפשטה באמצעות enum (ויתרונו על פני מאקרו!)

מדוע enum עדיף על מאקרו? כי enum מנוהל ישירות ע"י המהדר (Compiler) של השפה, מכבד את כללי תחום ההכרה (Scope) ומזוהה ע"י מנפה השגיאות (Debugger) — בניגוד למאקרו שמבצע החלפת טקסט עיוורת בקדם-המעבד לפני ההידור!
(החיסרון הקטן מול הקוד הבסיסי: תוספת שורת קוד והגדרת ישות נוספת עבור פעולה פשוטה).

enum {N = 100};
int a[N];
for (i = 0; i < N; i++)
    cout << a[i];
שלב 4 · חישוב אוטומטי ב-++C SIZEOF(array)

מניעת הופעה כפולה של הגודל באמצעות מאקרו SIZEOF

כדי להגדיר את גודל המערך בשורת הגדרת המערך בלבד (ולא להזכיר את הקבוע שוב בלולאה), ניתן לחשב את כמות האיברים דינמית ע"י חלוקת גודל המערך כולו בבתים בגודל איבר בודד:

#define SIZEOF(array) (sizeof(array) / sizeof(array[0]))
int a[100];
for (i = 0; i < SIZEOF(a); i++)
    cout << a[i];
שלבים 5 ו-6 · תמיכה מובנית ב-Java a.length & for-each

המערך כעצם היודע את גודלו (a.length) והפשטת הלולאה (for-each)

ב-Java המערך הוא עצם הנושא את גודלו בשדה מובנה a.length. וברמת ההפשטה הגבוהה ביותר — לולאת for-each מסתירה לחלוטין את ניהול האינדקסים, תנאי הקצה וקידום המונה!

// שלב 5: שימוש בשדה המובנה a.length
int[] a = new int[100];
for (int i = 0; i < a.length; i++) System.out.print(a[i]);
// שלב 6: הפשטת הלולאה כולה (for-each)
for (int k : a) System.out.print(k);
עזר גרפי 2.1 • סולם 6 רמות ההפשטה במעבר על מערך

איך אנחנו מסתירים בהדרגה פרטים טכניים ומונעים טעויות אנוש?

Abstraction Ladder
1. קבוע כפול (100) סכנת חוסר עקביות וגלישת זיכרון! 2. מאקרו #define ריכוז הקבוע N, אך החלפת טקסט בקדם-המעבד 3. enum {N=100} מוכר למהדר, מכבד Scope ונגיש ל-Debugger 4. מאקרו SIZEOF הגודל מוגדר פעם אחת בלבד במערך ומחושב אוטומטית 5. Java a.length המערך הוא עצם הנושא את גודלו ללא חישוב חיצוני! 6. for (int k : a) הפשטת הלולאה כולה! הסתרה מוחלטת של האינדקס i, תנאי הקצה וקידום המונה. מיקוד ב"מה" ולא ב"איך"
🔍

מיני-סימולציה 2.1: משווה 6 גרסאות ההפשטה (כולל השוואת #define מול enum)

לחץ על כל שלב כדי לראות את הקוד ואת הניתוח ההנדסי שלו
💥
סימולציה אינטראקטיבית גדולה 2.1

מעבדת שגיאות הריצה ו"מספרי הקסם": מה קורה כשמעדכנים גודל מערך מ-100 ל-200?

נסה לשנות את גודל המערך בהגדרה או בתנאי הלולאה מ-100 ל-200 (כפי שקורה כשמתכנת שוכח לעדכן אחד מהמופעים), או הפעל מנגנון הפשטה שימנע את השגיאה לחלוטין:

בחר מצב קוד בסביבת הריצה:
🔬 הדמיית סריקת הזיכרון בזמן ריצה:
⚡

הגשר הנרטיבי: ראינו איך הפשטה מצילה אותנו בדוגמה של מערך – אבל איך התפתחו מנגנוני ההפשטה משחר ימי המחשב ועד לתכנות מונחה עצמים?

הדוגמה של המערך והלולאה נתנה לנו טעימה ראשונה מכוחה של הפשטה. עכשיו אנחנו מוכנים לצאת למסע היסטורי ורעיוני מרתק: איך צמחה החשיבה התכנותית מתוך "תוהו ובוהו" של שפת המכונה, דרך שפת האסמבלר, הפשטה מבוססת נתונים (Fortran, Cobol), פרוצדורות מקוננות (Pascal) ומודולים (Modula-2)? בוא נעבור ליחידה הבאה!

סוף יחידה 2.1 • דוגמה לשימוש במנגנוני הפשטה
פרק 2: מנגנוני הפשטה • סעיפים 2.2.1–2.2.4 התפתחות מנגנוני ההפשטה (חלק א')

התפתחות מנגנוני ההפשטה: מאסמבלר וייצוג נתונים ועד פרוצדורות מקוננות ומודולים

איך התקדמה החשיבה התכנותית משפת מכונה לאסמבלר, מהפשטה מבוססת נתונים גלובליים (Fortran, Cobol) להפשטה מבוססת תהליכים (Pascal), הכשל במימוש מחסנית עם משתנים גלובליים ב-C, והולדת המודול (Modula-2).

🏛️
🧠

מודל מנטלי ועיקרון מנחה: המאבק ההיסטורי להסתרת המידע (Information Hiding)

💡 האנלוגיה המוחשית

משתנים גלובליים הם כמו לוח מודעות פתוח באמצע הרחוב שכל עובר אורח יכול למחוק ולשבש. המסע מאסמבלר ו-Fortran אל Pascal ו-Modula-2 הוא הניסיון לבנות "כספת סגורה" סביב הנתונים שרק לפונקציות מורשות מותר לגשת אליה!

⚖️ חוק הברזל הפדגוגי

בפרוצדורות מקוננות (כמו ב-Pascal) ובשפת C, כדי שמספר פונקציות ישתפו מבנה נתונים (כמו מחסנית), הנתונים חייבים להיות גלובליים — מה ששובר את הסתרת המידע ומגביל אותנו למופע אחד בלבד!

🎯 תובנת המפתח

מנגנון המודול (בשפת Modula-2) פתר את בעיית הסתרת המידע ע"י הפרדה בין מה שמוחצן למה שמוסתר, אך נותרה בו מגבלה מרכזית: מודול מתנהג כמו עצם בודד (מופע יחיד) ולא ניתן לשכפל ממנו מופעים רבים.

1. מה"תוהו ובוהו" של שפת המכונה אל האסמבלר וייצוג הנתונים (2.2.1–2.2.2)

שמות סימבוליים באסמבלר והפשטה מבוססת נתונים גלובליים ב-Fortran וב-Cobol

החשיבה התכנותית שלנו לא נולדה ביום אחד. בראשית ימי המחשוב, תכנות בשפת מכונה היה בגדר "תוהו ובוהו" — רצפים אינסופיים של מספרים בינאריים או הקסדצימליים וכתובות זיכרון פיזיות מוחלטות. מתוך הצורך האנושי לעשות סדר במורכבות הזו, החלו להתפתח מנגנוני ההפשטה שלב אחר שלב:

2.2.1 · אסמבלר (Assembler) 💻

מעבר ממספרים מוחלטים לשמות סימבוליים

שפת האסמבלר היא אחד ממנגנוני ההפשטה הקדומים ביותר (הנעזר במתרגם/מהדר לתרגום לשפת מכונה). במקום לכתוב קודי פקודות מספריים וכתובות זיכרון מוחלטות, האסמבלר איפשר לייצג כתובות זיכרון באופן סימבולי ולהעניק שמות סימבוליים לאופרטורים ולפקודות הבסיסיות (Mnemonic opcodes).
המגבלה: הקוד באסמבלר עדיין היה בלתי מובנה לחלוטין (Unstructured) וחשוף מאוד לשגיאות.

2.2.2 · ייצוג נתונים (Fortran, Cobol) 🗄️

הפשטה מבוססת נתונים ומידע גלובלי משותף

שפות כגון Fortran ו-Cobol שמו דגש מרכזי על אופן ייצוג הנתונים, ואפשרו הגדרת פעולות ישירות על מבני נתונים (באופן המזכיר עבודה מול מסדי נתונים).
המבנה הארכיטקטוני: קיומו של מאגר נתונים גלובליים (Global Data) משותף במרכז, שמספר פרוצדורות שונות (Proc 1, Proc 2, Proc 3, Proc 4) ניגשות אליו ישירות ומבצעות עליו מניפולציות.

2. פרוצדורות מקוננות (2.2.3), כשל המחסנית הגלובלית והולדת המודול (2.2.4)

איך Pascal קידמה את עקרון הסתרת המידע, איפה היא נכשלה בשיתוף נתונים, ואיך Modula-2 פתרה זאת?

בשלב הבא, שפות כמו Pascal שינו את כיוון החשיבה: במקום לשים את הנתונים הגלובליים במרכז, הן שמו את הדגש על התהליך ועל האלגוריתם המתבצע (פרוצדורות מקוננות – Nested Procedures). בגישה הזו, כל תהליך ראשי (למשל Proc 1) מגדיר באופן לוקלי (מקומי) את מבני הנתונים הדרושים לו (Var 1, Type) ואת תת-התהליכים שלו (Proc 2).

הגישה הזו העניקה למתכנתים קפיצת מדרגה אדירה: יכולת להתרכז ב"מה יש לבצע" במקום ב"איך לבצע", תכנות בשיטת "מהטפחות מטה" (Top-Down Programming), ואת ניצניו הראשונים של עקרון הסתרת המידע (Information Hiding).

אבל אז התגלה פגם חמור במנגנון הסתרת המידע! מה קורה כשאנחנו רוצים לבנות מבנה נתונים כמו מחסנית (Stack) בשפה פרוצדורלית כגון C, כך שמספר פונקציות נפרדות (init, push, top, pop) יפעלו על אותה מחסנית? תסתכל על הקוד הבא:

// משתנים גלובליים חשופים לכל התוכנית!
int datastack[100];
int datatop = 0;

void init() { datatop = 0; }
void push(int val) { if (datatop < 100) datastack[datatop++] = val; }
int top() { if (datatop > 0) return datastack[datatop - 1]; return 0; }
int pop() { if (datatop > 0) return datastack[--datatop]; return 0; }

❌ שתי הבעיות הקשות במימוש המחסנית הגלובלית:

  • פגיעה בהסתרת המידע: כדי שהפונקציות push ו-pop יחלקו את הנתונים, המשתנים datastack ו-datatop חייבים להיות גלובליים! כל חלק אחר בתוכנית יכול לגשת אליהם ישירות ולשבש אותם (למשל לאפס את datatop בטעות).
  • מופע יחיד בלבד: ניתן להחזיק רק מחסנית אחת בכל רגע נתון! מה אם התוכנית זקוקה ל-3 מחסניות שונות במקביל?

📦 2.2.4 הפתרון החלקי: מודול (Module ב-Modula-2)

שפת Modula-2 הוסיפה תמיכה מובנית במודולים: מודול הוא אוסף של פרוצדורות ונתונים עם יכולת שליטה מפורשת על מה שמוחצן החוצה (Export) לעומת הנתונים שנשארים מוסתרים ומוגנים בפנים!
⚠️ המגבלה שעדיין נותרה במודול: מודול מתנהג כמו עצם בודד (Singleton) או כמו מחלקה בעלת מופע יחיד בלבד — עדיין אי אפשר לייצר ממנו ריבוי מופעים עצמאיים באופן טבעי!

עזר גרפי 2.2 • השוואת ארכיטקטורות ההפשטה

הפשטה מבוססת נתונים (Global Data) מול הפשטה מבוססת תהליכים (Nested) ומודול (Module)

Data vs. Process vs. Module
1. הפשטה מבוססת נתונים (2.2.2) Global Data Proc 1 Proc 2 Proc 3 Proc 4 נתונים גלובליים חשופים לכל הפרוצדורות 2. פרוצדורות מקוננות (2.2.3) Proc 1 (תהליך עוטף) Var 1 Type Proc 2 (מקוננת) נתונים ותת-תהליכים לוקליים (Top-Down) 3. מודול – Modula-2 (2.2.4) ממשק מוחצן (Export) פרוצדורות פנימיות (push, pop) 🔒 נתונים מוסתרים ומוגנים מגבלה: מתנהג כעצם בודד (מופע יחיד!)
🕰️

מיני-סימולציה 2.2: חוקר 4 תחנות ההתפתחות הראשונות (2.2.1 עד 2.2.4)

לחץ על כל תחנה כדי לראות מה היא חידשה ומה הייתה המגבלה שלה
🛡️
סימולציה אינטראקטיבית גדולה 2.2

מעבדת המחסנית ב-C (משתנים גלובליים חשופים) מול מודול מוגן ב-Modula-2

התנסה במחסנית הממומשת עם המשתנים הגלובליים datastack ו-datatop: דחוף ושלוף איברים, ואז נסה לבצע התערבות חיצונית ישירה במשתנה הגלובלי או ליצור מחסנית שנייה — והשווה בין מימוש גלובלי ב-C לבין מודול מוגן!

מצב הגנה על נתוני המחסנית:
מצב המחסנית בזיכרון (datastack): datatop = 2

שים לב: במודול הנתונים מוגנים מפני דריסה חיצונית, אך גם במודול עדיין קיים רק מופע יחיד של המחסנית!

⚡

הגשר הנרטיבי: המודול העניק לנו הסתרת מידע אך השאיר אותנו עם מופע יחיד בלבד – איך יוצרים אינספור מופעים עצמאיים?

ראינו שמודול מתנהג כמו עצם בודד (מחלקה עם מופע אחד בלבד). אבל בתוכנה אמיתית אנחנו צריכים עשרות מחסניות, מאות רשימות ואלפי חלונות במקביל! ביחידה הבאה נגלה איך שפת Ada (עדה) פתרה זאת באמצעות טיפוס נתונים מופשט (ADT), במה מחלקה (Class) בתכנות מונחה עצמים הופכת את המיקוד של ה-ADT על ראשו (עמודות מול שורות!), ואיך מנגנון הירושה (Inheritance) כיווץ ספרייה של 125,000 שורות קוד ל-10,000 שורות בלבד!

פרק 2: מנגנוני הפשטה • סעיפים 2.2.5–2.2.7 התפתחות מנגנוני ההפשטה (חלק ב')

מטיפוס נתונים מופשט (ADT) למחלקות (Class) ולעוצמת הירושה (Inheritance)

איך טיפוסי נתונים מופשטים (בשפת עדה / Ada) פתרו את בעיית ריבוי המופעים, ההיפוך התפיסתי בין ADT (מוטה-שורות / פעולות) לבין מחלקה ב-OOP (מוטת-עמודות / עצמים), וניסוי הירושה שצמצם 125,000 שורות קוד ל-10,000 בלבד!

🧬
🧠

מודל מנטלי ועיקרון מנחה: היפוך המטריצה (מפעולות לעצמים) וכוח השיתוף של הירושה

💡 ממודול ל-ADT (2.2.5)

בניגוד למודול שהוא כמו בניין יחיד, טיפוס נתונים מופשט (ADT) הוא "תוכנית אדריכלית" (טיפוס חדש שמוגדר ע"י המתכנת) שממנה אפשר להקים אינספור מופעים עצמאיים (Var 1, Var 2, Var 3)!

⚖️ מ-ADT למחלקה (2.2.6)

ב-ADT הדגש מושם על הפעולות (שורות במטריצה) שבוחנות מבחוץ את הנתונים בעזרת case. במחלקה (OOP) הדגש מתהפך אל העצמים והנתונים (עמודות במטריצה), כשכל עצם יודע בעצמו כיצד להתנהג!

🎯 עוצמת הירושה (2.2.7)

מנגנון הירושה מאפשר שיתוף קוד טבעי בין מחלקות: בניסוי מעשי, ספרייה שלמה שנכתבה בשפת Ada ב-125,000 שורות קוד מומשה מחדש ב-++C עם ירושה ב-10,000 שורות קוד בלבד!

1. סעיף 2.2.5: טיפוס נתונים מופשט (Abstract Data Type – ADT)

הפתרון של שפת עדה (Ada) לבעיית המופע היחיד של המודול, וייצוג רשימה באמצעות פירוק למקרים (case)

ראינו קודם שמודול (בשפת Modula-2) פתר את בעיית הסתרת המידע, אבל השאיר אותנו עם מגבלה כבדה: מודול פועל כמו עצם בודד (מופע יחיד). כדי לפתור זאת, שפות כמו Ada (עדה) לקחו את המודולריות צעד מכריע קדימה והוסיפו תמיכה מובנית בטיפוסי נתונים מופשטים (ADT – Abstract Data Type).

מהו ההבדל המהותי בין מודול ל-ADT? ב-ADT המתכנת מגדיר טיפוס חדש לגמרי במערכת! מתוך הטיפוס הזה ניתן ליצור מספר רב של מופעים עצמאיים (Var 1, Var 2, Var 3), כאשר לכל מופע יש מצב פנימי משלו, וכולם חולקים את אותו סט של אופרטורים ופעולות (Op 1, Op 2, Op 3).

בוא נתבונן בדוגמה קלאסית של הגדרת רשימה (List) כטיפוס נתונים מופשט (ADT):

adt List
representation
    list = Nil | CELL of integer * list
operations
    nil = Nil
    cons(x: integer, l: list) = CELL(x, l)
    empty(l: list) = case l of Nil => true | CELL(x, l') => false
    tail(l: list) = case l of Nil => error | CELL(x, l') => l'

שים לב איך פועלים האופרטורים ב-ADT: הפעולות empty(l) ו-tail(l) הן פונקציות שמקבלות את מבנה הנתונים l כפרמטר, ומבצעות עליו פירוק למקרים (case) כדי לשאול מבחוץ: "האם אתה רשימה ריקה Nil או שאתה חוליה CELL?".

2. סעיף 2.2.6: מחלקה (Class) – היפוך המיקוד מפעולות לעצמים!

איך ייצוג מונחה עצמים מעלים את משפטי ה-case, ומה ההבדל בין תכנון מוטה-שורות (ADT) לתכנון מוטה-עמודות (OOP)?

כאן אנחנו מגיעים אל רגע הלידה של תכנות מונחה עצמים (OOP) ואל מושג המחלקה (Class)! בין מושג ה"מחלקה" לבין "טיפוס נתונים מופשט" (ADT) יש דמיון רב: בשפה מונחית עצמים, המחלקה היא אכן טיפוס נתונים מופשט שמאגד נתונים ופעולות ומאפשר ליצור מופעים רבים. אבל יש ביניהם הבדל רעיוני עמוק במיקוד:

  • ב-ADT (טיפוס נתונים מופשט): הדגש מושם על ייצוג הפעולות (האופרטורים) המנתחות את הנתונים.
  • ב-מחלקה (Class ב-OOP): הדגש מושם על ייצוג הנתונים (העצמים) וההתנהגות הפנימית שלהם!

בוא נראה איך נראית אותה רשימה בדיוק כשאנחנו מממשים אותה בגישה מונחית עצמים — על ידי פיצול לשתי ישויות עצמאיות: רשימה ריקה (EmptyList) ורשימה שאינה ריקה (NonEmptyList):

EmptyList = recursive self = record
    empty = true;
    head = error;
    tail = error;
    cons = fun(x) NonEmptyList(x, self);
end
NonEmptyList(x, l) = recursive self = record
    empty = false;
    head = x;
    tail = l;
    cons = fun(x) NonEmptyList(x, self);
end

תסתכל איזה יופי: נעלמו לחלוטין משפטי ה-case! אף פונקציה חיצונית לא צריכה לשאול "איזו רשימה את?". כל עצם (רשימה ריקה או לא-ריקה) "יודע בעצמו" מה להחזיר עבור empty, head ו-tail!

כדי להבין את ההבדל המבני העמוק בין ADT לבין OOP, דמיין טבלה (מטריצה) שבה השורות הן הפעולות (empty?, head, tail) והעמודות הן המבנים/המחלקות (nil, cons):

↔️ הפשטת ADT היא מוטת-שורות (Row-Oriented)

הקוד מאורגן לפי הפעולות (השורות): כל פונקציה (כמו tail) מרכזת בתוכה את הטיפול בכל סוגי המבנים (Nil ו-CELL).
• יתרון: קל להוסיף פעולה חדשה (מוסיפים שורה חדשה).
• חיסרון: הוספת מבנה נתונים חדש מחייבת לפתוח ולעדכן את כל הפונקציות הקיימות!

↕️ הפשטה מונחית עצמים היא מוטת-עמודות (Column-Oriented)

הקוד מאורגן לפי המבנים/המחלקות (העמודות): כל מחלקה (EmptyList או NonEmptyList) מאגדת בתוכה את כל הפעולות עבור עצמה.
• יתרון: קל מאוד להוסיף מחלקה/וריאציה חדשה בלי לגעת בקוד קיים (מוסיפים עמודה חדשה)!
• חיסרון: הוספת פעולה חדשה מחייבת עדכון של כל המחלקות בהיררכיה.

3. סעיף 2.2.7: ירושה (Inheritance) – מ-125,000 שורות קוד ל-10,000 בלבד!

איך מנגנון הירושה מאפשר שיתוף קוד דרמטי בין מחלקות ומונע שכפול מיותר?

השלב השביעי והמכריע בהתפתחות מנגנוני ההפשטה הוא מנגנון הירושה (Inheritance). מהי ירושה? זהו מנגנון הפשטה מתקדם המאפשר שיתוף ושימוש חוזר (Reuse) של קוד והתנהגות בין מחלקות שונות. במקום לכתוב מחדש פעולות דומות בכל מחלקה (כמו cons שראינו קודם שזהה גם ב-EmptyList וגם ב-NonEmptyList), מחלקות יכולות לרשת התנהגות משותפת ממחלקת-על!

כדי להמחיש עד כמה מנגנון הירושה עוצמתי בעולם האמיתי, נערך ניסוי השוואתי מדהים: נלקחה ספרייה תכנותית שלמה שנכתבה בשפת עדה (Ada) (שבה היו טיפוסי נתונים מופשטים ADT אך ללא ירושה מלאה) והכילה 125,000 שורות קוד (125K LOC). אותה ספרייה בדיוק מומשה מחדש בשפת ++C תוך שימוש בעקרונות ה-OOP ובמנגנון הירושה — והתוצאה הייתה מדהימה: המימוש החדש דרש 10,000 שורות קוד בלבד (10K LOC)! צמצום של יותר מ-90% בנפח הקוד בזכות שיתוף קוד בירושה!

עזר גרפי 2.3 • מ-ADT למחלקות וירושה (2.2.5–2.2.7)

ריבוי מופעים ב-ADT, מטריצת פעולות מול מבנים, וצמצום הקוד בירושה (125K מול 10K)

ADT · Class · Inheritance
1. ריבוי מופעים ב-ADT (2.2.5) Var 1 מצב עצמאי Var 2 מצב עצמאי Var 3 מצב עצמאי אופרטורים משותפים: Op 1, Op 2, Op 3 טיפוס חדש עם אינספור מופעים! 2. מטריצת ADT מול OOP (2.2.6) nil (ריקה) cons (לא-ריקה) empty? head tail ↔ כחול (שורות): גישת הפעולות ב-ADT ↕ סגול (עמודות): גישת העצמים ב-OOP 3. עוצמת הירושה (2.2.7) 125K שפת Ada (ללא ירושה) 10K בלבד! שפת ++C (עם ירושה ב-OOP)
📈

מיני-סימולציה 2.3: ניסוי הירושה – השוואת שורות הקוד בין Ada (ללא ירושה) ל-++C (עם ירושה)

שפת עדה (Ada) – מימוש הספרייה ללא מנגנון ירושה: 125,000 שורות קוד (125K LOC)
שפת ++C – מימוש מחדש של אותה ספרייה עם מנגנון ירושה: 10,000 שורות קוד בלבד (10K LOC – חיסכון של 92%!)
🎛️
סימולציה אינטראקטיבית גדולה 2.3

מעבדת המטריצה: הפשטת ADT (מוטת-שורות) מול מחלקה ב-OOP (מוטת-עמודות)

בחר תרחיש במטריצת הרשימה (empty?, head, tail מול nil, cons) וראה כיצד חלוקת הקוד לשורות (ADT) או לעמודות (OOP) קובעת מה קל ומה יקר להוסיף למערכת:

⚡

הגשר הנרטיבי: השלמנו את כל 7 תחנות ההתפתחות של מנגנוני ההפשטה – בוא נסכם ונתרגל!

עברנו יחד מסע מרתק: מבעיית החזרה על קבועים ("מספרי קסם") בלולאת המערך, דרך אסמבלר, ייצוג נתונים גלובליים, פרוצדורות מקוננות, מודולים וטיפוסי נתונים מופשטים (ADT), ועד למחלקות ולירושה בתכנות מונחה עצמים. ביחידה המסכמת הבאה נרכז את כל מושגי המפתח של פרק 2 בכרטיסיות זיכרון תלת-ממדיות (3D Flashcards) ונתכונן לחלק II (פרק 3: המושג "עצם")!

פרק 2: מנגנוני הפשטה • יחידה 2.3 אינטגרציה ואינדקס מושגים

מילון מושגים אינטראקטיבי וסיכום פרק 2 (מנגנוני הפשטה)

סיכום ציר ההתפתחות המלא משפת מכונה ועד מחלקות וירושה ב-OOP, בוחן אבחנה אדריכלי מהיר, ו-12 כרטיסיות זיכרון תלת-ממדיות על כל מושגי הפרק.

🏆
🧠

מודל מנטלי ועיקרון מנחה: כל שלב בהיסטוריה נולד כדי לפתור את מגבלת השלב שקדם לו!

💡 מהסתרת קבועים להסתרת מידע

מניעת חזרה על קבועים (enum, SIZEOF, .length, for-each) מונעת חוסר עקביות בקוד, והסתרת המידע (בפרוצדורות מקוננות ובמודולים) מגינה על הנתונים מפני שיבוש חיצוני.

⚖️ ממופע יחיד לריבוי מופעים ועצמים

מודול (Modula-2) פתר את ההסתרה אך הוגבל למופע יחיד. ADT (בעדה) איפשר ריבוי מופעים במיקוד על פעולות (שורות), ומחלקה ב-OOP הפכה את המיקוד אל העצמים עצמם (עמודות).

🎯 שיתוף קוד באמצעות ירושה

מנגנון הירושה השלים את המהפכה בכך שאיפשר למחלקות שונות לחלוק קוד והתנהגות — כפי שהוכח בצמצום ספריית Ada מ-125K שורות ל-10K שורות בלבד ב-++C.

עזר גרפי 2.4 • ציר ההתפתחות השלם של מנגנוני ההפשטה (2.2.1–2.2.7)

7 התחנות ההיסטוריות והרעיוניות שהובילו לצמיחת התכנות מונחה העצמים

7-Stage Timeline
1. אסמבלר שמות סימבוליים לפקודות וכתובות. בלתי מובנה 2. ייצוג נתונים Fortran / Cobol נתונים גלובליים Global Data חשוף 3. קינון תהליכים Pascal (Top-Down) נתונים לוקליים. שיתוף רק בגלובלי 4. מודול Modula-2 שליטה בהחצנה. מופע יחיד בלבד! 5. טיפוס ADT שפת Ada ריבוי מופעים! מוטה-שורות (פעולות) 6. מחלקה (Class) שפות OOP דגש על העצמים! מוטה-עמודות (עצמים) 7. ירושה Inheritance שיתוף קוד בין מחלקות 125K ➔ 10K שורות!
⚡

מיני-סימולציה 2.4: בוחן אבחנה מהיר – איזה מנגנון הפשטה פתר איזו מגבלה?

לחץ על כל דילמה הנדסית
🃏
סימולציה אינטראקטיבית גדולה 2.4

כרטיסיות זיכרון תלת-ממדיות (3D Flashcards) לכל 12 מושגי פרק 2

לחץ על כל כרטיסייה כדי להפוך אותה ולבחון את עצמך על כל אחד מ-12 המושגים של פרק 2:

נחשפו: 0 / 12
🚀

סיימנו את פרק 2 ואת חלק I (מבוא)! מה מחכה לנו בחלק II?

בפרק 2 ראינו כיצד כל שלב בהתפתחות מנגנוני ההפשטה (מאסמבלר, דרך ייצוג נתונים, פרוצדורות מקוננות, מודולים וטיפוסי נתונים מופשטים ועד למחלקות ולירושה) נולד כדי לאפשר לנו לבנות מערכות מורכבות יותר, בטוחות יותר וללא שכפול קוד.

🎯 התחנה הבאה שלנו – חלק II: עצמים, פרק 3: המושג "עצם": בפרק 3 נצלול אל מהות העצם: זיהוי, מצב והתנהגות, מימוש עצמים בשפת C, וההבחנה החשובה שבין זהות עצמית (Identity) לבין שוויון (Equality)!
🕹️

מעבדת חקר אינטראקטיבית

❓

5 שאלות פתוחות מדורגות + 5 שאלות אמריקאיות עם הסברים מלאים לכל יחידה

🚀 התחנה הבאה שלנו: חלק II – פרק 3: המושג "עצם"

השלמנו את פרק 2: מנגנוני הפשטה ואת חלק I (מבוא)! התחנה הבאה שלנו היא חלק II: עצמים – פרק 3: המושג "עצם", שבו נחקור יחד: