תזכורת להשתמש ב SLAP כאשר אתם כותבים קוד….

בפוסט הזה להכיר (או להזכיר) עקרון חשוב בכתיבת קוד, הנקרא SLAP.

Single Layer of Abstraction Principle (בקיצור SLAP) – הופיע לראשונה בספר Smalltalk Best Practice Patterns של קנט בק (כן, שוב הוא…). העיקרון אומר שפונקציה צריכה להכיל ביטויים באותה רמת-הפשטה.

פירוש הגדרת ה SLAP

בשפה יומיומית ניתן לתאר את SLAP כעיקרון הבא: מג״דים יושבים עם מג״דים, מ״פים יושבים עם מ״פים, וחפ״שים יושבים עם חפ״שים. אם בפורום המג״דים יושבים חפ״שים – בסבירות גבוהה הם לא יתרמו לדיון, כי היא חושבים ברמת פרטים מפורטת בהרבה. החפ״שים יגררו את הפורום לעסוק בפרטים שלא נכון לפורום לעסוק בהם.
למשל:
מג״ד א: ״פלוגה 1136 תזוז צפונה ותתפוס את ציר בנימינה״
חפ״ש: ״שנייה, הגי׳פ שלנו כל הזמן חורק. אולי כדאי לקחת אותו לטיפול לפני?״.
זו הכוונה.
מי הם בקוד המג״דים? מי הם החפ״שים? את זה אנחנו צריכים לבדוק ולהבין.

דוגמה

