מדריך C# – תכנות מונחה עצמים: מבוא
מאמר זה פותח סדרה של מאמרים בנושא תכנות מונחה עצמים (Object Oriented Programming). בניגוד לשאר המאמרים בסדרה זו, מאמר זה הינו תאורטי ואינו משלב קוד טכני ומטרתו היא להציג את הקונספט של תכנות מונחה עצמים.
שיטת Object Oriented – כל דבר הוא אובייקט
השיטה של תכנות מונחה עצמים (אובייקטים) הינה למעשה, חיקוי של חשיבת האדם, כלומר, היא אינטואיטיבית וקרובה יותר לדרך החשיבה של רוב האנשים המטפלים בבעיה העומדת בפניהם.
נסביר: בבואנו להסתכל על העולם – אנו באופן טבעי ואוטומטי מסווגים את כל העצמים על פי קטגוריות שונות. לדוגמא, העולם הפיזי מחולק על ידנו לחי, צומח ודומם. קיימת גם חלוקה פנימית יותר ובה את החי אנו מחלקים ליונקים, עופות, דגים וכך הלאה לחלוקות פנימיות יותר. כל עצם או מושג המוכר לנו בעולם הזה משתייך למספר רב של קטגוריות בסדר היררכי כלשהו.
למה?
התשובה לכך פשוטה, כך קל ונח יותר לשלוט בכמות האדירה של האינפורמציה בה אנו מוצפים וכך יעיל ונח יותר לסדר אינפורמציה זו במוחנו, בכדי שנוכל לשלפה ולזהותה בקלות בכל עת שנחפוץ.
נראה דוגמא – בבואנו לספר לחבר כי רכשנו רכב חדש אין צורך לפרט לו כי רכשנו מנוע, קרבורטור(מאייד), מצמד, דוושת דלק, רדיאטור, הגה וכו'…. אלא בעצם הגדרת המושג רכב יידע החבר כי כל אלו נכללים, ומכאן שאין צורך בפירוט מצדנו וזה יתרון עצום! אחרת עבור כל רכב היינו צריכים לציין את כל מרכיביו, בעוד שבזכות העובדה כי אנו מסווגים ומקטלגים את הדברים כל שנצטרך לציין הן התכונות המייחדות את הרכב שלנו ולא את המשותפות לכלל הרכבים.
תכונה נוספת המאפיינת את תהליך החשיבה של האדם הינה – שיוך של פעולה לעצם כלשהו. למשל, נשווה בין שתי הפעולות הבאות: הדלקת נר והדלקת המחשב – לכאורה, מבחינה לוגית משמעותן של שתי הפעולות זהה – שתיהן פעולות "הדלקה", אך הנגזרת של כל פעולה היא מימוש פיזי אחר – כלומר, סידרת הצעדים אשר נבצע שונה לחלוטין בין פעולה אחת לאחרת.
ומכאן כשנתבקש לבצע פעולת "הדלקה" אנו נדע איזו פעילות עלינו לבצע על פי שיוך של הפעולה לעצם אותו אנו מתבקשים להדליק. כלומר, קיים קשר חשוב בין הפעולה לבין העצם עליו או איתו היא מתבצעת.
שני התהליכים החשיבתיים שהגדרנו אצל בני האדם מיושמים בצורה דומה בגישה מונחית העצמים ע"י יצירת טיפוס חדש המכונה Class (מחלקה) המאפשר ריכוז של מאפיינים (משתנים) ומטודות (פונקציות) תחת הגדרה אחת.
לאחר הגדרת המחלקה ניתן לייצר מופעים שלה, כל מופע של מחלקה קרוי Object (אובייקט). כאשר המחלקה הינה תבנית ליצירת אובייקטים מסוגה, כלומר, המחלקה מהווה את ההגדרה של המשתנים והשיטות שיכילו אח"כ כל מופעיה (האובייקטים מסוגה).
אלמנטים עיקריים של שיטת Object Oriented
כפי שהוסבר, השיטה המכוונת עצמים תומכת ובנויה ממחלקות ואובייקטים, אך קיימים בה מרכיבים עיקריים ומהותיים נוספים בבניית מערכות והם מהווים את משולש תכונותיה העיקריות:
כימוס – Encapsulation
כימוס, הנקרא לעיתים הסתרת מידע, מאפשר הסתרת כל המרכיבים הפנימיים של האובייקט ומאפשר גישה אליהם אך ורק דרך הפונקציות של האובייקט המהוות את הממשק.
מכאן שאנו מגנים על מאפייני האובייקט באי מתן גישה ישירה אליהם, אלא אך ורק דרך פונקציות ממשק שהוגדרו לאותו אובייקט והן לא מאפשרות לבצע במאפייניו שינויים בלתי הולמים או חוקיים.
כימוס מספק את הגבול שבין מאפייניו הפנימיים של האובייקט לבין הממשק החיצוני שלו, בכך שהוא לא מאפשר שינוי באופן ישיר של משתני האובייקט, אלא רק דרך פונקציות הממשק שלו.
מושג נוסף אשר מרכיב את הכימוס הוא מושג ההפשטה (Abstraction):
הפשטה (Abstraction)
הפשטה המהווה חלק מהכימוס, מתייחסת לאופן שבו מיוצגת בעיה נתונה במרחב התוכנית. שפות התכנות לכשעצמן מספקות הפשטה. אנו התוכניתנים לא עוסקים בפנים של המחשב, כלומר, מרחב זיכרון, אוגרים ומחסניות של המעבד.
באופן כללי, תוכנית אינה אלא תיאור מופשט של הליך או תופעה, שמתקיימים או מתרחשים בעולם הממשי. אולם, הקשר בין ההפשטה ושפת התכנות הוא דו צדדי: מצד אחד משתמשים בשפת התכנות כדי לכתוב תוכנית המהווה הפשטה של העולם הממשי, ומצד שני משתמשים בשפת התכנות כדי לתאר בצורה מופשטת התנהגות פיזית של המחשב בו משתמשים (למשל על ידי שימוש במספרים עשרוניים במקום בייצוג בינארי שלהם, במשתנים במקום בכתובות מפורשות של תאי זיכרון וכדומה).
וכפי ששפות התכנות העיליות יוצרות הפשטה של ההתנהגות הפיזית של המחשב ומאפשרות להשתמש במרחבי הזיכרון שלו בצורה שקופה, מבלי צורך להעמיק בחומרה שמרכיבה אותו, כך אנו צריכים לתכנן את המחלקות שמשתתפות במערכת שלנו כדי שיאפשרו עבודה נוחה וברורה יותר ללקוחות המחלקות שלנו (=אותם תוכניתנים שייצרו אובייקטים מסוג המחלקות).
מכאן, שעל מנת לאפשר ללקוחות המחלקה שלנו להתמקד במטרה ולא לעסוק בפרטים הנוגעים לדרך הפעולה של המחלקה עצמה, עלינו לתכנן הפשטה של המחלקה בדרך הטובה ביותר. ממשק המחלקה הוא המימוש של ההפשטה!
נסביר את החשיבות בעזרת אנלוגיה למכשיר כספומט המוכר לנו מחיי היום יום. מכשיר זה אמור לקבל כקלט כרטיס וסיסמא תואמת ולאפשר ללקוח משיכת מזומנים או הצגת פרטי חשבון רלוונטיים. למכשיר הכספומט יש קבוצת פונקציות שעליו להציג בפני המשתמשים. פונקציות אלו מהוות את הממשק למשתמש. ממשק זה מתבטא בחריץ להעברת הכרטיס, חריץ להוצאת הכסף או פלט, צג, מקלדת וכו' – בעצם כל אחד מאלו מהווה חלק ממשק המכשיר.
מכשירי הכספומט לא השתנו הרבה מאז המצאתם, זאת משום שלמרות העובדה שהמערכת הפנימית שלהם השתנתה עם התפתחות הטכנולוגיה, הרי שהממשק הבסיסי שלהם לא היה צריך להשתנות בהרבה, ובכך איפשר למשתמשי הכספומט לעבוד באותה צורה לה הורגלו.
חלק אינטגרלי של תכנון ממשק מחלקה הוא הבנה עמוקה של מרחב הבעיה כולה. הבנה זו תעזור לנו ליצור ממשק המספק למשתמשים גישה למידע שהם זקוקים לו, אך מבודד אותם מהעבודה הפנימית של המחלקה. עלינו לתכנן מחלקה שתאפשר שינוי של מאפייני המחלקה בלי שתהיה לכך השפעה על קוד קיים.
בנוסף לכך, אל לנו להשתמש במונחים זרים ללקוחות המחלקה שלנו כי אם במונחים טבעיים אשר יהיו ברורים גם ללקוחות שהם לא מתמחים בנושא המנוהל ע"י המחלקה. כמו כן עלינו להגן על משתני המחלקה בצורה כזו שכל שינוי שלהם יהיה דרך פונקצית ממשק, בכדי שנהיה בטוחים כי ננקטו כל אמצעי הזהירות, דבר שלכאורה לא בהכרח יידעו לקוחות המחלקה לבצע, מטבע הדברים של חוסר בקיאות במחלקה שלנו.
לסיכום – עיצוב ההפשטה של מחלקות בדרך המועילה ביותר לתוכניתנים המשתמשים בהן הוא בעל חשיבות עליונה בעיצוב תוכנה הניתנת לשימוש חוזר (Reuse). ממשק יציב, סטטי ועקבי לאורך שינויי יישום עתידיים של המחלקה מחייב פחות שינויים עם הזמן.
ההפרדה בין המשתמש (התוכניתן המשתמש במחלקה שפיתחנו) לבין פרטי היישום – היא זו שהופכת מערכת שלמה ומורכבת לקלה יותר להבנה ולכן גם לאחזקה.
הורשה – Inheritance
הורשה היא הכלי שבאמצעותו ניתן להגדיר מחלקה במושגים של מחלקה אחרת ולא מאפס, כלומר, להגדיר מחלקה היורשת את מרכיביה (משתנים ופונקציות) ממחלקה אחרת.
על ידי הורשה יכול המתכנת להגדיר סוג של קשר בין מחלקה אחת לאחרת, באמצעות ההורשה ניתן לגזור מחלקה קיימת ובכך ליצור מחלקה חדשה אשר מכילה את הקיימת, בהמשך ניתן לשנות את המחלקה החדשה בדרך הרצויה.
המחלקה אותה אנו יורשים נקראת מחלקת הבסיס (Base Class) והמחלקה החדשה שנוצרה ע"י מחלקת הבסיס נקראת מחלקה נגזרת (Derived Class). המחלקה הנגזרת יורשת את כל החברים (משתנים ופונקציות) של מחלקת הבסיס.
יכולת זו מאפשרת חיסכון בכתיבת קוד ושימוש בקוד קיים. נדגים – אנו מתכננים מערכת לניהול אוניברסיטה כלשהי ומכאן שחלק מן הישויות המעורבות הן: סטודנטים, מרצים ועובדי מנהלה. עבור כל אחת מישויות אלו נצטרך לשמור את התכונות הבאות: שם פרטי, שם משפחה, תעודת זהות, כתובת וכו'. בנוסף, לכל אחת מהישויות נצטרך לשמור פרטים נוספים שונים המייחדים אותה – לדוגמא: שנת לימוד לישות סטודנט, תעריף לשעה לישות מרצה ומשכורת בסיס לישות עובד מנהלה.
אנו יכולים להגדיר כל ישות בנפרד על כל תכונותיה, אך בכך לכתוב קוד זהה המגדיר את התכונות המשותפות ופונקציות הממשק שלהן עבור כל ישות (מחלקה). פתרון זה יגרום לכתיבת קוד מיותרת, מה גם שכל שינוי במימוש של פונקצית ממשק יגרור צורך בשינוי בכל אחת מהמחלקות.
בתרשים זה מתוארות המחלקות ללא השימוש בהורשה, כאשר אנו רואים כאן את החזרה על התכונות המשותפות בכל מחלקה, כמו כן תהיה חזרה על פונקציות הממשק המנהלות תכונות אלו בכל מחלקה (לא מתואר בשרטוט).
פתרון אחר יעיל יותר, הן מבחינת חיסכון בקוד והן מבחינת ניהול עקבי שלו, הוא פיתרון בו נגדיר ישות אדם אשר תכיל את כל התכונות ופונקציות הממשק שמשותפות לכל הישויות, ובהמשך כל אחת משלושת הישויות תירש את מחלקת האדם (מחלקת הבסיס) ותוסיף לה את התכונות והפונקציות המייחדות אותה.
בתרשים זה מתוארות המחלקות עם שימוש בהורשה, כאשר אנו רואים שכל המידע המשותף מנוהל במחלקת בסיס אותה יורשות המחלקות הנגזרות. כל אחת מהמחלקות הנגזרות תוסיף על התכונות שהיא ירשה את התכונות המייחדות אותה ובנוסף תשנה ותוסיף את פונקציות הממשק הדרושות לה.
תכונת ההורשה שימושית גם במקרים בהם נרצה להוסיף תכונות או לשנות פעילות של Class קיים מבלי לפגוע ב- Class המקורי, או כאשר אין בידנו את הקוד של ה- Class המקורי (אלא קובץ לאחר קומפילציה) ואז הפתרון הוא הורשת המחלקה הקיימת.
דבר נוסף חשוב מאד, אותו נבין רק בתום קריאת הפרק הבא והוא שההורשה היא זו שמאפשרת את ה- Polymorphism.
רב-צורתיות – Polymorphism
בתחילה נעזר בדוגמא על מנת להמחיש את מטרת עקרון הרב-צורתיות.
נניח ואנו מתכננים מערכת לניהול מוסך. המוסך שלנו מטפל ברכבים מהסוגים הבאים: אוטובוסים, רכבים פרטיים ומשאיות. בהתאם לתכונה ההורשה שלמדנו קודם אנו מניחים כי נגדיר מחלקת בסיס Car שתכיל את כל הנתונים המשותפים וממנה יגזרו 3 המחלקות: Truck, Private ו- Bus, כאשר כל אחת מהמחלקות תוסיף ותעדכן את התכונות הייחודיות לה.
מחלקת Garage (מוסך) תכיל מבנה נתונים שינהל את כל הרכבים שנמצאים ברגע נתון מסוים בטיפול במוסך, נניח ומבנה הנתונים שלנו יוחזק בתוך מערך .
מכאן, שהמערך שלנו צריך להחזיק ולנהל אובייקטים מסוגים שונים (Truck, Private או- Bus)!
נסביר: רב-צורתיות מספקת יתרון בכך שהיא מעניקה את היכולת לקבץ אובייקטים להם יש מחלקת בסיס משותפת ולהתייחס אליהם בצורה עקבית.
אם נתבונן בדוגמא, נשים לב כי המשותף ל- 3 המחלקות (Truck, Private ו- Bus) היא מחלקת הבסיס Car, מכאן שכל אחת ממחלקות אלו מכילה בתוכה את מאפייני מחלקת Car ועל כן, כל אחת ממחלקות אלו היא גם מחלקת Car.
עיקרון ה- Polymorphism מאפשר להתייחס לכל המחלקות שנגזרו ממחלקת בסיס מסוימת כמחלקות מאותו סוג, הלא הוא מחלקת הבסיס, ובהתאם לכך ליצור מבנה נתונים של אובייקטים מסוג מחלקת הבסיס אשר יכול להכיל את האובייקטים מהסוגים השונים – מחלקת הבסיס ונגזרותיה. יש לציין כי בסיוע של המרה (casting) מתאימה או מנגנונים מיוחדים נוכל להתייחס לאובייקט מסוים במבנה הנתונים בהתאם לסוגו האמיתי (המחלקה שהוא מייצג מופע שלה) ולא רק כאל אובייקט מסוג מחלקת הבסיס.
למשל, כאשר נתאר מחלקה של יונקים (מחלקת הבסיס) נשים לב בוודאי כי "אכילה" היא פעולה חיונית לקיומם. כלומר, כל סוגי היונקים (מחלקות נגזרות) חייבים לדעת לבצע את הפונקציה Eat, מאידך כל אחד מהיונקים מייצג מימוש שונה לפונקציה Eat – הרי כל אחד אוכל דברים שונים בדרכים שונות. נניח כי אנו מחזיקים רשימה מקושרת של יונקים מהסוגים השונים (אפשרי להחזיק מבנה נתונים של אובייקטים שונים אם להם מחלקת בסיס משותפת).
ריבוי צורות היא האפשרות לקחת עצם (אובייקט) מטיפוס יונק (אחד כלשהו מתוך הרשימה) ולהורות לו לאכול (Eat) כאשר אותו עצם יבצע את הפעולה המתאימה לו ביותר – כלומר, תופעל הפונקציה Eat שהוגדרה עבורו! זוהי טכניקת תכנות המבצעת את הפונקציה של המחלקה המתאימה.
מבנה השפה המאפשר את ריבוי הצורות הוא הכריכה הדינמית (Dynamic Binding),
כאשר כריכה דינמית (או מאוחרת) היא האפשרות לקשור בין קריאה לפונקציה כלשהי לבין מימוש כלשהו של הפונקציה בזמן ריצה ולא בזמן ה- Link.
אם נחזור לדוגמא קודמת אזי, כל המבנים ברשימה שלנו הם מטיפוס יונק, אך כל אחד הוקצה בזמן ריצה להיות יונק מסוג מסוים (אחת מהמחלקות הנגזרות), לכל אחד מהיונקים קיימת פונקצית Eat בעלת מימוש שונה. נניח ואנו מבצעים קריאה לפונקצית Eat של מבנה מסוים ברשימה– אם הכריכה לא היתה דינמית – אלא מתבצעת בשלב הקומפילציה – אזי היתה מופעלת הפונקציה Eat של מחלקת הבסיס, מאחר וזהו טיפוס מסוג יונק (בטרם הריצה לא ניתן לדעת איזה סוג יונק יוקצה עבור מבנה זה), בעוד שבכריכה דינמית הקשירה לפונקצית Eat המתאימה תתבצע בזמן ריצה – זמן בו כבר ידוע סוגו המדויק של המבנה ועל כן תופעל הפונקציה המתאימה.
בתרשים זה מתוארת היררכית המחלקות, כאשר בדוגמא זו התעלמנו ממאפייני כל מחלקה והמאפיינים המשותפים הנמצאים במחלקת הבסיס Mammal . ניתן לשים לב כי בכל אחת מהמחלקות מוגדרת פונקצית Eat בעלת מימוש שונה! (יש לציין כי במחלקת Mammal המימוש יכול להיות ריק או לא קיים כלל) .
נציג את הרשימה שהוזכרה בדוגמא לעיל, כאשר סוג טיפוס המצביעים מצביע ל- Mammal (כפי שהוסבר, כל המחלקות הנגזרות הן גם מסוג מחלקת הבסיס). כאשר, בזמן ריצת התוכנית כל אחד ממצביעים אלו הוקצה דינמית לטיפוס כלשהו מסוג המחלקות הנגזרות. ובזכות תכונת הכריכה המאוחרת (דינמית) המאפשרת את עיקרון ה- Polymorphism, כשנבצע קריאה לפונקציה Eat באמצעות צומת כלשהי ברשימה המקושרת – תופעל הפונקציה בהתאם לסוג ההצבעה (Cow/Person/Lion) ולא בהתאם לסוג המצביע (Mammal), משמע, מערכת זמן הריצה תבטיח קריאה לשיטה (פונקציה) מתאימה של האובייקט הנגזר.
יתרון חשוב נוסף של הרב צורתיות הוא שקוד ישן יכול להשתמש בקוד חדש. כלומר, גם אם נוסיף עכשיו סוג חדש של יונק שנגזר ממחלקת יונק, נוכל להכניס אותו לרשימה והקוד כולו ימשיך לפעול מבלי שהיה צורך לשנות אותו.
נסכם, עיקרון ה- Polymorphism מעניק את האפשרות להתייחס לאובייקטים מסוגים שונים באופן זהה, באם יש להם מחלקת בסיס משותפת. אפשרות זו יוצרת תוכניות מאד נוחות לקריאה, כתיבה ותחזוקה – ניתן להגדיר שיטות (פונקציות) שעל כל מחלקה לתת, ולהפעיל שיטות אלו על אובייקט מבלי לדעת מהו בעצם הסוג שלו. זוהי עוצמה תכנותית אדירה!
תגובות בפייסבוק