קרב וירטואלי-אבסטרקטי

במשך השנים אני פוגש סטודנטים לשעבר שלי, והם מספרים לי על ראיונות עבודה שהם עברו. זה מעניין לשמוע איך “בעולם האמיתי” מתייחסים לידע האקדמי שאנחנו לומדים בקורסים, אבל זה גם טוב לדעת שהידע הזה לא נלמד לשווא – הוא נחוץ ושימושי. כיוון שצברתי אצלי לא מעט שאלות שכאלה, שנשאלו בראיונות עבודה, החלטתי לשתף בכמה מהן מידי פעם, אולי זה יהיה לעזר. אז היום – על ההבדל המיתולוגי בין ג’אווה ל-c++ בשאלת המחלקות המופשטות.
ג’אווה היא שפה שנבנתה על ברכי C++ ולכן מאוד דומה לה. הסינטקס דומה מאוד, והמון ביטויים ורעיונות מ C++ מצאו את דרכם גם לג’אווה.
אבל השפות נבדלות אחת מהשניה בצורה עמוקה. אין פה מקום לפרט את כל ההבדלים, אז נתמקד היום באחד מהם, בהתייחסות ובביטוי של השפות למחלקות מופשטות.
מהי מחלקה מופשטת? ננסה להסביר את זה בדוגמא – נניח שאנחנו כותבים אפליקציה לניהול מוסך. כשאנחנו מתכננים וחושבים על המחלקות שיהיו באפליקציה, כנראה שנחשוב על המחלקה Vehicle שמייצגת כלי רכב. מהמחלקה הזאת ירשו המחלקות שייצגו את כלי הרכב השונים שמטפל בהם המוסך – נניח רכבים פרטיים, רכבי שטח, רכבים דו גלגליים, משאיות וכו’. יש טעם להגדיר את Vehicle כמחלקה נפרדת כי יש דברים שמשותפים לכל כלי הרכב, למשל מספר רכב, או שם בעלי הרכב. אבל הנה שאלה חשובה – האם ברמה האפליקטיבית, אנחנו יכולים למצוא אובייקט שהוא רק Vehicle? כלומר אובייקט שהוא לא ג’יפ או אופנוע או רכב משפחתי, אלא פשוט “כלי רכב”? האם יכול להיות מצב היפותטי בו עובד במוסך פונה לחברו ואומר לו “הנה נכנס עכשיו כלי רכב לטיפול” בלי שזה יהיה כלי רכב ספציפי? התשובה היא לא. נכון שאפשר לדבר על כל הישויות שנמצאות במוסך כ”כלי רכב”, זאת בדיוק הסיבה שהגדרנו מחלקה כללית שממנה כולם ירשו, אבל המחלקה הזאת, מחלקת ה- Vehicle לא יכולה לעמוד בפני עצמה. לאובייקטים שלה אין תפקיד עצמאי באפליקציה והם חסרי שימוש ולפעמים אפילו יכולים לגרום לשגיאות בתוכנה.
(דוגמא מעניינת שאולי תעזור להבין – נניח שיש לנו מחלקה שנקראת Animal שמייצגת חיה. איך הייתם עונים על השאלה “איך עושה חיה?” ? תחשבו על זה).
מחלקה כזאת, שמצד אחד מוצדק להגדיר אותה כי היא מהווה בסיס משותף למחלקות אחרות, אבל מצד שני כשלעצמה היא חסרת קיום באפליקציה, נקראת מחלקה מופשטת. ובראש ובראשונה, מחלקה מופשטת היא מחלקה שאי אפשר ליצור ממנה אובייקטים.
בג’אווה אפשר להגדיר מחלקה מופשטת ע”י צירוף המילה abstract להגדרת המחלקה –

public abstract class Vehicle { …}

וכעת, נסיון לייצר אובייקטים מהמחלקה יוביל לשגיאת קומפילציה –

Vehicle v = new Vehicle();

שימו לב שאין מניעה להגדיר משתנה מסוג המחלקה המופשטת, כל עוד לא יצרנו אובייקט –

Vehicle v; // OK

גם ב C++ קיים המושג של מחלקה מופשטת, אבל שם זה שונה. אם תנסו תראו שאין ב C++ אפשרות להגדיר מחלקה מופשטת סתם כך. כדי לעשות את זה, המחלקה צריכה להכיל שיטה מופשטת אחת לפחות.
מהי שיטה מופשטת? שיטה שאין לה מימוש במחלקה, שיטה שהיא רק חתימה. אם תרצו, זאת דרך של מחלקת האב “לסמן” לבנים מה הכותרת של הפונקציונליות שאמורה להיות, בלי להגיד מה התוכן. הבנים שיורשים ממחלקת האב חייבים לדרוס את כל השיטות המופשטות שמוגדרות אצל האב.
נחזור שוב לג’אווה – אפשר להגדיר בג’אווה שיטה מופשטת ע”י צירוף המילה abstract בחתימת השיטה:

public abstract class Vehicle {
public abstract void drive();
}

שימו לב שוב להגיון שבדבר – המחלקה Vehicle מצהירה שיש לה פונקציונליות שנקראת drive אבל היא לא מממשת אותה. כל מחלקה שתירש מ-Vehicle תהיה חייבת לתת את הפרשנות שלה למושג “drive”, לפרש בעצמה את הפונקציונליות. האבא מכתיב ממשק, אבל לא את המימוש שלו.
ב C++ אפשר להגדיר שיטה מופשטת ע”י צירוף המילה virtual בחתימת השיטה, ו =0 בסופה (מהסיבה הזאת היא נקראת pure virtual):

class Vehicle {
public:
virtual void drive() = 0;
};

גם פה, כמו בג’אווה, כל מחלקה שתירש מ-Vehicle תהיה חייבת לדרוס את הפונקציה drive.
הנה כמה הבדלים בין השפות בנושא הזה, מעבר להבדלים התחביריים –
1. בג’אווה אפשר להגדיר מחלקה מופשטת שלא תכיל אף שיטה מופשטת. ב C++ אי אפשר. האמת היא שהדרך היחידה של מחלקה להפוך למופשטת היא להכיל שיטה מופשטת.
2. בג’אווה, שיטה מופשטת לעולם לא תכיל מימוש. ב C++ היא יכולה להכיל מימוש (למרות שזה די נדיר).
3. בג’אווה, מחלקה שמכילה שיטה מופשטת חייבת להכיל את המילה abstract בהגדרתה, אחרת היא לא תעבור קומפילציה. ב C++ ראינו שאין אינדיקציה נפרדת למחלקה מופשטת.
יש גם את הדברים המשותפים בין השפות, שזה בעיקר העיקרון של מחלקה מופשטת ושיטה מופשטת, והרעיון של הגדרת ממשק ריק ע”י האבא והשארת המימוש שלו לבנים, שבסופו של דבר, מעבר להבדלים בין השפות, זה הדבר החשוב.
והנה שאלה קטנה לבחינת העירנות – ראינו שבג’אווה ניתן להגדיר

Vehicle v;

אבל ב C++ המשפט הזה יגרום לשגיאת קומפילציה. למה?
ובנוסף, המילה virtual ב C++ היא מאוד שימושית – מכירים עוד תפקידים שלה בשפה מעבר לציון שיטה מופשטת?