הנה פונקציה (ממערכת אמיתית), שתשמש כדוגמה. קראו אותה עד הסוף:
הפונקציה אכן ארוכה. במקור היא ארוכה אפילו יותר – קיצרתי אותה לצורך הקריאות.
יש כלל שאומר שפונקציה לא צריכה להיות יותר ארוכה מ 25/10/5 שורות (גרסאות שונות של הכלל מציינות מספר מקסימלי שונה של שורות בפונקציה) – היא לא עומדת בכלל הזה בכלל.
אני מאמין שפונקציות טובות הן קצרות, אך לא נכון לספור שורות. לעתים יש פונקציות בנות עשרות שורות שקל מאוד להבין אותן.
SLAP ו "רכיבים עמוקים, ממשקים רחבים״ הם כלים עדיפים בעיני – בכדי להגיע לפונקציות קצרות, מאשר לספור שורות. אם הפונקציה ארוכה ופשוטה (כלומר: עומדת בכללים הללו) – אז הכל טוב.
עברו על הפונקציה עכשיו, ונסו לסמן לעצמכם לעצמכם אלו שורות הן מג״דים, אלו הם מ״פים, ואלו הם חפ״שים, מה הן רמות ההפשטה השונות שהפונקציה מטפלת בהן. עשו זאת לפני שאתם ממשיכים לקרוא את הפוסט 🙂
לא אענה אלו שורות הן לדעתי ברמות הפשטה שונות, אלא דווקא אקפוץ למצב הסופי של יישום עקרון ה SLAP – ואראה כיצד הוא יכול להביא את הקוד שלנו למקום טוב יותר.
הכלי העיקרי לתיקון פונקציה עם רמות הפשטה שונות הוא Extract Method Refactoring. הנה התיקון שלי:
סימנתי ב:
  • צבע תכלת – מג״דים
    •  => flow logic, קוד שמנהל החלטות גדולות ב flow המערכת.
  • צבע כחול – מ״פים
    • => פעולות טכניות ברמת הפשטה גבוהה: גישה לבסיס נתונים או מערכות צד-שלישי, הפעלה של לוגיקה עסקית ממוקדת (במקרה הזה: ולידציה), וכו׳.
  • צבע כחול עמוק – חפ״שים
    • => פעולות לוגיות בודדות ("one liners״), control flow בסיסי, כתיבת לוגים, וכו׳.
אתם בוודאי מסכימים שהפונקציה קצרה וקריאה יותר. עכשיו היא נראית יותר כמו ״סיכום מנהלים״ – שקל לקרוא ולעקוב אחריו. כל אחת מהפונקציות שהוצאתי: ()startFlowTypeI ו ()startFlowTypeII, וכו׳ – צריכה לשמור על SLAP בעצמה, ואם לא – אוציא ממנה גם עוד פונקציות.
ברמת המטאפורה: פיצלנו את דיון המג״דים לדיוני משנה בפורום המ״פ – וזו פרקטיקת ניהול טובה.
בשלב הזה אתם אמורים להרגיש חוסר נוחות מסוים עם מה שאני אומר, גם אם אתם חושבים שה Refactoring היה מוצלח: למה לעזאזל עדיין יש חפ״ש בפורום המג״דים? איך נכנסו לשם מ״פים? – לא אמרנו שזה לא צריך לקרות?
התשובה הקצרה, היא ש SLAP הוא בסה״כ guideline. לא כדאי להיות קנאים.
התשובה הקצת יותר ארוכה, היא שבעיקרון אי אפשר ולא נכון לממש את SLAP עד סופו. טעינה של אובייקטים מה DB היא שכבת הפשטה נמוכה יותר מהרצה של flows – אך אנו זקוקים למידע הזה כדי להמשיך את הפונקציה.
טיפול בשגיאות או לוגים, היא גם לוגיקה דיי ״נמוכה״ – אבל נדרשת כמעט בכל פונקציה. אפשר לחשוב על זה שגם פורום המג״דים רוצים חפ״ש שיסכם את הפגישה. אם היו עשרה חפ״שים בפגישה – זה כבר היה יותר מדי.
אם ננסה לשמור על SLAP ב 100% – סביר שהקוד שלנו יהפוך לקריא פחות. חשוב למצוא את נקודת האיזון הבריאה.

מורכבת נוספת ביישום SLAP

שימו לב שהשינוי בפונקציה לא היה רק פעולות Extract function, אלא גם גם שינוי עמוק יותר למבנה. במקום להחזיר ערכי Status שונים של כישלון מגוף הפונקציה – הפכתי את הדיווח על שגיאות ל Exception ותפסתי אותו בפונקציה הראשית. ה try..catch לא היה בפונקציה המקורית.
החזרה של ערך מתוך גוף הפונקציה לפעמים היא קריאה יותר – אך היא מקשה מאוד על Extract method refactoring – כי הקוד שיצא לפונקצית-משנה לא יכול פתאום לשלוט בערך ההחזרה של הפונקציה הראשית.
לעתים מבנים מסוימים של control-flow בפונקציה מגבילים אותנו מביצוע Refactoring או כתיבת קוד פשוט. לא בבית ספרנו!
שימוש ב Exception הוא לא הפתרון היחיד, הוא זה שהעדפתי במקרה הזה.
הוספתי טיפוס חדש ופרטי של Exception בשם ValidationException. הוא ישמש אותי גם בפונקציות ()startFlowTypeI ו ()startFlowTypeII. הגדרתי טיפוס חדש כדי שאוכל ב catch להבחין בבירור בינו לבין שגיאה ממקור אחר.
היתרון ב Exception הוא שאני יכול להשתמש בו בפונקציות בקינון עמוק יותר, באותו האופן בדיוק.
הפונקציות יצאו קצרות לא בגלל שהקפדנו על אורך הפונקציה במספר שורות, אלא בגלל ששמרנו על רמת הפשטה אחידה.

סיכום

במהלך הפוסט נצמדתי לדוגמת-קוד קונרטיות, ובשאיפה שקל ״לחוש״ אותה.
התיאוריה של SLAP גורסת שמעבר בין רמות הפשטה שונות גוזלת משאבים קוגניטיביים מהמתכנת – ולכן אנו רוצים:
  • להימנע ממעבר תדיר בין רמות הפשטה שונות באותו שטף קריאה של קוד => באותה הפונקציה. שמירה על רמה אחידה של רמת הפשטה מקלה על הקריאה וההבנה של הפונקציה.
  • להשתדל שהמעבר בין רמות הפשטה שונות יהיה בקריאה לפונקציות-משנה. המעבר מפונקציה לפונקציה הוא אינטואטיבי לשינוי רמת הפשטה.
מקווה שזה הגיוני, ומקווה שתעזרו ב SLAP על מנת לכתוב קוד פשוט יותר, ואינטואטיבי יותר.
שיהיה בהצלחה.

פשוט קוד: טיפול בשגיאות

אני עומד לעסוק בנושא בסיסי מאוד: Exception Handling. בסיסי – אך שיש מה לעסוק בו.

כסיפור רקע לדיון בנושא, נניח שאני כותב קוד שעוסק בלקוחות. במצב מסוים קיבלתי מזהה לקוח שאינו קיים.
כאשר מתודה נכשלת לבצע את מה שמוטל עליה – עליה להחזיר סטטוס מתאים, או לזרוק Exception. אכן אני רוצה לזרוק Exception בכדי לעצור את ה flow (שלא יכול ממש להמשיך) ולדווח לאלו שהפעילו אותי – על התקלה.

הערה: אני מניח שלא מדובר במערכת Embedded  או Realtime שם נמנעים מ Exception בשל העלות של stack unwinding. בשפות / מערכות אחרות (כמו Golang) – החליטו לא להשתמש ב Exceptions על מנת להיות צפויים ומבוקרים יותר. למשל: כאשר Exception נזרק ממקבץ של 4 שורות קוד – אני לא יודע בוודאות איזו שורה זרקה את ה Exception, ואולי אף איני לצפות את ה Exception שעלול להיזרק מכל שורה – מה שיוביל אותי לטיפול כללי ופחות מדויק בחריגה.

בדוגמה אני אכתוב מעל ה JVM, בשפת קוטלין, ולכן ארצה להזכיר לרגע את מבנה השגיאות ה JVM:

  • שפת ג'אווה מתייחסת בשונה ל Checked Exceptions (היורשות מהמחלקה Exception) – אשר יש חובה קוד להתייחס אליהן, ו Unchecked Exceptions (היורשות מהמחלקה RuntimeException) המתנהגות כמו Exceptions ברוב שפות התכנות, ואפשר להתייחס אליהן – או להתעלם ואז הן יעברו לטיפול מי שקרא לי.
    • הרעיון החדשני והמעניין של Checked Exceptions (המושפע מרעיון בשם checkers בשפת OCaml) הוכר בפועל כהחמצה, ולא הועתק (למיטב ידיעתי) לשום שפת תכנות נפוצה אחרת.
    • גם בשפת קוטלין "ירדו" מהרעיון, ואין צורך להתייחס אחרת לחריגות היורשות מהמחלקה Exception.
  • Error הוא ענף אחר בהיררכיית ההורשה, ושגיאה (Error) שתיזרק לא תיתפס כחריגה (Exception) – כי היא מחלקה מטיפוס אחר. ה Error נשמר למצבים חמורים שהאפליקציה לא אמורה לטפל בהם – ובעיקר נזרקים ע"י ה JVM (כגון OutOfMemoryError). לא זכור לי שאי פעם נתקלתי ב Error בפרודקשיין, בטח לא כזה שקוד היה יכול למנוע / להתמודד איתו.
    • IntellJ (ה IDE) מימש משפטי TODO (כאלו שלא נכתבים כהערה) ככאלו שיזרקו Error. אם שכחתם לכתוב את הקוד שהתכוונתם אליו – ואתם מריצים את הקוד, אנחנו לא רוצים שהפרט הזה ייתפס כ Exception ויכתב לשורה ללוג אשר קל לפספס. הנה שימוש נכון ל Error.
 
חזרה לסיפור שלנו: אז איזה סוג של Exception כדאי לזרוק?
  • ברור לי שזה לא Error. (למרות שנתקלתי במקרים בהם אנשים זרקו Errors ב flows אפליקטיביים).
  • אני לא מתעסק בהבחנה בין Checked ל Unchecked Exception – וטוב שכך.
  • יש מן עצה כזו שאומרת "Favor the use of standard exceptions", אך עדיין נותר לי מבחר גדול של Exceptions רק מתוך הספריות הסטנדרטיות של ג'אווה:
הרשימה באמת, היא אפילו יותר ארוכה
 
אני רוצה לכתוב קוד טוב יותר וקריא יותר – אבל איך בוחרים?
 
צעד הגיוני ונפוץ יחסית, הוא ללכת ולהתייעץ בכל מיני "מילונים" לסוגי החריגות השונות, ומתי להשתמש בהן:

הניסיון בפועל, מראה שהמילונים האלה לא ממש עוזרים להיות עקביים. למשל במקרה שלנו, מזהה-הלקוח שלא נמצא, ייתכנו פרשנויות שונות של מפתחים באיזה סוג Exception להשתמש:

  • יש כאלו שיבחרו ב IllegalArgumentException – כי הפעילו את המתודה עם ארגומנט לא חוקי.
  • יש כאלו שיבחרו ב IllegalStateException – כי בכלל ארגומנט לא חוקי לא אמור להגיע כך ללב המערכת. זו תקלה במצב המערכת. "אם הגענו עד לכאן – זו כבר תקלה ב state".
  • יש כאלו שיבחרו ב NotFoundException – באמת לא מצאתי את הלקוח הזה.
  • יש כאלו שיבחרו ב UnsupportedOperationException – זה לא חוקי לשלוף פרטי לקוח עם מזהה לא נכון (?!).
  • יש כאלו שפחות מתאמצים ופשוט זורקים RuntimeException (החריגה ארעה בזמן ריצה) או פשוט סתם Exception.
  • יש כאלו שיותר מתאמצים ומגדירים טיפוס חדש של חריגה למצב הספציפי, למשל: CustomerNotFoundByIdException. 
  • אפילו ראיתי מקרה בו בתסריט כזה נזרק SecurityException. אני מניח שטיפול ב id שגוי של משתמש הוא גם בעייתי בהיבטי האבטחה של המערכת.

אם מפתחים שונים בוחרים חריגות מטיפוסים שונים לאותו המצב, אז:

  1. הקוד שלנו לא ממש אחיד => מה שמפחית במעט את קריאות הקוד.
  2. אם אני רוצה לתפוס את המקרה כ Exception – אני לא באמת יודע לאיזה Exception לצפות, ויותר גרוע: אני יכול לתפוס את סוג ה Exception – שנזרק ע"י קטע קוד אחר שהמתכנת שלו חשב שזה טיפוס ה Exception הנכון ביותר לתאר אותו.

אם חשבתם אולי לשלוח "שק" של Exceptions שיתאר בצורה רחבה יותר את מה שקרה – אנא חשבו על זה שוב. זה רעיון לא מוצלח…. >> אז מה עושים? כיצד אתם בוחרים באיזה סוג Exception להשתמש? איך קבוצת הפיתוח בה אתם חברים, כקבוצה, מקבלת כזו החלטה? (החלטות הן לא תמיד מסונכרנות)

בחזרה למהות

בואו נזכר בתוצאה של זריקת Exception. מדוע בעצם אנחנו זורקים אותן? מה אנחנו רוצים שיקרה?

אנו זורקים Exceptions כיוון שמשהו השתבש, ואנחנו לא יכולים, או לא נכון לנו להמשיך את ה flow. למשל: אם אנו מגלים שאנו עומדים ליצור חוסר-עקביות (inconsistency) בבסיס-הנתונים, כנראה שעדיף לנו לעצור עכשיו מאשר ליצור בעיה עתידית, קשה יותר לזיהוי ולתיקון.

אם אנחנו יודעים להתאושש מהתקלה (למשל להשתמש ב default הגיוני לפרמטר שחסר) – אז אנחנו פשוט ממשיכים. אין צורך לזרוק Exception.

מה התוצאה של זריקת ה Exception, מצד הלקוח שקרא לפונקציה שלנו? נסכם בפשטות את האפשרויות:

  1. Rollback של טרנזקציה / פעולה, או פעולות cleanup – זה קורה לפעמים.
  2. כתיבת הודעת לוג (שגיאה / אזהרה) – בכדי ללמוד עליה ולטפל ידנית במצב ו/או לשפר את המערכת שמצב זה לא יקרה בשנית. את זה עושים כמעט תמיד.
  3. הצגת אינדיקציה למשתמש-הקצה ב UI, ברמת פירוט כזו או אחרת, שמשהו השתבש – אם קיים משתמש כזה.

זהו. זו בגדול התוצאה.

ברוב המקרים – הטיפול האמיתי ב Exception יהיה מעבר ל Flow בקוד:

  • המשתמש יראה את הודעת השגיאה וינסה שוב (אולי מדובר ב timeout?). אולי ינסה שוב עם קלט מעט אחר.
  • המתכנת יחקור את התקלה. אולי יש לבצע טיפול ידני? אולי נשפר את הקוד כך שתקלה זו לא תקרה בעתיד. זה לרוב קורה שעות אחרי שה Exception נזרק.

הנה קוד לדוגמה שתופס Exception:

try {
...
} catch (e: Exception) {
  logger.error("something went wrong $e")
}

למה בעצם משנה איזה טיפוס של Exception נזרק? – הקוד יפעל באופן זהה עבור כל הטיפוסים.
למה לנו לבחור בטיפוס ל Exception  – אם זה הקוד, וזו התוצאה?

היי, רגע!

אני מקווה ששמתם לב שמשהו מאוד לא בסדר בדוגמת הקוד שהרגע סיפקתי. כמה משהו:

  • לא שלחנו את ה exception עצמו ל logger. זה אומר שלא יודפס ה stack-trace – וזו בעיה חמורה, כי יהיה קשה מאוד להבין מה השתבש בחקירה שתגיע מאוחר יותר.
    • אני מבקש בזאת מכותבי ה logging framework הבא לג'אווה שהחתימה למתודה (…)logger.error תחייב לספק ארגומנט מסוג ?Exception. השכחה לשלוח את ה Exception ללוגר היא מספיק מזיקה. אם אנו רוצים לכתוב ללוג error שלא קשור ל Exception – אני מעדיף שנשלח ערך null.
  • בלענו את ה Exception. האם זו הייתה הכוונה?
    • לפעמים כן – וזה בסדר. מספיק שכתבנו ללוג (אבל נוסח ההודעה יהיה יותר כמו "failed to do…") ומישהו יטפל בזה אח"כ.
    • לפעמים לא – וזו יכולה להיות ממש תקלה. 
      • גם במקרה כזה הייתי שמח אם ברירת המחדל של השפה הייתה להמשיך ולזרוק את השגיאה, עם אפשרות להוסיף משפט break אולי swallow או משהו – במקרים בהם לא רוצים במודע להמשיך לזרוק את השגיאה.
  • הדפסנו את האובייקט של ה Exception – ולא את הודעת השגיאה (e.message).
    • ברוב הפעמים, מימוש ברירת-המחדל ל ()toString של מחלקת ה Exception יהיה message – ואז הכל בסדר.
    • במקרים בהם המימוש הוא אחר, למשל: כתובת האובייקט בזיכרון – איבדנו את ההזדמנות לאסוף מידע שימושי לגבי השגיאה.

בואו נסתכל בדוגמת קוד אחרת, יותר טובה (אך עדיין בעייתית) ומאוד נפוצה:

fundoA() {
try {
  ...
  doB()
  ...
  } catch (e: Exception) {
    logger.error("something went wrong: $e.message", e)
    throw e
  }
}

fundoB() {
try {
  ...
  doD()
  ...
  } catch (e: Exception) {
    logger.error("Dubi: something din't work as expected: $e.message", e)
    throw e
  }
}

fundoD() {
try {
  ...
  } catch (e: Exception) {
    logger.error("doD: WTF?! $e.message", e)
    throw e
  }
}

קרתה שגיאה אחת, אבל בלוג יכתבו 3 stack trances שעשויים להראות כמו 3 שגיאות. ה stack traces הם כמעט חופפים, כאשר בכל פעם נוספת רק עוד שורה אחת.
אפשר בקלות לבזבז זמן בחקירה של הלוג שנכתב ע"י דודי (כלומר: doD) או הלוג שנכתב ע"י דובי (doB) – בעוד חסר לנו ההקשר החשוב שהתחלנו לפעול בעצם מ doA.

בגדול:

  • ב catch clause בחרו: או שאתם מדפיסים הודעה ללוג – או שאתם זורקים שגיאה הלאה.
    • כל Framework / WebServer שמכבד את עצמו יתפוס את כל השגיאות שהתרחשו – ויכתוב אותן ללוג.
    • כתיבת ה Stack trace ללוג ברמה הגבוהה ביותר – תספק את מירב המידע, ולכן זו השגיאה שאנו רוצים לחקור. חבל לכתוב הודעות כפולות ללוג.
  • הערת משנה: אם יש stack trace אז יהיה כתוב איזה פונקציה נקראה. עדיין המון אנשים אוהבים להוסיף בעצמם את שם המתודה (ולפעמים גם את שם המחלקה) להודעת השגיאה – וזה דיי מיותר. נחשו מה קורה כאשר עושים rename לשם המתודה?
    • בהודעת השגיאה חשוב לציין מידע חדש ומעניין. למשל: פרמטרים מסוימים של הריצה – שלא תהיה לנו גישה אליהם מאוחר יותר.

הנה דוגמת הקוד הזו בגרסה הטובה יותר שלה:

fundoA() {
try {
  ...
  doB()
  ...
  } catch (e: Exception) {
    throw Exception("failed ... id=$id", e) // = java's "new Exception"
  }
}

fundoB() {
  ...
  doD()
  ...
  // if you have nothing smart/new to say - spare our time.
}

fundoD() {
try {
  ...
  } catch (e: Exception) {
    throw Exception("Failed DoDing: x=$x, y=$y", e)
  }
}

ה Framework הוא זה שיתפוס את ה Exception ויכתוב אותה, ואת ה stack trace המלא – ללוג. זה ה Best Practice שנקרא global exception handling.

שווה לציין, שב Web Frameworks ההתנהגות המקובלת היא שה Framework תופס את ה Exception ומוציא החוצה הודעה לקונית בנוסח "HTTP 500 internal server error". אנחנו לא רוצים לשלוח ברשת payload גדול של כל ה stacktrace, ומבחינת אבטחה אנחנו לא רוצים לחשוף החוצה את ה internal של המערכת שלנו וללמד את התוקף הפוטנציאלי מה קורה. לכן, כברירת-מחדל, הודעות השגיאה ששקדתם עליהן יגיעו ללוג – ולא ללקוח.

בואו נחזור לשאלה האחרונה ששאלנו:

למה בעצם משנה איזה טיפוס של Exception נזרק? – הקוד יפעל באופן זהה עבור כל הטיפוסים.
למה לנו לבחור בטיפוס ל Exception  – אם זה הקוד, וזו התוצאה?

ראיתי הרבה קוד בחיים, וברובו אנחנו פשוט תופסים "Exception". אם כך המצב – מדוע אנחנו מתעסקים בבחירה של "טיפוס ה Exception" אותו נזרוק?!

התשובה

כמו יהודי טוב, אענה בשאלה. את השאלה שאנחנו צריכים לשאול את עצמנו כל פעם שאנו עומדים לבחור בסוג מיוחד של Exception:

מי הלקוח שעומד לתפוס את החריגה מהטיפוס המסוים, ומה הוא עומד לעשות עם המידע הנוסף שהטיפוס הזה סיפק לו?

זו שאלת המפתח, שלצערי כמעט ולא נשאלת.

אם מי שעומד לתפוס את ה Exception הוא ה Framework בכדי לכתוב אותו לוג – אין כל טעם לשגר Exception מטיפוס מיוחד.
אם מי שעומד לתפוס את ה Exception הוא קוד, בכדי לבצע באופן גנרי rollback / resource cleanup – אין כל טעם לשגר Exception מסוג מיוחד.

המצב היחידי שבו יש טעם לשגר שגיאה מסוג מסוים – הוא אם מישהו יחכה לסוג הספציפי של ה Exception.

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

אני אצטט לרגע אנשים שחקרו את הנושא עמוק ממני:

"Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result – decreased productivity and little or no increase in code quality." — Joseph R. Kiniry

ובכן זה התסריט המשמעותי היחיד לשימוש בטיפוס ספציפי של Exception:

try {
...
} catch (e: CustomException) {
  // do some cleanup / revert logic
} catch (e: Exception) {
  throw Exception("Failed DoDing: x=$x, y=$y", e)
}

כאשר קוד מתייחס לטיפוס הזה, ומריץ איתו branching אחר של הקוד.

חשוב לציין ש cleanup logic צריך ברוב הפעמים לשבת ב finally clause – ולרוץ ללא קשר לסוג התקלה שהתרחשה.

התקדמנו.

במקרה המסוים שלקוד אכפת הטיפוס של ה Exception, באיזה טיפוס כדאי להשתמש? מהו בעצם ה CustomException?

נחזור רגע להמלצה המוכרת: "Favor the use of standard exceptions".
שימו לב שאם CustomException יהיה Exception סטנדרטי כמו IllegalArgumentException אזי הוא יוכל להיזרק לא רק מהקוד שלנו – אלא מכל קוד אחר במערכת שבחר "להשתמש ב Standard Exceptions". אולי אלו 3rd Party, אולי גם הספריות הסטנדרטיות של ג'אווה.

בעצם, השימוש ב Standard Exception חושף אותנו לחוסר ודאות לגבי מקור ומשמעות ה Exception – וזה לא דבר טוב.

לכן: כאשר אתם רוצים לתאר מצב יוצא דופן, שיוביל לטיפול מיוחד – הגדירו מחלקה חדשה של Custom Exception, או לפחות מחלקה ידועה, בה אתם משתמשים במזהה מיוחד (למשל: קוד שגיאה ייחודי).

מתי, אם כן, אפשר להשתמש ב Standard Exception?

  • כאשר אתם רוצים לייצר הבחנה בין שגיאות שונות של הפונקציה שלכם. אם יש רק מצב-שגיאה אחד משמעותי – מספיק להשתמש פשוט ב Runtime)Exception).
    • חשוב מאוד לתעד מה המשמעות של כל Exception ספציפי שנזרק. בלי תיעוד – זה תרגיל ב fuzzy human communication ביניכם למי שעומד לכתוב את הקוד שיטפל בשגיאה.
  • כאשר קהל היעד שלכם לא ידוע (נניח: אתם כותבים ספריה סטנדרטית), ואתם לא יודעים איך המשתמשים עומדים להשתמש ב Exception שזרקתם , או שיש מגוון רחב של אפשרויות תגובה.
  • כאשר אתם משוכנעים שאתם הם אלו שעומדים לזרוק את השגיאה הזו, ולא פונקציה אחרת שאתם קוראים לה!

