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

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

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

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

כתבו קוד קריא

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

  • שמות בהירים יותר למשתנים. ולא, 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. שכתבו בביסים קטנים; במקום לחכות לרגע שלא תהיה ברירה אלא לשכתב את כל הקוד בבת אחת, השקיעו מספר שעות בשבוע כדי "לעשות מסג'" לחלקים קטנים ממנו בכל פעם.

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

למה אתם *חייבים* ניהול גרסאות – גם כשמתכנתים בבית

כל מי שהוא חלק מצוות פיתוח בחברה יעיד שהוא משתמש בכלי ניהול גרסאות (source control) כלשהו כגון Subversion, Git, Team Foundation וכד'. ואכן כשמספר אנשים עובדים על אותו קוד היתרונות של כלי כזה הם עצומים, והוא פותר בעיות בסיסיות מאוד כמו: איפה שמור הקוד? מה קורה כששניים עובדים במקביל על אותו קובץ? איך מתעדים מה כל אחד עשה? וכד'.

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

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

כלי ניהול גרסאות במשפט אחד

את התפקיד הבסיסי של כל כלי ניהול גרסאות באשר הוא ניתן לנסח כך:

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

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

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

source-control

בדוגמה שלנו, השתמשתי בכלי ניהול הגרסאות Git, המייצר תיקייה בשם .git (שימו לב לנקודה בתחילת השם).

ניהול גרסאות למפתח יחיד

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

די לפחד משינויים

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

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

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

למה עשיתי את זה?!

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

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

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

כעת כאשר נרצה לדעת מדוע ומתי קטע קוד כלשהו נכתב, נוכל לעיין בהיסטוריית השינויים (revision history) של הקובץ ולקרוא את ההודעות שהשארנו לעצמנו. ניתן אפילו לקבל דו"ח מפורט על השינויים שבוצעו בכל שורה ושורה בקובץ ע"י פקודת annotate file, או בשמה הכן יותר – blame :)

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

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

רגע, מה עשיתי עכשיו?

סוג שונה של מעקב אחר שינויים הוא מעקב אחרי השינויים שעשינו מאז ה-commit האחרון. זה אולי נשמע מוזר, אבל כמה פעמים שכחתם בקוד שלכם פקודת print שלא צריכה להיות שם או console.log מיותר?

הכלים הגרפיים לניהול גרסאות כמו Tortoise Git, TortoiseSVN וכד' מאפשרות לנו לראות בצורה נוחה את כל השינויים שביצענו, ולעבור על הקוד לפני שאנחנו מבצעים commit. ואכן לפני כל commit, אני אוהב לעבור במהירות על השינויים שביצעתי, לוודא שאני לא מכניס שטויות לקוד ושהקוד עומד בסטנדרטים שהצבתי לעצמי. ברוב המקרים אני מוצא משהו לתקן, והורג באגים עוד לפני שהם נולדים.

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

Source control diff
אופס, זה לא אמור להיות פה…

עבודה על מספר גרסאות במקביל

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

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

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

ה"קפיצה" הזאת בין ענפים תשנה בבת-אחת את כל קבצי הקוד בתיקייה לגרסה הרצוייה, ותאפשר לנו לערוך אותה, לבצע commits ולשחרר גרסאות בדיוק כאילו לא ביצענו בה שינויים. לאחר תיקון הענף הבעייתי (לרוב יהיה זה הענף הראשי, default branch, או הגזע, trunk) נוכל לקפוץ בחזרה לענף החדש ולהמשיך לעבוד עליו.

כדי לייצר ענף חדש לרוב נשתמש בפקודה branch <new branch> שלמעשה אומרת לכלי ניהול הגרסאות "כל השינויים שאבצע מעתה צריכים להיכנס לענף new branch".

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

Source Control Branches
עבודה עם ענפים. בדוגמה זאת יצרתי את הענף הכחול מתוך הענף הירוק ועבדתי על שני הענפים במקביל. לבסוף איחדתי את שני הענפים ע"י ביצוע merge.

גיבוי

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

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

תהליך העבודה שלי

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

Source Control Process

התחילו כבר עכשיו!

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

  1. הורידו והתקינו את הכלי TortoiseHg.
  2. פתחו את התיקייה בה נמצא הקוד שלכם, לחצו מקש ימני ואז TortioseHg -> Create Repository Here ובחלונית שנפתחה לחצו Create. בשלב זה תיווצר תיקיה בשם .hg וקובץ נוסף.
  3. לחצו מקש ימני על תיקיית הקוד שלכם (לא על התיקייה שנוצרה), ואז Hg Commit.
  4. סמנו את קבצי הקוד אותם תרצו להוסיף לניהול הגרסאות. בד"כ יהיו אלו כל קבצי הקוד בלבד, ולא קבצים ותיקיות הנוצרים אוטומטית כמו קבצי exe, pyc וכד'.
  5. הזינו הודעה קצרה כגון "First commit" ולחצו Commit.
  6. אם נשאלתם האם להוסיף את הקבצים שסומנו, לחצו Add.

זהו! הקוד שלכם עכשיו מנוהל גרסאות! עכשיו אתם מוכנים לבצע את השינוי הראשון שלכם.

  1. ערכו את אחד מקבצי הקוד.
  2. לחצו מקש ימני על הקובץ או התיקיה, ולחצו Hg Commit.
  3. ברשימת הקבצים בצד שמאל תוכלו לראות את כל הקבצים שהשתנו, ובצד ימין את השינויים בכל אחד מהם.
  4. לצפייה בשינויים בתוכנה מתקדמת יותר, לחצו לחיצה כפולה על הקובץ הרצוי.
  5. כעת ניתן לכתוב הודעה ולבצע commit.
  6. לחצו מקש ימני על הקובץ ואז TortoiseHg -> Revision History לצפייה בהיסטוריית השינויים של הקובץ.