למה Javascript מרגישה לחלקנו כמו סינית?

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

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

בתור מישהו שכותב Javascript כבר שנים רבות, הבעיה הזאת תמיד סיקרנה אותי. הרי התחביר של Javascript דומה מאוד לשפות אחרות (כמו C++) ויכולותיה "out of the box" מצומצמות מאוד, אז מדוע מתכנתים רבים מתקשים בהבנתה? אחרי מחשבה רבה אני מאמין שמצאתי את שורש הבעיה.

אסינכרוניות מובנית

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

ניקח לדוגמה את הפסאודו קוד הבא, הכתוב בסגנון קלאסי:

name = wait_for_user_input()
print "Welcome " + name

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

אחד המאפיינים המעניינים והחשובים ביותר של שפת Javascript הוא הריצה ב-thread אחד בלבד. כלומר אם היינו כותבים קוד כזה ב-JS, כל ממשק המשתמש (אתר, אפליקציה וכד') היה נתקע עד לקבלת הקלט.

אז איך בכל זאת מאפשרת שפת Javascript פעולות המצריכות המתנה (למשתמש, לשרת וכד')? התשובה לכך היא, לדעתי, המפתח להבנת Javascript.

פונקציות Callback ותכנות מבוסס אירועים

ניקח את הדוגמה הקודמת ונשכתב אותה למשהו דומה יותר לקוד Javascript:

function handle_user_input(name) {
    console.log("Welcome " + name);
}

wait_for_user_input(handle_user_input);

למעשה שינינו שלושה דברים:

  1. הגדרנו פונקציה חדשה המטפלת בקלט שהגיע מהמשתמש. או במילים אחרות, הגדרנו event handler, כאשר האירוע (event) בו הוא מטפל הוא הגעת קלט מהמשתמש.
  2. העברנו את הפוקציה החדשה בתור פונקציית callback לפונקציה wait_for_user_input. כלומר, רק כאשר יתקבל הקלט מהמשתמש, הפונקציה שלנו שלנו תיקרא ותכתוב את הקלט ללוג.

ע"י ביצוע שינוי פשוט זה הפכנו את הקוד ל-non blocking, כלומר הקוד לא יעצור ויחכה לקלט, אלא ימשיך לרוץ וכאשר יגיע קלט – הוא יטופל מיד (כמובן שיש צורך לשנות את המימוש של wait_for_user_input, אבל כרגע זה פחות חשוב).

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

wait_for_user_input(function(name) {
    console.log("Welcome " + name);
})

אני מאמין שהשינוי הקטן הזה הוא המפתח להבנת קוד Javascript. אז מה קרה פה? במקום להגדיר בנפרד את פונקציית ה-callback ולתת לה שם, הפכנו אותה לפונקציה אנונימית (anonymous function) והעברנו אותה ישירות לפונקציה wait_for_user_input.

השינוי הזה הפך את הקוד למשהו שלא דומה לקוד של אף שפת תכנות קלאסית!

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

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

document.getElementById("btn").addEventListener("click", function(evt) {
  if (validate_data()) {
    do_ajax_call(function (data) {
      if (data.success) {
        document.getElemenetById("next").addEventListener("click", function() {
          window.location.href = '/thank-you';
        });
      } else {
        console.log("error: " + data.msg);
      }
    });
  }
});

לא עוד Callback Hell

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

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

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

fetch_data_from_server(function(data) {
    // handle data
    fetch_more_data_from_server(more_data) {
        // handle more data
    }
});

ניתן שמות לפונקציות האנונימיות ונחלץ אותן החוצה:

fetch_data_from_server(handle_data);

function handle_data(data) {
    // handle data
    fetch_more_data_from_server(handle_more_data);
}

function handle_more_data(more_data) {
    // handle more data
}

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

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

Callback Hell

לקריאה נוספת:

7 טעויות נפוצות של מפתחים צעירים – ואיך להימנע מהן

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

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

טעות #1 – לא לשאול מספיק שאלות

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

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

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

טעות #2 – להמציא את הגלגל

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

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

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

טעות #3 – לכתוב יותר מדי קוד בבת אחת

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

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

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

טעות #4 – לא להעתיק מאחרים

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

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

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

var sum = 0;
for (var i = 0; i < array.length; i++) {
    sum += array[i];
}

לעומת זאת, חיפוש פשוט בגוגל יראה שניתן לממש גם בשורה אחת:

var sum = array.reduce((a, b) => a + b, 0);

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

טעות #5 – להעתיק קוד בלי להבין אותו

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

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

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

טעות #6 – לעצור כשזה עובד

רבות דובר על definition of done, או במילים אחרות: "מתי מחליטים שמשהו גמור?". מפתחים צעירים נוטים להכריז "משימה הושלמה" מיד כאשר הקוד שלהם רץ בהצלחה בפעם הראשונה, ומדלגים על אחד השלבים החשובים ביותר בכתיבת קוד – בקרת האיכות.

אז רגע לפני שאתם רצים ללוח המשימות ומסמנים וי, עצרו, ושאלו את עצמכם את השאלות הבאות:

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

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

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

טעות #7 – לפחד מבאגים

מזל טוב, סיימתם לכתוב קטע קוד והגיע הזמן לשחרר אותו לעולם הגדול! אבל רגע… מה אם הקוד לא טוב מספיק? מה אם הוא מכיל באגים קריטיים שיביאו לסוף העולם? ומה אם…

עצרו!

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

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

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

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

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

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

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

כתבו קוד קריא

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

  • שמות בהירים יותר למשתנים. ולא, 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 לצפייה בהיסטוריית השינויים של הקובץ.