Drowning

איך לתחזק עשרות אלפי שורות קוד ולהישאר פרודקטיביים

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

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

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

כתבו קוד קריא

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

  • שמות בהירים יותר למשתנים. ולא, temp לא נחשב שם טוב.
  • חלוקה לפונקציות קטנות (10 עד 20 שורות) עם שמות ברורים.
  • הקפדה על מוסכמות כתיבת קוד. למשל, שמות משתנים באותיות קטנות עם קווים תחתונים בין המילים. המוסכמות יאפשרו לכם ולאחרים לקרוא את הקוד מהר יותר.
  • שימוש בקבועים במקום מספרים שלא תמיד ברור מה הם.

לדוגמה, קטע הקוד הבא הוא קשה מאוד לתחזוקה:

temp = datetime.now()
r = []
for x in lst:
    if temp - x.d > timedelta(10):
        continue
    r.append(x)

לעומת קטע הקוד הבא, שהתנהגותו דומה:

RECORD_TTL = timedelta(days = 10)

def discard_old_records(record_list):
    result = []
    now = datetime.now()
    for record in record_list:
        if now - record.date > RECORD_TTL:
            continue
        result.append(record)
    return result

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

העדיפו קוד קריא על הערות

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

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

לדוגמה, ההערה הבאה מיותרת לחלוטין:

# Return output data formatted as JSON
return json.dumps(output_data)

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

הנה דוגמה להערה מועילה:

# Due to a bug in service X (http://...) we should retry on a 500 error
if status_code == 500:
    raise TemporaryError

כסו את הקוד בבדיקות

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

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

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

  1. לדאוג שלפחות 70% משורות הקוד מכוסות על ידי הבדיקות.
  2. לבדוק בעדיפות עליונה חלקים חיוניים ורגישים בקוד (למשל קוד אבטחה ואימות הרשאות, שליחת מיילים ללקוחות, flow ראשי של התוכנית וכד').
  3. לבדוק בעדיפות עליונה קוד תשתיתי, עליו נסמכים חלקים אחרים בתוכנית.

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

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

השתמשו בכלי ניתוח קוד סטאטי

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

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

ניתוח קוד סטאטי חשוב אף יותר בשפות סקריפט כגון python, Javascript, PHP, Ruby, בהן אין מהדר המאתר שגיאות בזמן בניית התוכנית, ושגיאות מתגלות לרוב בזמן ריצה.

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

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

תעדו שינויים ב-source control

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

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

הנה דוגמה להודעת commit גרועה:

changing files

והנה דוגמה להודעות commit שימושית:

bugfix: handling sporadic failures on service X

החיו קוד מת

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

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

כדי לא להגיע למצב הזה, השקיעו מדי פעם זמן לביצוע "החייאה" לקוד מת:

  1. כתבו בדיקות יחידה; הן יגדילו את היכרותכם עם הקוד, וגם יאפשרו לשנות אותו בלי לחשוש.
  2. שכתבו בביסים קטנים; במקום לחכות לרגע שלא תהיה ברירה אלא לשכתב את כל הקוד בבת אחת, השקיעו מספר שעות בשבוע כדי "לעשות מסג'" לחלקים קטנים ממנו בכל פעם.

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

3 תגובות בנושא “איך לתחזק עשרות אלפי שורות קוד ולהישאר פרודקטיביים”

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

כתיבת תגובה

האימייל לא יוצג באתר. (*) שדות חובה מסומנים