הכרנו פעם שעברה את המושג של מתודה וירטואלית, המאפשרת לאפליקציה שלנו להחליט בזמן ריצה לאיזו פונקציה לקרוא.
טעות נפוצה שיכולה להיווצר משימוש בפונקציות וירטואליות, היא קריאה להן בConstructor.
למה הכוונה?
הנה דוגמה קלאסית (ושכיחה!): נניח שיש לנו איזושהי פונקציה שמאתחלת לנו את אחד הMemberים של המחלקה. למשל, פונקציה שמאתחלת את הLogger של המחלקה שלנו:
|
|
כעת, אנחנו מעוניינים שמי שיורש מהמחלקה שלנו יוכל להחליף את האתחול של הLogger בפונקציה משלו.
הדבר הראשון שעולה לנו לראש היא להפוך את הפונקציהGetLogger לprotected וvirtual:
|
|
יופי, עכשיו כל אחד יכול לדרוס את הפונקציה הזאת ולעשות איתה מה שהוא רוצה, למשל:
|
|
נראה שהפתרון טוב, אלא שיש פה בעיה.
מה הבעיה? נניח שלמחלקה היורשת יש איזשהו פרמטר משלה, והלוגר מאותחל לפיו:
|
|
נראה תקין, נכון?
למרות זאת, מי שינסה להריץ את הקוד הזה, יגלה שאנחנו שולחים לBarkLogger את הערך null תמיד.
טוב, אז מה קורה פה?
כזכור, כאשר מתבצעת קריאה לConstructor של המחלקה שלנו, קודם מתבצעת הקריאה לאבות, והקריאה למחלקה שלנו מתבצעת אחרונה:
למשל, אם יש לנו את הירושה הבאה:
|
|
אז קודם כל יקרא הConstructor של object, אחר כך הConstructor של Animal, אז הConstructor של Dog, ולבסוף הConstructor של Labrador.
עכשיו, עצם העובדה שקראנו לפונקציה וירטואלית בConstructor של Animal, גורם לכך שנכנסנו אליה, לפני שבכלל נכנסנו לConstructor של Dog, ולכן הMemberים של Dog אינם עדיין מאותחלים, ולכן תמיד נקבל בmBark את הערך הדיפולטי.
מה אפשר לעשות?
האופציה הראשונה היא להשאיר את זה ככה – זו אופציה שמומלצת רק אם שאר האופציות לא תופסות, ורק אם המחלקה הנ"ל לא אמורה להידרס ע"י מפתחים אחרים (כלומר היא סוג של מחלקה פנימית). ברגע שהמחלקה היא תשתיתית ומשתמשת הרבה מפתחים, האופציה הזאת לא טובה – כיוון שיש סיכוי גבוה שהם יתקלו כל פעם מחדש בבעיה כזו.
האופציה השנייה היא לפעפע את הפרמטרים כלפי מעלה. למשל, במקרה שלנו, לדאוג לכך שהConstructor של Animal יקבל ILogger בConstructor. זה יראה בערך כך:
|
|
הפתרון מונע קריאה לפונקציות וירטואליות בConstructor, אבל הוא יותר מסובך, אנחנו קוראים לפונקציה (או Constructor אחר) בקריאה לbase. (ראו גם טיפ מספר 88)
בד"כ זה לא כל כך פשוט להעביר את הפרמטרים למעלה.
האופציה השלישית היא ליצור פונקצית Init, שתדאג לאתחל את האובייקט:
|
|
ואז לאתחל את האובייקטים שלנו עם איזשהו Factory:
|
|
עם מימוש כזה:
|
|
כאן AnimalFactory היא מחלקה פנימית של Animal, אז היא יכולה לגשת לפונקציה הprotected ששמה Init.
ואז יש איזשהו Property שחושף את הFactory הזה, שאנחנו יכולים להשתמש בו:
|
|
אמנם אין יותר קריאות וירטואליות בConstructor, ואין סכנה שניגש לMemberים של המחלקה לפני שאותחלו, אבל יש פה את החסרונות הבאים: כל היצירות של אובייקטים חייבות לעבור דרך הFactory הנ"ל. על כן, גם לכל המחלקות היורשות חייבת להיות אותה חתימה של Constructor (במקרה שלנו – Constructor דיפולטי). כך שהפתרון מסרבל את השימוש במחלקה שלנו.
פתרון רביעי הוא לוותר באופן כללי על הקריאה של פונקציה לפונקציה הוירטואלית בConstructor, ולדאוג שכל אחד יאתחל את הLogger בעצמו. היתרון בפתרון הזה הוא שהוא פשוט. החסרון בו הוא שהLogger מאותחל מספר פעמים (בכל Constructor הוא מאותחל ונדרס).
מאוד חשוב לשים לב לקריאה לפונקציות וירטואליות בConstructor. הReSharper מזהיר על כך.
בנוסף לדוגמאות שכתבתי, אתם עשויים להיתקל בבעיות כאלה כאשר תיצרו Mockים לאובייקטים שלכם לUnit Testים.
יום וירטואלי (אבל שלא נקרא מהConstructor) טוב!