פורסם ב זה רק קוד

לפצל או לא לפצל (לפונקציות)?

זמן קריאה: 7 דקות

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

בתורת הקוד הנקי העיקרון הזה נקרא DRY – Don't repeate yourself.

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


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

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

אז מי צדק? החוש שלי או החוש של כותב הפוסט?

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

יש הרבה יתרונות לקוד שמחולק למעט פונקציות גדולות, על פני קוד שמחולק להרבה פונקציות קטנות 2.

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

רגע, אז לפצל או לא לפצל לפונקציות?

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

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

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

רשימת הסיבות הטובות שלי לשקול להוציא קוד לפונקציות

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

  2. יצירת ממשק שהרבה מערכות ישתמשו בו.
    עוד סיבה מצוינת לפצל קוד לפונקציות היא כדי ליצור הפשטה (Abstraction). הרעיון של הפשטה הוא שחלקים שונים במערכת יוכלו לדבר זה עם זה, בלי להכיר פרטי מימוש. כך, אם המימוש של הפונקציה ישתנה, הקוד שמשתמש בה לא יושפע מכך.
    לדוגמא – אם יש לנו מערכת שמשתמשת ב-DB, נוכל להשתמש בפונקציה של הבאת שורה מה-DB לפי מזהה. הפונקציה שלנו תיראה כך – 
def get_by_id(my_id):
    return db.get(my_id)

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

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

  2. במקום לשים הערה.
    לפעמים אנחנו מסתכלות על חתיכת קוד סבוכה במיוחד ומרגישות צורך לכתוב הערה שתסביר מה הוא עושה. במקרה כזה אופציה טובה יותר תהיה להוציא את הקוד לפונקציה. לפונקציה נוכל לתת שם שיסביר מה הקוד עושה. שמות טובים ואינפורמטיביים מתעדים את הקוד יותר טוב מהערות, ולאורך יותר זמן.
    תודה ל- @lisardggY, ו- @Gili ו- @giltayar שעזרו לנסח את הנקודה הזו.
  1. לנקות את הפונקציה הראשית.
    אני חושבת שהרעיון של SRP – להפריד כל חתיכת לוגיקה שעומדת בפני עצמה לפונקציה – הוא קצת מבלבל ומאיים. הרי כמעט כל שורה בקוד יכולה להכיל בתוכה קצת לוגיקה ואי אפשר להפריד עד אינסוף. לכן אני מעדיפה לחשוב על הניקיון של הפונקציה הראשית שלי. כל חתיכת לוגיקה בקוד שהיא לא חלק מהעבודה של הפונקציה הראשית אפשר להוציא לפונקציה נפרדת. כך, מי שרוצה להכיר רק את הקוד של הפונקציה הראשית תוכל לקרוא ולקלוט אותו בקלות.
    לדוגמא – validation של קלט, העשרת מידע (כשהיא לא חלק מהלוגיקה הראשית) וכו'.

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

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


הערות:

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

10 תגובות בנושא “לפצל או לא לפצל (לפונקציות)?

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

  2. [נעמיד פנים לרגע שאפשר לנסח כללים ברורים לנושא הזה וגם לפעול לפיהם…]
    לטעמי, הטענות שהבאת נגד חלוקה לפונקציות קטנות הן למעשה לא נגד חלוקה באופן כללי אלא רק נגד חלוקה לא נכונה. כי אם מחלקים את הקוד לפי תפקיד לוגי ברור, ונותנים לזה שם ברור (לא פחות חשוב!), אז אפשר לקרוא את הפונקציה ה"גבוהה" ולהבין אותה בלי להיכנס כלל לתפקוד וללוגיקה של פונקציות המשנה. וזה יהיה נכון בכל רמה. לדוגמה פשטנית, אם בתוך פונקציה אני קורא לפונקציה אחרת בשם sort, זה מובן מאליו – לא צריך להסתבך עם המימוש והמשתנים המקומיים שלו, והלוגיקה נעשית רק פשוטה יותר למעקב.
    [זהו, אפשר לחזור למציאות 🙂 ]

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

    1. אני ממש, ממש, ממש בעד להוציא קוד מכוער ומפחיד לפונקציה – אפילו אם זה ממש רק לדחוף את הזבל לחדר אחר.

      כך, קוראות הקוד יודעות איפה הבלאגן מתחיל ואיפה הוא נגמר; הן לא צריכות להבין מה הקוד עושה מעבר לשם הפונקציה (ואולי docstring, ואולי unit test… הכל כדי לא להתעסק עם הקוד המכוער עצמו); וה-import-ים האקזוטיים גם הם מוסתרים להם בקובץ נפרד בלי שהקוראת צריכה לדעת מה תפקידם בחיים (זה קשור גם לאבסטרקציה בעצם)

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

השאר תגובה