איך נראה קוד טוב ?

[12 ביולי 2008] [8 תגובות]

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

אינדנטציה (indentation) - ובעברית: "הזחה"

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

הקוד הבא אינו ברור מכיוון שאינו משתמש בהזחות. נסו לקבוע כמה פעמים יודפס המשפט Where am I עד לסיום התוכנית:

while (x<100){
cout << "I am lost !" << endl;
if (i=0){
x++
cout << "Where am I ?" << endl;
i=50
}
i--;
}
cout << "Can you tell me where I am ?" << endl;

הקוד הבא משתמש בהזחות, הוא קריא וברור. כעת ניתן לראות מהן הפקודות השייכות לבלוק ה-if והיכן מסתיימת לולאת ה-while:

while (x<100){
        cout << "I am lost !" << endl;
        if (i=0){
                x++
                cout << "Where am I ?" << endl;
                i=50
        }
        i--;
}
cout << "Can you tell me where I am ?" << endl;

הערות (Comments)

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

קוד ללא הערות משאיר את מי שקורא אותו באפלה:

int x, y;
cin >> x;
cin >> y;
int c=0;
for (int i=0; i<y; i++){
                c+=x;
}
cout << "c";

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

/*
-----------------------------------------
The next few lines multiplies two numbers
-----------------------------------------
*/
int x, y;
/* Read two numbers from the console */
cin >> x;
cin >> y;
int c=0;  // c will hold the result, initialize it to zero
/* calculate (x*y) */
for (int i=0; i<y; i++){
                c+=x;
}
/* Output the result to the console */
cout << c;

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

שמות בעלי משמעות למשתנים ולפונקציות

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

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

נשנה את הקוד ונכניס משמעויות לשמות המשתנים. למשתנה x נקרא multiplicand (ניכפל), ל-y נקרא multiplier (כופל) ול-c נקרא result (תוצאה) :

/*
-----------------------------------------
The next few lines multiplies two numbers
-----------------------------------------
*/
int multiplicand, multiplier;
/* Read two numbers from the console */
cin >> multiplicand;
cin >> multiplier;
int result=0;
/* calculate (multiplicand*multiplier) */
for (int i=0; i<multiplier; i++){
                result+=multiplicand;
}
/* Output the result to the console */
cout << result;

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

שימוש בבלוקים גם אם לא חובה

הבלוקים הם הסימנים {} התוחמים מקטע קוד הנקרא בלוק. סימנים אלו נפוצים כאשר מסמנים את תחילתה וסופה של מחלקה (class), תחימת פונקציה, תחימת לולאות while ו-for ובכל פעם שיש לקבץ מספר פקודות ביחד. שימו לב לקוד הבא:

if (x==0)
        cout << "All Good" << endl;

קוד זה תקין, נכון ויעבוד מצויין. אם ערכו של x הוא אפס, יודפסו המילים All Good למסך. אין צורך בסימני הבלוק {} מכיוון שישנה רק פקודה אחת שאמורה להתבצע במידה ותנאי ה-if מתקיים.

למרות שהכל עובד כמו שצריך, מתהדר ורץ, יש כאן פתח לבעיה חמורה. מישהו בתמימות עלול להוסיף עוד פקודה לביצוע בתוך ה-if כך:

if (x==0)
        x=1;
        cout << "All Good" << endl;

כעת אם x שווה ל-0 ערכו יהפוך לאחד - כל זה יקרא בתוך ה-if, ולאחר ה-if תתבצע הדפסת All Good למסך. אם ערכו של x היה שונה מ-0, הוא לא יהפוך לאחד, אך השורה All Good כן תודפס למסך למרות שזה (כנראה) לא מה שהמתכנת התכוון לעשות.

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

if (x==0)
{
        cout << "All Good" << endl;
}
אי אפשר להתבלבל כאן. אם מישהו יוסיף קוד בעתיד, הוא ידע שאם הקוד נמצא בתוך הבלוק - הוא קשור ל-if ואם מחוץ לקוד - הוא יקרא בכל מקרה.

שבירת הקוד לפונקציות

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

void get_numbers(int *number_1, int *number_2){
        cin >> *number_1;
        cin >> *number_2;
}
int multiply(int multiplicand, int multiplier){
        int result=0;
        /* calculate (multiplicand*multiplier) */
        for (int i=0; i<multiplier; i++){
                result+=multiplicand;
        return result;
}
void display_result(int result){
        cout << "The correct answer is: " << result << endl;
}

int main(){
        int number_1, number_2;        

        // Get two numbers from the user
        get_numbers(&number_1, &number_2);

        // Multiply the numbers
        int result = multiply(number_1, number_2);
        
        // Output the answer
        display_result(result);
}

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

קוד ברור וקריא = קוד טוב

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

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

8 תגובות

  1. Itay Alon | 7/12/2008 8:26:25 AM

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

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

  2. טל | 7/12/2008 11:43:55 AM

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

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

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

  3. אנונימי | 7/24/2008 5:50:58 AM

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

  4. טל | 7/24/2008 10:24:05 AM

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

  5. אלי | 12/30/2009 12:36:11 PM

    הכל טוב ויפה 

    עד שזה מגיע ל .... הערות 

    הערות חסרות כשהקוד אינו ברור. הגישה המודרנית (XP, Agile, Scrum) מעודדת כתיבת קוד קריא ללא הערות.

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

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

    קוד אמורים לקרוא כמו ספר

    ספר מומלץ בנושא הוא Clean Code של הדוד בוב :)

  6. טל | 12/30/2009 3:02:59 PM

    אלי, אני גם נגד הערות כמו:

    //if dummy is true send a message

    if (dummy==false) send_message

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

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

    //Notify the user about the failure

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

    מה כבר אפשר לשנות שיהפוך את ההערה ללא נכונה?

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

  7. דימה ספקטור | 2/25/2013 10:54:03 AM

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

  8. אביב כהן | 9/25/2014 3:01:19 PM

    בגדול עצות טובות. הערה (pun unintended) אחת.

    לא לחלוטין מסכים עם מה שכתבת על כתיבת הערות. הערות במינון מינמלי זה אחלה, מעבר לזה יש עם זה שתי בעיות:

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

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

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

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

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

    כתבה טובה 

הוסף תגובה