בואו נבחן את הנושא לעומק.
עקרונות תכנות מונחה-עצמים
תשאלו כמה אנשים שאתם סומכים עליהם מהם עקרונות ה OO, ואם מזלכם דומה לשלי, קרוב לוודאי שהם יתחילו להסתבך באיזו פינה אפלה של מימוש ה messaging בין אובייקטים. דיי מפתיע, אך נראה שרוב אנשי התוכנה שעוסקים ב OO לא יודעים להסביר מהם עקרונות ה OO.
נקודה בהולה לתשומת-לב האוניברסיטאות? (כנראה שלא… מבחינתם העיקר שתדעו משוואות דפירנציאליות חלקיות ואוטומאטים).
אז בואו נעשה קצת סדר: בצורה דיי עקבית, בספרי תכנות בעברית (למי שיצא לעיין), מופיעים שלושת העקרונות בסדר הבא:
- הורשה (inheritence)
- ריבוי-צורות (polymorphism)
- הכמסה (encapsulation) – או כימוס, כפי שהעירו. תודה.
זהו בדיוק סדר החשיבות ההפוך של עקרונות ה OO!
אין לי מושג מה הסיבה לסדר ההפוך. אולי ספר אחד חטא וכל השאר העתיקו. בכל מקרה: הנה סדר החשיבות הנכון:
- הכמסה (private)
- ריבוי-צורות (implements / instanceof / interface / upcasting / downcasting)
- הורשה (extends / protected / super / @Override)
(בסוגריים ציינתי את הכלים בג’אווה המשרתים כל עקרון. #C הוא דומה למדי).
העיקרון החשוב ביותר בתכנות מונחה-עצמים הוא הכמסה. אובייקטים הם יישויות עצמאיות המכילות מצב (state) ופונציונליות (methods) כאשר חלק מהמצב ומהמתודות שלהן הוא פרטי. פרטיות זו היא המפתח לפיתוח מערכות גדולות. שינוי של חלק פרטי אמור לא-להשפיע על החלקים האחרים של המערכת. אם מפתחים של חלקים אחרים של הקוד לא יודעים כיצד מומש החלק הפרטי (הם יכולים להסתכל בקוד, הם זקוקים לקצת משמעת עצמית וקומפיילר שיקשה עליהם) – אזי הם אמורים להיות לא מושפעים משינויים בו.
זוהי קפיצת המדרגה העיקרית מול שפות פרוצדורליות שניהלו את המידע בצורה גלובלית (נאמר C).
ומה עם ריבוי צורות והורשה?
עקרונות אלו הם כלים המאפשרים לכתוב קוד מתוחכם יותר. במשך שנים לא הייתה בניהם הבחנה ברורה. אולי בגלל שפת ++C שאפשרה ריבוי צורות כמקרה פרטי של הורשה, ולא להיפך.
הסבר קצר: שפת ++C הכילה יכולת מקבילה ל abstract בג’אווה / #C שנקראה virtual. מפתחים יכלו להגדיר מחלקות שכל המתודות שלהן virtual (מה שנקרא pure virtual class) וכך לקבל באופן עקיף משהו מקביל ל interface בג’אווה / #C. לא היה לממשק, כפי שאנו מכירים אותו בשפת ג’אווה, שום ייצוג רשמי בשפה – זה היה [1] Pattern.
האבחנה הברורה בין ריבוי-צורות והורשה הייתה לנחלת הכלל רק כאשר הגיעה ג’אווה, שפה שלקחה את עקרונות ה OO צעד אחד קדימה והגדירה אלמנט חזק בשפה לממשק, הרי הוא ה interface.
אט אט, שמו לב עוד ועוד מפתחים שב interface הם משתמשים המון והוא עובד מצוין, אבל בהורשה הם משתמשים פחות – ויש עם הורשה כמה בעיות.
מה הבעיה עם הורשה?
התובנה על המגבלות/בעיות בהורשה לא נתגלו עם ג’אווה. עוד בשנת 1992 הציג סקוט מאיירס, בספרו האלמותי “++Effective C” מספר בעיות עם הורשה והזהיר: “use inheritance judiciously”. שנתיים אחר-כך, בספרם המפורסם של GoF, הדבר נאמר יותר במפורש: “Favor object composition over inheritance” – נדבר על כלל זה בהמשך.
הבעיה העיקרית עם הורשה היא שהיא פוגעת בהכמסה. אם הכמסה הוא העיקרון החשוב ביותר, והורשה הוא החשוב פחות – די ברור מכאן מה צריך לעשות. יתרה מכך, להורשה יש אלטרנטיבה לא רעה. היא דורשת הקלדה של קוד נוסף – אך היא איננה מערערת על ההכמסה. כלומר ניתן להיפטר לחלוטין מהורשה ולכתוב קוד OO מצוין.
רוב הסיכויים שמפתחים ותיקים ומנוסים ירצו עדיין להשתמש בהורשה [2] ועבור מפתחים צעירים (הייתי אומר 5 שנים או פחות) – מומלץ לדסבלה (disable it).
הורשה פוגעת בהכמסה: שדות או מתודות המסומנות protected נגישות לכל מי שיורש ממני ואין לי מושג מה יעשו איתם. התחזוקה של מחלקת האב הופכת למורכבת.
הורשה פוגעת בהכמסה: נוצר coupling לא מפורש בין המחלקה היורשת למחלקת האב, כך ששינויים במימוש אצל האב, גם באזורים שמוגדרים private, עלולים להשפיע על המחלקה היורשת. הנה דוגמה:
הורשה פוגעת בהכמסה: אם מחלקת האב הכריזה על Serilazible, Clonable, Entity וכו’ – המחלקה היורשת יכולה לשבור הגדרה זו בכל רגע בלי יכולת להכריז על ההיפך. יותר גרוע: ירשתי ממחלקה שלא הייתה Serializable ולאחר שנה מחלקת האב הפכה לכזו – הקומפיילר לא יאמר לי דבר. הבעיה תשאר בעינה.
נו… אני מקווה שהבהרתי את הנקודה.
מה הפתרון? הפתרון הוא להשתמש בקשר הרכבה בין אובייקטים, מה שנקרא Composition. הוא כ”כ נפוץ ושימושי כך שהוא קיבל סימון משלו ב UML. אני אמנע מלכתוב על נושא שכבר תועד רבות. למי שמתעצל לחפש, הנה מקור קצת ישן, אך שנראה טוב:
http://www.artima.com/designtechniques/compoinh.html