איך התחלנו לנהל שגיאות ב Rust (חלק א' - Result)

מבוא

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

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

מה עשינו לפני

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

חשוב לציין, שעצם העובדה ש get_registry_key מחזירה String איננה הבעיה העיקרית כאן, אפשר לראות שמיד לאחר הקריאות ל open_subkey ו get_value של winreg crate מתלוות קריאות ל unwrap עליהן ארחיב ממש עוד רגע, אבל בינתיים רק אגיד שבכל מקום בו אנחנו רואים unwrap, הקוד שלנו יכול לקרוס (panic) במקרה של None או שגיאה - דבר שלרוב לא נרצה שיקרה בפרודקשן.

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

שימוש ב Result

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

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


בגלל העובדה ש Result הוא enum, נשתמש ב match statement כדי לזהות את תוצאת החזרה ולפעול בהתאם.
בפועל, אנחנו נראה פעמים רבות (במיוחד בדוגמאות קוד באינטרנט) את השימוש ב unwrap מתבצע על Result.

המימוש של unwrap מאוד straightforward:

אם הערך שחזר מהפונקציה תקין - נחזיר אותו, אם לא - התכנית תקרוס (panic).

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

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

איך הקוד נראה אחרי

מהרגע שהחלטנו שאין יותר unwrap בקוד זה אומר לטפל בכל Result שחוזר מכל פונקציה פנימית בה אנחנו משתמשים. במקרה שלנו גם get_value וגם open_subkey מחזירות Result. אז בשימוש החדש בהן פשוט נפתח את הערך שחוזר מהן.


בדוגמה הזו פתחנו את ה Results שחזרו, במקרה של הצלחה - המשכנו עם הערך שחזר, ובמקרה של שגיאה - החזרנו את אותו ה Error מתוך הפונקציה, וזה יוצר מצב שמי שקרה לפונקציה שלנו יקבל את השגיאה הפנימית במקרה של אי הצלחה.

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

אז איך פותרים את זה?
כאן נכנס אחד שפיצ'רים האהובים עליי בראסט - error propagation עם אופרטור `?`.
במקום לטפל בשגיאה כמו שראינו בדוגמה הקודמת, אנחנו יכולים פשוט לשים `?` בסוף הביטוי ואותה לוגיקה בדיוק תתרחש מאחורי הקלעים.


אחרי השינוי הזה קיבלנו קוד הרבה יותר אלגנטי וקריא ואופרטור ה`?` מסמן בדיוק איפה יש טיפול בשגיאה שעלולה לצוץ.

הערה: אופרטור `?` יכול לבוא גם אחרי Option אך בשביל להשתמש בו נצטרך שהפונקציה כולה תחזיר Option.

סיכום

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

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

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

תגובות

פוסטים פופולריים מהבלוג הזה

מהם קבצי DLL ואיך להשתמש בהם?

מדריך C# | שימוש ב LINQ

תכנות מונחה עצמים | Dependency Inversion Principle