facebook pixel מדריך: כותבים קוד יותר טוב - תיעוד - www.4project.co.il
Main logo www.4project.co.il
כל הרכיבים לפרוייקט שלכם
עגלת קניות

העגלה ריקה

כותבים קוד יותר טוב - תיעוד


2025-08-02 13:49:37

הקדמה

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

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

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

ההתייחסות שלי תהיה בעיקר לשפת C/C++, המשמשת בפיתוח לכרטיסי Arduino, אבל כל הפרקים בסדרת מדריכים אלה יהיו רלוונטיים גם לשפות וסביבות פיתוח אחרות.

אתם לא לבד

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

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

תיעוד

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

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

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

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

איך לתעד קוד

בואו נתחיל מהדבר הבסיסי ביותר - איך אפשר להוסיף תיעוד בתוך הקוד.

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

ב-C ו-C++ יש שני דרכים לעשות זאת:

תווים //

כל מה שתכתבו אחרי שני קווים נטויים // יחשב כהערה ולא יכלל בקומפילציה

קוד: בחר הכל
// This is a comment line
x = a + b; // Can be added after code as well

סימון זה מיועד להוספת הערה של שורה אחת

בלוק /* … */

אם אתם צריכים לכתוב הסבר קצת יותר ארוך משורה, אפשר להשתמש בבלוק שמתחיל ב-*/ ומסתיים ב-/*. כל מה שיופיע בין לבין יחשב כהערה ולא יכלל בקומפילציה.

קוד: בחר הכל
/* This is line 1 of the comment
    Line 2
    Line 3 */


אפשר להשתמש בבלוק הערות גם בשורה אחת:
קוד: בחר הכל
x = a + b; /* This is another way to add one line of comment */


וגם להפך, אפשר לכתוב מספר שורות של הערות שכל אחת מהן מתחילה ב-//:
קוד: בחר הכל
// This is line 1 of the comment
// Line 2
// Line 3


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

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

כלים אוטומטיים ליצירת תיעוד

