דרכים לנהל שגיאות ואקספשנס

בעולם אוטופי, כל פונקציה שנקרא לה תניב לנו את המידע שאנחנו צריכים, ולא תעצור את הflow של הקוד. אבל אנחנו לא בעולם אוטופי, וכל פונקציה שנקרא לה טומנת בתוכה את הפוטנציאל להשתבש ולחשב לנו את המסלול מחדש. והאמת היא, שלהקריס תוכנה זה כנראה יותר קל מלקחת סוכריה מתינוק, אז מן הראוי שerror & exception handling תהיה המומחיות הכי קריטית בלשמור על התוכנה…עובדת. או במילים אחרות: רובוסטית, אנטי-שבירה, כזאת שבנויה להתמודד עם הלא רצוי ולא מצופה. איך כדאי לנו להתמודד עם תקלות בלתי צפויות בקוד? הדמיון הוא כמובן אינסופי, אבל יש מספר טכניקות פופולאריות שמשתמשים בהן: לזרוק שגיאה, להחזיר null, להחזיר אובייקט ריק או monad.

לזרוק שגיאה ירגיש מאוד טבעי עם פונקציות IO שיכולות להתפוצץ, כמו שגיאה בחיבור למסד נתונים או באי מציאת קובץ. אנחנו נוכל גם לזרוק שגיאות תפורות אישית לבעיות מסוימות, וככה נוכל לאבחן את התקלות בקלות רבה ולהכין backup plans לכל שגיאה (כלומר, לרשת מה- Exception, לממש ולהחזיר שגיאות משלנו). לזרוק שגיאה מתאים למקרים בהם אנחנו רוצים ללכת בגישה של easier to ask forgiveness than permission, כלומר אנחנו נקרא לפעולה ונתפלל שהיא תעבוד. אם לא, אוקיי סליחה, ננסה משהוא אחר. שוב, פעולה של IO היא הדוגמה הכי אלגנטית למקרה כזה כי היא יכולה להיות בקלות פצצה מתקתקת. בג’אווה יש לנו checked exception שמכריח אותנו להתמודד עם כל פונקציה שעלולה לזרוק שגיאה. בזמן שזה נחמד בתיאוריה, הכפייה להתמודד תמיד עם שגיאה היא לא בהכרח הכרחית או רצויה, שכן יש מקרים בהם פשוט לא נרצה להתמודד עם השגיאה.

בנוסף ללזרוק שגיאה, אנחנו יכולים להחזיר null, שמתאים לגישה של look before you leap, דהיינו תמיד לבדוק את מה שאנחנו מקבלים. הגישה מתאימה למקרים בהם null הוא פלט לגיטימי, כמו למשל כשאובייקט (שורה) לא נמצא בדטאבייס. אין הכרח לפוצץ את הפעולה, מכיוון שהפעולה הסתיימה בהצלחה ולא כשלה בגלל עצמה, אלא מכוח חיצוני לה (למרות שכן ניתן להטיל ספק במקרים בהם אי מציאה של אובייקט נחשב לכשל לוגי). להחזיר null עוזר לנו לצמצם קוד שמכיל הרבה trycatch, אבל בתמורה נצטרך לתחזק קוד עמוס ב != null, והכלל מספר אחד בקוד הוא: כל תחזוקה שוטפת עלולה להוביל לטעויות. מתכנת עלול לשכוח, או כמובן לא לדעת- שהפונקציה עלולה להחזיר null, ומלאך המוות יקפוץ לביקור.

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

אופציה פופולארית נוספת, היא שימוש ברעיון של monad, שברמה הבסיסית שלו הוא מעטפת אבסטרקטית וגנרית המכילה בתוכה את האובייקט שאנחנו רוצים להחזיר, ויחד עם זה עוד מידע כמו השגיאה עצמה, הודעת שגיאה וכדומה. היתרון מספר אחד של מונאד הינו שהוא מגדיר סינטקס אחיד וברור, והוא שכל פונקציה מחזירה את המונאד, ובתוכה את האובייקט הרצוי כמו חבילה מאלי אקספרס. וכמובן, הוא עוזר למנוע nullpointexceptions לא מתוכננים כי המונאד אף פעם לא צריך להיות null. דוגמאות לכך הן ה-Maybe שקיים בתכנות פונקציונאלי וכמובן ב-reactive programming, וה- IO Monad שבהאסקל, שהם מתפקדים כ-קצת יותר ממעטפת אבסטרקטית.

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

יכול להיות שכל הדרכים לגיטימיות בדרכים שלהן, עם קצת יתרונות אחד על פני השני. אפשר לטעון שאין צורך להחזיר null כל עוד משתמשים במונאד, אבל גם אפשר להשתמש תמיד ב-trycatch בלבד. בסופו של יום אנחנו רואים שלכל אחד אכן יש חוזקות משלו בהתאם לסיטואציה. לא כדאי להגדיר חוק אחיד של איך להתמודד עם שגיאות שקורות, אלא לתפור לכל בעיה את הדרך שלה להתמודד עם שגיאות. ככה נוכל להנות (כמעט) מכל העולמות ולהתמודד עם מינימום השלכות וחסרונות. but exceptions… exceptions never changes.

לטעון תגובות?