2020-07-11

תזכורת להשתמש ב 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 על מנת לכתוב קוד פשוט יותר, ואינטואטיבי יותר.

שיהיה בהצלחה.