לפעמים תהיו נדרשים להשתמש בכלים אוטומטיים שמייצרים תיעוד של הקוד כמסמך חיצוני. אחד הכלים הפופולריים שיצא לי להשתמש הוא doxygen. כדי שהכלי הזה יוכל לעבוד, תצטרכו להוסיף כל מני סימונים לתוך ההערות שלכם, שיעזרו לכלים אלה להבין למה אתם מתכוונים. לדוגמה, זו הדרך לתאר פונקציה עם ציון של כל מני מאפיינים שונים (כמו הפרמטרים, ערך המוחזר וכו') כדי שהמסמך הנוצר יראה בצורה אחידה ומקצועית:
קוד: בחר הכל
/**
* @file factorial.h
* @brief This file contains the declaration of the factorial function.
*/

/**
* @brief Calculates the factorial of a non-negative integer.
*
* @param n The number for which the factorial is to be calculated.
* @return The factorial of the number.
*
* @details
* The factorial of a non-negative integer n is the product
* of all positive integers less than or equal to n. For
* example, the factorial of 5 is 5 * 4 * 3 * 2 * 1 = 120.
* This function uses recursion to compute the factorial.
*
* @note This function is designed for non-negative integers.
*       Inputting a negative number may lead to undefined behavior.
*/
int factorial(int n);


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

מה לתעד

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

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

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

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

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

נעבור על כל מקרה מהרשימה.

הגדרת פונקציה

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

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

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

הגדרת משתנים מיוחדים

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

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

אם אתם מגדירים מערך של משתנים והקוד זקוק לכמות מסויימת מסיבה כלשהי, תסבירו מה הדרישה.
דוגמה - אתם צריכים לטפל במחרוזת באורך מקסימלי של 64 תווים, אבל ההגדרה של המחרוזת מוסיפה 1:
קוד: בחר הכל
char string[MAX_STRING_LENGTH+1];


תוסיפו הערה שמסבירה שצריכים מיקום נוסף לתו 0 שקיים בסוף כל מחרוזת בשפת C:
קוד: בחר הכל
// +1 for string NULL termination
char string[MAX_STRING_LENGTH+1];


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

אלגוריתמים וחישובים ארוכים

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

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

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

אפשר לחלק לחישוב של המונה בנפרד, ושל המכנה בנפרד, ואז שורה נוספת שתחלק ביניהם:
קוד: בחר הכל
float multR;
float totalR;
float result;
// Numerator calculation
multR = R1*R2;
// Denomerator calculation
totalR = R1+R2;
// Result
result = multR/totalR;


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

בלוקים לוגיים של קוד

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

הנה דוגמה של פונקציית ()setup בפרוייקט בו אני משחק עם מערכת Plug-n-Play שאני מפתח בימים אלה. גם כשזה מיועד לעצמי כרגע, אני מוסיף את ההערות האלה בצורה אוטומטית כי אני פשוט כבר רגיל לכתוב ככה. תאמינו לי שתקתוק על המקלדת זה חלק קטן מהזמן שתבלו מול הקוד שלכם. בדיקות ו-debug לוקחים הרבה יותר זמן ממה שאתם חושבים, ולראות את הקוד מסודר יפה, עם הערות שמופיעות בצבע שונה ברוב סביבות הפיתוח (ירוק ב-VSCode+PlatformIO), עוזר מאוד לעבור על הקוד במהירות כדי להגיע לדברים החשובים לכם באותו הרגע.

בנוסף לכל זה, כשאצטרך לכתוב מדריך למערכת Plug-n-Play, הקוד כבר יהיה יפה ומתועד.
קוד: בחר הכל
void setup()
{
    uint8_t rc;

    // Wait for serial connection
    while (!serial) {}

    // EBF is the first thing that should be initialized, with the maximum timers to be used
    EBF.Init(NUMBER_OF_TIMERS, 16);

    // Timer will just print current temperature for debug
    EBF.InitTimer(TEMPERATURE_TIMER, onTemperatureTimer, 1000);
    EBF.StartTimer(TEMPERATURE_TIMER);

    // Timer will clear the second line of the LCD after some time
    EBF.InitTimer(STATUS_RESET_TIMER, onStatusResetTimer, 2000);

    // Status LED initialization
    statusLed.Init();

    // Serial initialization
    serial.Init();
    serial.println("Starting...");

    // LCD initialization
    rc = lcd.Init();
    serial.print("LCD init = ");
    serial.println(rc);

    lcd.print("Hello PnP!");

    // Temperature sensor initialization
    rc = tempSensor.Init();

    serial.print("Sensor init, rc = ");
    serial.println(rc);

    // 1HZ measurement mode
    tempSensor.SetOperationMode(EBF_STTS22H_TemperatureSensor::OperationMode::MODE_1HZ);

    // Register high and low thresholds callbacks
    tempSensor.SetOnThresholdHigh(onTemperatureHigh);
//    tempSensor.SetOnThresholdLow(onTemperatureLow);

    // Set threshold levels
    tempSensor.SetThresholdHigh(29.0);
//    tempSensor.SetThresholdLow(20.0);

    // Buttons module initialization
    rc = buttons.Init();
    serial.print("Buttons init = ");
    serial.println(rc);

    // Assign callbacks
    for (uint8_t i=0; i<buttons.numberOfButtons; i++) {
        buttons.SetOnPress(i, onButtonPress);
        buttons.SetOnRelease(i, onButtonRelease);
        buttons.SetOnLongPress(i, onButtonLongPress);
    }
}


דברים "לא סטנדרטיים"

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

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

אתם מבצעים לולאה מסוף המערך להתחלה בגלל סיבה כלשהי? תסבירו למה.
אתם עושים שני מעברים על המערך מסיבה כלשהי? תוסיפו הסבר למה.
אתם עוברים קודם על כל המיקומים האי-זוגיים ואחרי זה על זוגיים? תוסיפו הסבר.
אתם עושים לולאה בתוך לולאה עם חישוב המשתמש בשני האינדקסים של הלולאות בצורה כלשהי לפיו אתם ניגשים למיקום במערך? תסבירו.
אתם מקדמים את האינדקס של לולאה ב-4 ולא ב-1? (אני מתכוון ל-++i שבדרך כלל עושים בלולאות). אתם רוצים להתייחס לערכי הבתים בתוך המערך של בתים כ-integer של 32 ביט? תסבירו את זה במילים.
החלטתם להשתמש ב-()while במקום ב-()for כדי שאפשר יהיה לקדם את האינדקס בקפיצות שונות תלוי בערכים במערך? תסבירו את השיקולים ואת הערכים שגורמים לקפיצות שונות.
בחרתם ב-()do/while במקום ה-()while כי אתם רוצים לעשות חישוב לפני שניגשים למערך? תתעדו זאת.
אתם עושים casting בין סוגים שונים של נתונים? תסבירו למה.

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

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

הנה עוד דוגמה טובה לסיום:
אתם רוצים לבדוק ביט מספר 3 במשתנה של 32 ביט, האם הוא דלוק או כבוי.
הפעולה "הסטנדרטית" יחסית, היא להזיז את ערך המשתנה 3 ביטים ימינה, לעשות AND לוגי עם 0x01 ולבדוק אם התוצאה היא אפס או אחד.

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

ואם אתם רוצים להיות עוד יותר צדיקים, רצוי להוסיף פונקציות עזר עם שם ברור שיבצעו את ההזזה וה-AND בתוכן, כך שהקוד יהפוך להרבה יותר ברור בכל מקרה שתצטרכו לבדוק את הביט הזה. למשל כך:
קוד: בחר הכל
// Returns LCD enable flag bit[3]
unsigned char GetLCDEnableFlag(unsigned int statusFlags) {
    return ((statusFlags >> 3) & 0x01);
}

if(GetLCDEnableFlag(statusFlags) == 0) {
    BlinkStatusLed();
}


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

תוויות:


מדריכים נוספים:

  • מדריך עבודה עם VSCode+PlatformIO

    עבודה עם VSCode+PlatformIO

    2025-05-05 14:42:01

    במדריך זה נראה איך מתקינים PlatformIO בתוך VSCode. נוסיף כמה פרוייקטים לכרטיס Arduino, נוסיף ספריות ונראה לכם איך עובדים עם זה.

  • מדריך מצחיק שאתה צריך לשאול - מה זה SDA ו-SCL?

    מצחיק שאתה צריך לשאול - מה זה SDA ו-SCL?

    2025-01-28 09:14:27

    ידידינו מ-SparkFun מפרסמים סדרה של סרטונים קצרים בנושאי האלקטרוניקה השונים, שהם קוראים לה "מצחיק שאתה צריך לשאול"...
    השאלה בסרטון זה היא: מה זה SDA ו-SCL?

  • מדריך מצחיק שאתה צריך לשאול - מה זה TX ו-RX?

    מצחיק שאתה צריך לשאול - מה זה TX ו-RX?

    2025-01-27 09:12:28

    ידידינו מ-SparkFun מפרסמים סדרה של סרטונים קצרים בנושאי האלקטרוניקה השונים, שהם קוראים לה "מצחיק שאתה צריך לשאול"...
    השאלה בסרטון זה היא: מה זה TX ו-RX?

  • מדריך מצחיק שאתה צריך לשאול - מה זה EEPROM?

    מצחיק שאתה צריך לשאול - מה זה EEPROM?

    2025-01-26 09:51:02

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

  • מדריך מצחיק שאתה צריך לשאול - מה זה EPROM?

    מצחיק שאתה צריך לשאול - מה זה EPROM?

    2025-01-24 10:45:44

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