Why you should not use default parameters
אתחיל משאלה פשוטה, מה נמצא בראש רשימת הדברים שמעצבנים אתכם שאתם רואים קוד? תשובות נפוצות:
- חוסר יעילות
- שכפול קוד
- אין בדיקות
- אין תיעוד
ואוהו כמה זמן אנחנו עובדים כדי שהקוד שלנו יהיה יעיל, בלי שכפולי קוד, עם תיעוד טוב ועם בדיקות. אבל לפעמים יש שיטות לא נכונות למניעת שכפולי קוד, או לפחות מקומות בהם אנחנו נגרום ליותר נזק בטווח הרחוק.
נתבונן בקוד הבא:
class Something { public: Something(bool isInteresting_) : isInteresting(isInteresting_) { } private: bool isInteresting; };
ברוב המוחלט של הפעמים, אצטרך להעביר true, שכן אעבוד על דברים מעניינים, לכן אגדיר ערך ברירת מחדל:
Something(bool isInteresting_ = true);
כך לא אצטרך להגדיר כל פעם מחדש שאני עובד על משהו מעניין.
סיפורינו מתחיל אחרי שלוש שנות עבודה של מספר מתכנתים. כמובן שהם עבדו על הרבה מאוד דברים בשלוש השנים הללו, חלקם מעניינים יותר, חלקם פחות. רצתה יד הגורל (במקרה זה, היד של הבוס שלכם) למצוא את כל הדברים הלא מעניינים שעבדתם עליהם... סתם, רצו לדעת מהם אותם הדברים
"אין בעיה! תן לי שלוש דקות תמימות", כך שמעתי את איגור האומלל אומר לראש הצוות שלו, בדיוק לפני שהוא פרץ בבכי תמרורים. "מה קרה?" שאלתי את איגור האומלל (זה שם המשפחה שלו, הוא בעצם בחור די שמח). בין קינוח אף להתייפחות איגור ניסה להסביר שעבדנו על למעלה מ 1000 דברים שהשתמשו ב Something. עכשיו לך תדע איזה מהם מעניינים ואיזה לא.
אוקיי, להירגע, זה רק קוד, רובו לא נושך (בדרך כלל), נמצא פתרון.
רצינו לעשות שימוש ביכולת של ה IDE שנקראת find all references (כלומר תמצא לי את כל מי שמשתמש בבנאי..), קיבלנו אלף תוצאות, זה הזכיר לי תמונה ממש מעצבנת שראיתי פעם:
פתרון אחר שרצינו לנסות הוא לחפש ביטוי רגולרי בסגנון: Something.*false… התוכנית עבדה.. ish (כלומר עבדה-בערך).
- דבר ראשון שהיינו צריכים לעשות הוא לסנן סוגי הקבצים שאינם רלוונטים. אחר כך - לסנן הערות, בדיקות יחידה, סתם טקסטים שבמקרה התאימו וכו', זה לקח זמן.
- בעיה נוספת היא שהביטוי הרגולרי פשוט לא נכון, לדוגמה, הוא לא יעלה על המקרה הבא:
bool notInteresting = false; Something very(notInteresting);
טוב, אז בכל זאת, מה עושים? ובכן, קצת פסיכולוגיה. בני אדם הם אנשים עצלנים. מתכנתים, כידוע, הם זן מיוחד של אנשים, בלי לדעת יותר מדי ביולוגיה: מתכנתים הם עצלנים מאוד. מה זה אומר ולמה זה עוזר לנו? זה עוזר לנו בקטע מאוד חשוב, מתכנת כמעט תמיד יכתוב:
Something interesting;
במקום לכתוב:
Something interesting(true);
איגור יצא מגדרו, הוא ידע ישר מה לעשות. במקום להגדיר בנאי יחיד, יש להגדיר שני בנאים:
Something() : isInteresting (true) { } Something(bool isInteresting_) : isInteresting (isInteresting_) { }
הסיבה שאיגור האומלל היה כל כך שמח היא שעכשיו הוא יכול לשתמש ביכולת המובנית של ה - IDE שלו "find all references" על מנת למצוא את כל השימושים בבנאי השני. ואכן, איגור הצליח לצמצם את מספר השימושים ממספר אלפים למספר בודד של שימושים. תחשבו על התמונה למעלה, היא אותו הדבר, רק שהיו אומרים שאם בכלל קיים המספר 8, הוא בשורה הראשונה.
בכל אופן, שימוש בערכי ברירת מחדל חוסך מאיתנו סוג של שכפול קוד, אבל הקוראים יכולים לראות מה ערך ברירת המחדל. נובעות מכך הבעיות הבאות:
- זו לא הדרך הנקיה ביותר להצהיר על מה ערך ברירת המחדל יהיה (בשביל זה קיים תיעוד וקיימות בדיקות)
- בעיה נוספת, פרקטית יותר - כשנרצה לשנות את ערך ברירת המחדל, לא יספיק לנו לבצע מחדש לינקוז' (linkage) של הספריה שלנו, אלא נצטרך לקמפל מחדש גם את כל התלויות.
- הרבה פעמים, לפחות אצלינו, ערך ברירת המחדל נלקח מקובץ, ע"י ספרית util פנימית כלשהי. הקוד בערך כך (עם ערכי ברירת מחדל):
Something(bool isInteresting_ = FileConfigurer::GetDefaultInteresting());
עכשיו כל הקוראים צריכים להכיר את FileConfigurer ואת ה include שלו… זה לא דבר שהופך את איגור האומלל לשמח יותר, תשמחו עלי, ניסיתי.
בנוסף על כך, בגלל מגבלה של ++c, אם נרצה להוסיף פרמטר ללא ברירת מחדל לפונקציה, הוא יצטרך להופיע לפני הפרמטר עם הברירת מחדל. לא אכנס פה לפרטי פרטים אבל אציין שזה מעיק בסיטואציות מסוימות.
קצת בנושא פרמטרים מרובים, תחשבו על הפונקציה הבאה:
Something(bool isExciting = true, bool isWonderful = true, bool isTasty = true, bool isGr8 = true, bool isLovely = true);
אני יודע מה עבר לכם בראש, זו פונקציה שהופכת את איגור לאומלל. אתם צודקים. (אגב, אתם צודקים גם מעוד סיבות, המחלקה הזו כנראה עוברת על Single Responsibility Principle)
במקרה כזה, לא נרצה להוסיף פונקציות ככל שמספר הפרמטרים גדל (כי מספר הפונקציות גדל הרבה יותר מהר), מה גם שכמו בדוגמה הזו, זה לא ממש אפשרי (ניתן לייצר רק בנאי אחד שמקבל רק פרמטר בוליאני אחד, יותר מזה והקומפיילר לא ידע לאיזה בנאי לפנות)
הפתרון המוצע הוא להוסיף Parameter Object. במקרה שלנו, כך יראה הקוד (כולל שימושים)
class Something { public: struct Configurations { bool isExciting; bool isWonderful; bool isTasty; bool isGr8; bool isLovely; }; static Configurations getDefaultConfiguration(); Something(); Something(Configurations confs); private: Configurations; };
Something withDefaults; Something::Configurations myConf = Something::getDefaultConfiguration(); myConf.isTasty = true; Something withoutDefaults(myConf);