שווה לציין שגם הספריות הסטנדרטיות של ג'אווה אינן עקביות לגמרי ב Exceptions אותן הן זורקות, או אפילו לגבי השימוש ב Checked ו Unchecked Exceptions.

השאלה היותר חשובה, היא מתי לתפוס Standard Exceptions?

  • כאשר אתם קוראים למתודה שמצהירה שהיא עשויה לזרוק את ה Exception מהטיפוס הזה, וברורה וחשובה לכם המשמעות הסמנטית של סוג השגיאה הזו.
  • כאשר אתם הקוראים הישירים לפונקציה, ואין בתווך עוד קוד שעלול לזרוק את השגיאה הסטנדרטית הזו.

אני רוצה לצורך הדיון לספק דוגמה לשגיאה שעלולה להיזרק מהספרייה הסטנדרטית של ג'אווה, ועוד מ API חדש לג'אווה 8 (הרבה לקחים נלמדו, ונלמדים בעולם הג'אווה לאורך השנים). קוד הג'אווה הבא:

int day = Instant.now().get(ChronoField.DAY_OF_MONTH);

יזרוק חריגה "סטנדרטית" (כי הקוד הוא של ג'אווה [א]) מסוג UnsupportedTemporalTypeException.

למה? בכדי להדגיש ש Instant (המייצג של epoch ב Java Time APIs) נועד לשימוש ע"י מכונה, ולא בכדי לייצג מידע על תאריך שיגיע למשתמשים בני-אדם.

האם זה נכון, לזרוק שגיאה בשל "שימוש שנראה שגוי בספריה"?

זה דיון ארוך שלא ארצה להיכנס אליו עכשיו, אבל אקצר בכך שזו לא התנהגות ממש צפויה – ושחשוב מאוד לכתוב קוד צפוי שלא יפתיע אותנו ב Production ביום שישי בערב…

אני רצה לסיים ולציין תנאים מומלצים לשימוש ב Standard Exception:

  • החריגות הסטנדרטיות הללו הוגדרו במערכת שלכם, ולא יכולות להיזרק בטעות ע"י ספריית צד-שלישי שנכתבה ע"י מישהו שלא מכיר את הכללים שקבעתם לשימוש בחריגה.
  • ישנם כללים ברורים וידועים – מה אומר השימוש בחריגה הזו.
  • לחריגה הספציפית הזו, יש משמעויות ברמת המכונה / קוד במערכת – והן לא נועדו בכדי להסיר אחריות מהודעת השגיאה שתדווח.
דוגמה אחת היא javax.ws.rs.WebApplicationException
  1. היא אמנם "סטנדרטית בתעשייה" ולא ייחודית לכם. לכן, נכון לזרוק אותה – אבל כדאי להימנע ולהסתמך עליה בלכידת Exception לצרכים אפליקטיביים. היא עשויה להיזרק ע"י כל אחד.
  2. יש לה סמנטיקה משמעותית וברורה ברמת המכונה: Web Applications Servers יתפסו אותה ברמתם – אבל יעבירו את הודעת השגיאה שהוגדרה בה, כמו שהיא, ללקוח. אפשר גם לציין במחלקה הזו גם HTTP Status קוד שיעבור גם הוא ללקוח. למשל: HTTP 403 – הוא קוד בעל משמעות חשובה.
    1. מי שזורק את WebApplicationException, חשוב שיבין את הסמנטיקה שלה – ולא יכתוב בה הודעת שגיאה עם מידע פנימי שלא אמור להגיע ללקוח שביצע את הקריאה.

בדוגמה אחרת, הגדרנו ב Next-Insurance טיפוס Exception פנימי בשם RobinUserInputException.

  1. רובין היא מערכת פנימית שלנו, המשמשת רק משתמשים בארגון. אם משתמש במערכת הזין קלט שנראה שגוי – אנו רוצים להעביר את ההודעה (באנגלית של "מתכנתים", שלא עברה הגהה) ישר מלב הקוד במערכת – עד למשתמש הקצה. אין פה בעיות אבטחה.
    1. ספציפית ל RobinUserInputException יש שתי הודעות שגיאה: כזו שמיועדת ללקוח הקצה (הפנימי), ושניה (שרק תודפס ללוג, ולא תגיע למשתמש) עבור המתכנת שירצה אח"כ לחקור את הבעיה.
  2. כללי-השימוש, מתועדים ומוסברים היטב.

זה שימוש סמנטי (בעל משמעות ברמת הקוד), ופנימי. יש יתרון ממשי בהגדרה ושימוש בסוג כזה של חריגה.

סיכום

אני מקווה שהצלחנו להאיר ולהדגים כמה עקרונות חשובים לגבי Exception Handling.

לצערי, הרבה חשיבה מסביב ל Exception Handling מוקדשת ל Tooling (מחלקות שונות של Exceptions) – ולא למהות השגיאה, וכיצד לטפל בה.

חדי העין אולי שמו לב שההמלצה שסתרתי בפוסט, "Favor the use of standard exceptions" – מגיעה מספר מכובד (של מחבר מכובד) בשם Effective Java. הנה סיכום הדברים:

גם אני, גדלתי על הספר הזה, אבל עם הזמן יצא לי לתהות שוב לגבי חלק מההמלצות שלו – ולהגיע למסקנה שכמה מההמלצות שלו – אינן טובות.
הייתי רוצה להאמין שגם Joshua Bloch, וקוראים אחרים – הגיעו / יגיעו לתובנות דומות (בהנחה שהן נכונות).

הספר נכתב במקור בזמן שאני עוד הייתי סטודנט, והרבה הבנה התפתחה מאז על כתיבת קוד, ועל ג'אווה בכלל [ב].

שיהיה בהצלחה!

—–

[א] אני קצת צוחק, כי זו באמת חריגה ממוקדת, שלא משתמשים בה מחוץ ל Java Time API – אבל יש כאלו שרואים כל חריגה של הספריות של ג'אווה כחריגות "סטנדרטיות".

[ב] כן, אני יודע ש Item 60 מופיע גם במהדורה השלישית של הספר שיצאה רק לפני שנתיים. הייתי שמח לתפוס את המחבר לשיחה על האייטם הזה ועוד כמה אייטמים שנראים לי שגויים – ולברר איתו את העניינים. לא סביר שזה יקרה.

קוד ספרותי = סופן של ההערות בקוד?

כ 70% מעבודת הפיתוח היא תחזוקה של קוד קיים – ולא כתיבה של קוד חדש. התחזוקה דורשת קריאה רבה של קוד קיים. בעצם, גם כתיבה של קוד חדש דורשת קריאה של קוד קיים: תוך כדי כתיבה אנו קוראים שוב ושוב.
המסקנה: יש יתרון משמעותי בכך שקוד יהיה קל לקריאה.שאלה: איך הופכים קוד לקריא?
תשובה נפוצה: מוסיפים הערות!

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

טרמינולוגיה: קצת סדר

לאידאל של קוד אסתטי, אלגנטי וקל לקריאה נוהגים לקרוא בימנו "Clean Code" או "קוד נקי".
קוד נקי מורכב מ 2 אלמנטים עיקריים – מבנה הקוד (למשל: כל פונקציה עושה רק דבר אחד) וקוד ספרותי ("קל לקרוא את הקוד כמו ספר קריאה").

בפוסט זה אני רוצה להתמקד ב"קוד הספרותי" (לא לבלבל עם Literate Programming – דבר אחר לגמרי) בלבד. לא בגלל שמבנה הוא פחות חשוב (חלילה!) – פשוט אחרת לא אגמור את הפוסט.

גם בתוך הקוד הספרותי יש 2 אלמנטים מרכזיים: שפה טבעית (מונח שהמצאתי הרגע) ותיעוד עצמי (Self Documentation).

כלומר: רק כאשר יהיה לנו קוד החותר לשפה טבעית וגם "תיעוד עצמי" – נגיע לקוד הספרותי עליו אנו מדברים. כמובן שקוד קריא באמת ("Clean Code") יהיה רק כאשר גם המבנה יהיה "נקי".

גישת הקוד הספרותי היא נפוצה – אם כי איננה קונצנזוס. היא התבססה בעיקר בעקבות 3 ספרים "פורצי-דרך":

מכתב בשפת Ruby. מקור.

מהו "קוד ספרותי"?

הרעיון של קוד ספרותי מבוסס על 2 הנחות:

  1. קוד נקרא הרבה יותר פעמים מאשר הוא נכתב – ועל כן השקעה בקוד כדי שיהיה קריא, היא משתלמת.
  2. הערות הן משהו שאנו נוטים (באופן לא מודע, אולי) להתעלם ממנו. אנו יודעים שזה לא העיקר.
    א. הערות יוצאות לאורך הזמן מסנכרון עם הקוד. הקוד הוא האמת – וההערות נשארות צל של העבר.
    ב. אנו מסוגלים לקרוא קוד תוך התעלמות מההערות, להבין את הקוד בצורה שגויה ולהמשיך הלאה.
אם הנחות אלו לא נכונות עבורכם – ייתכן וכל הגישה אינה מתאימה לכם.

קוד ספרותי מבוסס על שני עקרונות:
עיקרון א': שאיפה לשפה טבעית
על הקוד להיות קל לקריאה כמו ספר. השפה אליה אנו שואפים היא אנגלית ולא שפת-מחשב, כך שכל statement צריך לשאוף להיות משפט ברור באנגלית – ולא "קוד סתרים".
האמת ש"קוד ספרותי" הוא שם לא-מדויק, אולי אף מעט מטעה:
סיפור של שייקספיר (מתפלפל) או של ג'ורג .ר.ר מרטין (לא-נגמר) – הם לא המודלים אליהם אנו שואפים. המודל מדויק יותר יהיה עיתון / "קוד עיתונאי":
  1. תמציתי.
  2. ברור וחד-משמעי.
  3. מדויק.
  4. קל לקרוא קטעים ממנו.
    ניתן לקפוץ לעמוד 6' לקרוא פסקה ולהבין – מבלי שקראנו את כל העיתון. זאת בכדי שנוכל להתמקד בקטעי קוד שמעניינים אותנו כרגע, מבלי שנזדקק לקרוא מאות שורות של קוד קודם לכן בכדי להבין את הקטע המעניין.
עיקרון ב': תיעוד עצמי (Self-Documentation)
על הקוד לתאר את עצמו ולהבליט את הכוונה.
כל פעם שאנו מוסיפים הערה – זו נורת אזהרה שכתבנו קוד שלא מסביר את עצמו. עלינו לנסות להסיר את ההערה ולגרום לקוד לבטא את המסר ללא עזרה.

ייתכן ובשלב זה נראה לכם שנכנסנו לדיון פילוסופי-תאורטי.
זה לא המצב. מיד נראה דוגמאות קוד.

אז כיצד כותבים קוד "ספרותי"?

נתחיל בחתירה ל"שפה טבעית". הנה מספר עקרונות שננסה להדגים:
  1. שמות (משתנים, פונקציות, מחלקות) ברורים בשפה האנגלית.
  2. שמות המתארים "מה" ולא "כיצד". למשל: Parser ולא LineScanner.
  3. שמירה על רצף קריאה קולח, ללא צורך לחזור לאחור או לדלג לפנים בקוד בכדי לקבל את ההקשר.

נתחיל במתן שמות:

// bad
var ic; // says nothing
function monitorTransIP() // what is IP?!
var hashUrl = "ae4a0192#erlkde"; // url of a Hash?
// good
int itemCount;
function monitorInProcessTransactions() // proper English
var urlHash = "ae4a0192#erlkde"; // no. a Hash of a URL...

כפי ששמתם לב, על השמות להיות באנגלית ולסייע להרכיב קוד שנראה ככל האפשר כמשפט באנגלית. כמובן שגם Camel Case הוא חשוב. נסו לקרוא שמות כמו mONITORiNpROCESStRANSACTIONS… 🙂

לא קל לקלוע ישר לשמות מוצלחים. ישנן 4 "דרגות" של שם:

  1. שם סתמי – המחשה: NetworkManager
  2. שם נכון – המחשה: AgentCommunicationManager
  3. שם מדויק – המחשה: AgentUdpPacketTracker
  4. שם בעל משמעות ("meaningful") – המחשה: AgentHealthCheckMonitor*
* כמובן שהשם AgentHealthCheckMonitor הוא מוצלח רק במערכת בה שם זה מתאר בדיוק וביתר משמעות את אחריות המחלקה. נתתי דוגמאות להמחשה ממערכת שאני מכיר וחושב עליה – כמובן השמות שציינתי לא נכונים / מדויקים / בעלי משמעות באופן אוניברסלי, אלא רק למערכת הספציפית.
עצלנות ולחץ גורמים לנו להיצמד לתחתית הסקלה (1,2), בעוד הקפדה ומקצועיות דוחפים אותנו לראש הסקלה (3,4).

טריק מומלץ הוא לתת לפונקציה / משתנה חדש את השם "foo", מבלי לחשוב. תוך כדי עבודה אתם מבינים מה הפונקציה עושה ומשנים את שמה. לאחר 10 דקות עבודה ייתכן והשם שונה כבר שלוש או ארבע פעמים, אך יהיה ניתן לראות עליות בדרגה של השם. לעתים פשוט צריך לנסות איזה שם ו"לחיות" אתו כמה דקות על מנת למצוא שם מוצלח יותר.

מתי מותר להשתמש בקיצורים?
קשה לטעון שהקוד הבא הוא לא קריא:

for (int i = 0; i < ObjList.length; i++){
    // doSomething
}

אף על פי ש i ואפילו objList הם לא שמות ברורים באנגלית.
מדוע אם כן אנו מצליחים לקרוא את הקוד? א. יש בו קונבנציה מאוד ברורה. ב. אנו רואים במבט אחד את אורך החיים של i וכך מבינים בדיוק מה הוא עושה.

הכלל אם כן אומר: ככל ש scope החיים של המשתנה הוא מקומי וקטן יותר – ניתן לקצר בשם. ככל ש scope החיים של המשתנה הוא גדול יותר (למשל: קבוע ממחלקה אחרת) – יש להאריך בשם.

דוגמה:

// bad
for (int iterationIndex = 0; iterationIndex < l.length; iterationIndex ++){ 
    // doSomething(l[iterationIndex]) - what is "l" ?!?!
}
// good
for (int i = 0; i < completedTaskList.length; i++){
    // doSomething(completedTaskList[i])
}
// better?
completedTaskList.forEach(function(task){
    // doSomething(task)
});

הדוגמה אחרונה אכן מקרבת אותנו לשפה טבעית ("forEach") וגם מקצרת את הקוד, אולם יש בה גם נקודה חלשה: היא שברה במעט את רצף הקריאה. באנגלית אנו נוהגים לומר: "…for each completed task" בעוד דוגמת הקוד דומה יותר ל "…with completed tasks, for each" (סוג של: "אבא שלי, אחותו …" במקום "אחות של אבי") – שפה קצת מקורטעת.
ספציפית בג'אווהסקריפט יש תחביר של for… in ששומר אפילו טוב יותר על רצף הקריאה, אבל מציג כמה pitfalls משמעותיים – ולכן אני נמנע ממנו.
בסופו של דבר אנו מוגבלים לאופציות הקיימות בשפת התכנות, ועלינו להחליט איזו אופציה אנו מעדיפים. כדאי לשקלל את כל המרכיבים לפני שבוחרים.

בדוגמה הנ"ל אני מעדיף את האופציה האחרונה שהיא קצרה, מקובלת, ובעלת "שפה טבעית". נכון: היא לא מושלמת.

ג'ורג' אורוול. סופר ועיתונאי, מודל "לכתיבה עיתנואית":
"Never use a long word where a short one will do"

שמירה על רצף קריאה

הנה כמה דוגמאות כיצד ניתן לחזק את רצף הקריאה:

// not very good
if (node.children() && node.connected()) {
  // doSomething
}

// better
if (node.hasChildren() && node.isConnected()) {
  // doSomething
}

נכון, Hungarian Notations היא סגנון שעבר זמנו, אבל ספציפית הקידומות has ו is מסייעות מאוד לקרוא את המשפט כאשר חסר לנו סימן פיסוק חשוב: סימן השאלה. בנוסף, הקוד המעודכן קרוב יותר לשפה האנגלית.

בשפת Java, מקובל לכתוב:

if ("someValue".equals(myString)) { ... }

וכך להתעלם גם מ nulls, על הדרך. חיסרון: צורה זו שוברת את רצף החשיבה של הקורא. על כן אני מעדיף בכל-זאת את הצורה:

if (myString.equals("someValue")) { ... }
עומס טקסט כמובן גם משפיע לרעה על קלות הקריאה. הייתי שמח לו הייתי יכול לכתוב בג'אווה:
if (myString == 'someVale') { ... }

ג'אווה היא שפה מרבה-במילים (verbose), תכונה המעמיסה טקסט על המסך ומקשה על הקריאה הקולחת.

באופן דומה, עבור הקורא:

if (myString.isEmpty()) { ... }
יותר קולח מקריאה של
if (myString.equals("")) { ... }
למרות שהתבנית מאוד מוכרת.
הנה עוד דוגמה קטנה לכתיבה מעט שונה, אך קולחת יותר:
// switch => reader has to remember 'statusCode' = the context
switch (statusCode) {
  case 169 : // return Something();
  case 201 : // return Something();
  case 307 : // return Something();
  default: // return SomeOtherStuff();
}

// Better: each line is a complete sentence 
switch (true) {
  case statusCode == 169 : // return Something();
  case statusCode == 201 : // return Something();
  case statusCode == 307 : // return Something();
  default: // return SomeOtherStuff();
}

במקום שהעין תקפוץ כל הזמן ל statusCode להיזכר בהקשר (בדומה למשפטי "with"), כל משפט הופך למשפט שלם. כל זאת – בעזרת אותו סט כלים זמין בשפה.

עדכון: קיבלתי כמה הערות מאנשים שחשבו שדווקא הדוגמה השנייה פחות קריאה בגלל "התרגיל" שהיא עושה (true בתוך ה switch), אני מניח שזה עניין של טעם. ראיתי אינסוף ויכוחים על סגנון עם מאפיינים דומים (בדיקת קלט ו return בתחילת הפונקציה – ליציאה מהירה) – ואינני מצפה להסכמה מלאה: יש טיעונים לכאן ולכאן. בסופו של דבר מה שחשוב הוא לבדוק כיצד הקוד יהיה קריא יותר עבור הצוות / הארגון שלכם.

בסופו של דבר, חשוב שתקשיבו כיצד אתם מסבירים לאחרים את הקוד שלכם. נסו לחתור לכך שהקוד ייקרא בדיוק כפי שהסברתם: מילה במילה (עד כמה שאפשר / סביר).



"תיעוד עצמי" – סיפורו של מתכנת

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

כשהמתכנת חדש, הוא אינו כותב הערות. הוא עסוק בלגרום לקוד בכלל לעבוד (A).
עם הזמן, המתכנת מבין שיש חיסרון בקוד מבולגן ובלתי-קריא. הוא מתעד ומסביר עוד ועוד מה הקוד עושה. לעתים כל שורת קוד מקבלת הערה – בכדי שהכל יהיה "ברור" (B).
לאחר זמן נוסף, המתכנת מקבל אמון בכתיבת הקוד שלו. תבניות מסוימות בקוד "מדברות בעד עצמן" והוא לא זקוק לתיעוד. הוא נחשף לרעיונות של "קוד ספרותי" ומגלה כיצד ניתן לתעד את הקוד ללא הערות. זה עובד, הוא מתלהב ועכשיו יש לו "משחק חדש" – לנסות ולחתור ל "0 הערות בקוד", תוך כדי שהוא משאיר את הקוד קריא למדי. הוא מפצח מקרה אחר מקרה (C) – ומצליח לכתוב קוד קריא ללא הערות, עד אשר הוא מגיע למצב בו הוא כותב קוד באורך אלפי שורות קוד ללא שורת הערה אחת.
זה אתגר, זה מהנה וזה עובד. הוא משתמש בטכניקות שוב ושוב ומצליח ליצור קוד קריא ללא הערות. הוא מביט בקוד שלו בהנאה – ומתמוגג. "הצלחתי" (D).

אבל… עוברים כמה חודשים והוא מקבל פרספקטיבה רחבה יותר. הוא מבין שיש דברים שלא ניתן לתעד בקוד בלבד.
דוגמה פשוטה: קוד מדבר על "כאן ועכשיו". איך הוא יספר למתכנת שיקרא את הקוד שלו שהוא עושה כאן "פעולה א" בגלל שמודול אחר מתנהג בצורה "ב"? שפונקציה זו היא בגלל בעיה היסטורית שהתרחשה בעבר? שפה עשו משהו זריז – אבל יש כוונה לשפר את זה בעתיד?
הוא גם שם לב שלמרות שאפשר לכתוב כל הערה מתוך הקוד – לעתים פשוט יותר לכתוב הערה.
כאשר יש מעט הערות בקוד, וכל הערה היא חשובה – נוהגים לקרוא את ההערות יותר ולהתייחס אליהן ביתר רצינות. המתכנת חוזר לכתוב מעט הערות ובמשך הזמן מוצא את נקודת האיזון בין "קוד ללא הערות" ל "הערות שלא משתלם לבטא כקוד" (E).

העברת הערות לקוד

"כאן צריך הערה!" הוא הקול הפנימי שבפסקה הבאה ננסה להתנגד לו.
כל הערה היא סימן מובהק לקוד לא-מספיק-קריא. האם אנו רוצים לשים עוד פלסטר, או לטפל בבעיה מהיסוד?

בואו ננסה!

ראשית ננסה להיפטר מ"מספר קסם" (Magic Number). מספר קסם הוא מספר שפתאום מופיע בקוד ולא ברור כיצד החליטו עליו. קסם.

/*= Huh?! =*/
totalHeight = $el.height + 14;


/*= Better =*/
totalHeight = $el.height + 6+6+1+1;


/*= Even Better =*/
// two times the border (6) + two times the margin (1)
totalHeight = $el.height + 6+6+1+1;


/*= Introduce constant; Even Better =*/
var BORDER_WIDTH = 6, MARGIN = 1;
totalHeight = $el.height + 2 * BORDER_WIDTH + 2 * MARGIN;

הערה: השתמשתי בהערות מסוג /*= =*/ כמטה-הערות בהן אני משתמש להעיר על הקוד / ההערות.

"14" הוא מספר קסם – לא ברור כיצד הגיעו אליו. פירוק לנוסחה (6+6+1+1) משפר את המצב, ובעזרת הערה – המצב אפילו טוב יותר. אבל, ההערה יכולה לצאת מסינכרון עם הקוד ולהפוך ללא-מעודכנת. לבסוף אנו כותבים את ההסבר בעזרת קוד בלבד – כזה שקשה יותר להתעלם ממנו או להשאיר אותו לא מעודכן. מצוין!בואו נראה דוגמה נוספת:

/*= Why do we need this comment ? =*/
// remove "http://" from the url
str = url.slice(7);


/*= Introduce constant; Slightly better =*/
var HTTP_PREFIX_LENGTH = 7;
str = url.slice(HTTP_PREFIX_LENGTH);


/*= comment -> code; Better =*/
str = url.slice('http://'.length);

הצלחנו לבטל את ההערה, ולהפוך אותה לחלק מהקוד – קוד קריא. נהדר!

פירוק של ביטויים לא ברורים לאיבר נוסף עם שם ברור אינה שמורה רק למשתנים. הנה טיפול ב"ביטוי קסם":

/*= Why do we need this comment ? =*/
// check if document is valid
if ((aDocument.isAtEndOfStream() && !aDocument.hasInputErrors()) &&
    (MIN_LINES <= lineCount && lineCount <= MAX_LINES)) {
    print(aDocument);
}


/*= extract method; comment -> code =*/
if (isDocumentValid(docStream, lineCount)) {
    print(aDocument);
}
הוצאנו (extract) פונקציה, נתנו לה שם ברור – וביטלנו את הצורך בהערה!

עד כאן טוב ויפה. יש מקרים שבהם נדמה לרגע שאין לנו דרך סבירה לבטל את ההערות, שהן פשוט נדרשות:

/*= We need these comments to highlight sections, don't we? =*/
function foo(ObjList){
    var result = [], i;

    // first fill objects
    for (i = 0; i < Objlist.length; i++){
        // doSomething
    }

    // then filter disabled items
    for (i = 0; i < result.length; i++){
        // doSomething
    }

    // sort by priority
    result.sort(function(a, b) {
        // apply some rule
    });

    return result;
}


/*= Extract Methods; comments -> code =*/
function foo(ObjList){
    var result = [];

    result = fillObjects(Objlist);
    result = filterDisabledItems(result);
    result = sortByPriority(result);

    return result;
}

כשאנו רואים פונקציה (foo) שמחולקת בעזרת הערות (כגון "first fill objects") למקטעים, זהו רמז טוב שהפונקציה עושה יותר מדבר אחד. הפתרון העדיף הוא לחלק את הפונקציה למספר פונקציות, שכל אחת עושה פעולה אחת ויש לה שם ברור שמתאר מה היא עושה.
נכון, התוצאה היא יותר פונקציות קטנות וממוקדות – שזה בד"כ יתרון נוסף. אם המחלקה שלכם כוללת יותר מדי פונקציות קטנות – זהו סימן בד"כ שאפשר לפצל את המחלקה לכמה מחלקות

האם גם בדוגמה הבאה ניתן לוותר על ההערה?

function calcRevenue(){
    /*= Walla! This comment is Absolutely Irreplaceable! =*/

    // Order Matters!
    calcMonthlyRevenue();
    calcQuartrlyRevenue();
    calcAnnualRevenue();
}


function calcRevenue(){
    /*= Hmmm... better luck next time =*/

    var lastMonthRevenue = calcMonthlyRevenue();
    var lastQuarterRevenue = calcQuartrlyRevenue(lastMonthRevenue);
    calcAnnualRevenue(lastQuarterRevenue);
}
במקרה זה הייתה לנו הערת "meta" על הקוד: "סדר השורות חשוב!" – הערה שנראה במבט ראשון שאין לה תחליף.
ביטלנו את ההערה ע"י יצירת קשר הכרחי (אם כי מעט מלאכותי) בין הפונקציות המתאר בדיוק את הקשר.
דוגמה זו היא מעט קיצונית ויכולה להיכשל בקרב מפתחים שלא מכירים את ה convention של "תלות מפורשת בין פונקציות". מצד שני, יצא לי להיתקל בהערת "Oder Matters" שהתעלמו ממנה ויצרו באג – כך שאני לא בטוח מה עדיף.אני מניח שבשלב זה הרעיון כבר ברור. הייתי רוצה לקנח בדוגמה לא מפתיעה, אך חשובה: מבני נתונים

/*= Custom data structure: working, but not descriptive =*/
var row = new Array[2]; // team's performance
row[0] = "Liverpool";
row[1] = "15";


/*= Comments -> code; but what are 0 & 1? =*/
var teamPerformance = new Array[2];
teamPerformance[0] = "Liverpool";
teamPerformance[1] = "15";


/*= Introduce Class =*/
var tp = new TeamPerformance();
tp.name = "Liverpool";
tp.wins = "15";

ביקורת

ישנן גם התנגדויות לגישת "הקוד הספרותי". הנה ביקורת מפורסמת (וצבעונית) שהתפרסמה. הנה התגובה של דוד בוב.

אקצר לכם 10 דקות של וידאו (אם הנושא מעניין אתכם – הייתי משקיע את הזמן):
לביקורת, למרות צבעוניותה, יש נקודה: כתיבת קוד ספרותי גורמת לנו לבצע Refactor מסוג Extract הרבה פעמים – מה שיוצר יותר פונקציות קטנות ופחות רצף קריאה של קוד. דוד בוב עונה: פונקציות קטנות מבטיחות קוד "עיתונאי" בו אפשר להביט בנקודה x מבלי לקרוא הרבה שורות קוד קודמות בכדי להבין את הקונטקסט.

בסופו של דבר חשוב להבין ש"קוד ספרותי" הוא כלי. יש להשתמש בו בתבונה ובמידה.
"קוד ספרותי" דורש עוד עבודה.
"קוד ספרותי" דורש מיומנות נרכשת.
"קוד ספרותי" לא גורם ללקוחות לשלם יותר.
אבל אם הקוד שלנו הוא קוד "חי" שעובר הרבה שינויים ותחזוקה – קוד ספרותי יכול להיות מאוד משתלם לאורך זמן.

סיכום

ראינו כיצד ניתן לשפר את הקריאות שלנו ולאו דווקא בעזרת הערות. בעזרת כתיבת קוד שדומה לשפה הטבעית (במקרה זה: אנגלית) הן ברמת התחביר והן ברמת השמות. ראינו שהשמות לא משנים אולי כ"כ ל Compiler, אבל הם משנים הרבה מאוד לבני-אנוש שקוראים את הקוד. לא קל להגיע מיד לשמות מוצלחים, זו עבודה ומיומנות.

ראינו כיצד אפשר לכתוב קוד המבטא את המסר גם ללא עזרה מאותן "הערות נלוות". כתיבת הערה צריכה להדליק לנו נורת אזהרה: "כאן נכתב קוד לא-מספיק-ברור", כזה שמחייב תלאי בדמות הערה בכדי להסביר מה עשינו.
כמובן שיש פעמים בהן נרצה הערות. קוד מתאר "כאן ועכשיו" ולא יכול לבטא כוונות או היסטוריה. לעתים, ניתן לבטא כוונה בעזרת קוד בלבד – אך התוצאה היא מסורבלת ואנו נעדיף הערות. בונוס: כשיש מעט הערות בקוד – קוראים אותן יותר וביתר תשומת-לב.

למדנו ש"קוד ספרותי" הוא רק אבן-דרך אחת בדרך ל"קוד נקי". מבנה התכנית חשוב לא פחות. לעתים קרובות יש לנו אלגוריתם או flow מסובכים / לא-ברורים. כתיבת קוד "ספרותי" תעזור רק במעט, בעוד הפתרון האמיתי הוא להכין לעצמנו כוס גדולה של קפה ולשבת על הקוד עד אשר נבין מדוע הוא מסובך – וכיצד אפשר לפשט אותו. שיפור מבנה הקוד הוא לא משהו שכיסינו בפוסט זה.

בנוסף ל"קוד ספרותי" ו"מבנה" יש אלמנטים נוספים המשפיעים על קריאות הקוד, כגון "אסתטיקה של קוד" (ריווחים, עימוד וכו') ואחידות הקוד.
באחידות של קוד יש לשמור על האיזון בין שימור האחידות הקיימת לחידוש. עם הזמן נפתח סגנון טוב ומתקדם יותר – וכדאי למצוא את הדרך להתחיל ליישם אותו ולא רק "להיתקע עם הסגנון הישן".

בסופו של דבר "קוד ספרותי" הוא לא מדע מדוייק וכדאי לזכור את המטרה: קוד שיהיה קל-מאוד לקריאה עבור אלו שקוראים אותו, גם לאחר מנה הגונה של תחזוקה. חשוב יותר לעשות את מה ש"עובד" עבור הצוות שלכם, מאשר להיצמד לכלל זה או אחר.

איך מתחילים לכתוב "קוד ספרותי"?
הכי פשוט להיצמד לכלל הצופים (The Boy Scout Rule): "השאר את השטח נקי יותר ממה שקיבלת אותו".
כל פעם שאתם נוגעים בקוד ומבצעים שינוי – שפרו מעט את הקריאות וקדמו מעט את הקוד לעבר "קוד ספרותי". עם הזמן – השינוי יהיה ניכר ויגיעו גם התוצאות.

שיהיה בהצלחה!