כללים לכתיבת קוד איכותי, תקין ועמיד

כללים לכתיבת קוד איכותי, תקין ועמיד

  • פרסומת

פיתוח תוכנה נחשב לאחד המקצועות היותר צעירים באקדמיה ובתעשיה, ואכן, רק לאחרונה התפתח תחום זה באופן המאפשר יצירתן של תוכנות איכותיות, יציבות ועמידות לאורך זמן. בעבר הלא רחוק, פיתוח תוכנה נחשב לעניין מסובך שנועד לכישלון ברוב המקרים. בשנת 2006 נערך מחקר שעקב אחרי 9,000 פרוייקטים שונים של פיתוח תוכנה. מתוך הפרוייקטים שנמצאו תחת מעקב, רק 35% הסתיימו בהצלחה. 46% הסתיימו באיחור או בעיות אחרות, וכ-19% בוטלו.[1] זוהי בהחלט תמונת מצב עגומה. עם הזמן, קהילת מפתחי התוכנה ומדעני המחשב המציאה מערכת של חוקים ששמירה עליהם מבטיחה את תפקודה התקין של התוכנה לאורך זמן.

התוצאות של יותר מ-9,000 פרוייקטים של פיתוח תוכנה שהסתיימו בשנת 2006 [Rubenstein, 2007]

התוצאות של יותר מ-9,000 פרוייקטים של פיתוח תוכנה שהסתיימו בשנת 2006 [Rubenstein, 2007]


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

הערכה של ההוצאות הכלכליות (באחוזים מהתקציב הסופי) של שלבי הפיתוח לעומת שלבי התחזוקה. (a) בין 1976-1981 ו-(b) בין 1992-1998 [Schach, 2011]

הערכה של ההוצאות הכלכליות (באחוזים מהתקציב הסופי) של שלבי הפיתוח לעומת שלבי התחזוקה. (a) בין 1976-1981 ו-(b) בין 1992-1998 [Schach, 2011]

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

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

תכנון ראשוני (Design)

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

דיאגרמת UML. נלקח מהאתר Creately.com

דיאגרמת UML. נלקח מהאתר Creately.com

מסמך הדרישות

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

ניתוח מסמך הדרישות

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

אנליזה ותכנון

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

יישום מעלה-מטה (Top-Down Implementation)

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

הפשטה (Abstraction)

הפשטה בעולם התוכנה מתייחסת להפרדה בין רעיון מסויים, לבין המימוש של אותו הרעיון. מטרת התהליך היא להפוך את העצמים והפונקציות לכמה שיותר כלליות, ועליהם ״להלביש״ עצמים פרטניים יותר לפי הצורך. בשפות רבות תהליך ההפשטה הפך לחלק מהשפה על ידי שימוש במילים כמו abstract בשפות כמו PHP ו- Java או virtual בשפת C++.

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

class Circle {
    private width;
    private height;
    
    public Circle( int width, int height ) {
        this.width = width;
        this.height = height;
    }

    public void draw() {
        // Draw circle
    }
}

class Rectangle {
    private width;
    private height;
    
    public Rectangle( int width, int height ) {
        this.width = width;
        this.height = height;
    }

    public void draw() {
        // Draw rectangle
    }
}

אפשר לרכז את המאפיינים המשותפים וליצור היררכיה של עצמים החולקים את המשותף:

abstract class Shape {
    private width;
    private height;
    
    public Shape( int width, int height ) {
        this.width = width;
        this.height = height;
    }

    abstract public void draw();
}

class Circle extends Shape {
    public void draw() {
        // Draw circle
    }
}

class Rectangle extends Shape {
    public void draw() {
        // Draw circle
    }
}

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

מודולציה (Modular Design)

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

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

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

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

כתיבת פונקציות קצרות וייעודיות

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

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

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

<?php
function add_and_multiply( $number, $add, $multiply ) {
    return ( $number + $add ) * $multiply;
}

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

<?php
function add( $number, $add ) {
    return $number + $add;
}

function multiply( $number, $multiply ) {
    return $number * $multiply;
}

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

<?php multiply( add( $number, $add ), $multiply);

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

יצירת ממשק תכנות יישום (Application Programming Interface)

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

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

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

כתיבת הערות

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

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

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

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

/**
 * Implements a Human class.
 * 
 * Use this class to create a human being.
 *
 * Example Usage:
 *
 * Human human = new Human();
 * human.setAge( 25 );
 * human.setGender( "Male" );
 * human.walk();
 */
Class Human { }

הערות לפונקציות – גוש זה יכיל הסבר על מהות הפונקציה, הפרמטרים (המשתנים המוזנים לפונקציה – Parameters), הארגומנטים (המשתנים הנמצאים בתוך הפונקציה – Arguments) ומה היא מחזירה (Return Value).

/**
 * Get the age of a person.
 *
 * @param Person p The person who's age will be returned.
 *
 * @return int The person's age.
 */
int getAge( Person p )
{
    return p.age;
}

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

class Person
{
    /**
     * The age if this person.
     * @var int The age.
     */
    private int age;
}

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

boolean isPrimeNumber( unsigned long n )
{
    // The primality of integers under 4 is certain.
    if( n > 3 )
    {
        // Loop through all possible factors of n.
        for( unsigned long i = 2; i <= (n/2); i++ )
        {
            // If there is no remainder for the modulus operation then i is a factor of n.
            if( n % i == 0 )
            {
                return false;
            }
        }
    }
    return true;
}

בחירת שמות (Naming Conventions)

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

class R
{
    private String n;
    private int w;
    private int h;

    public R( String n, int w, int h )
    {
        this.n = n;
        this.w = w;
        this.h = h;
    }

    public String gn() { return this.n; }
    public int gw() { return this.w; }
    public int gh() { return this.h; }
    public int ga() { return this.h * this.w; }
}

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

class Rectangle
{
    private String name;
    private int width;
    private int height;

    public Rectangle( String n, int w, int h )
    {
        this.name   = n;
        this.width  = w;
        this.height = h;
    }

    public String getName() { return this.name; }
    public int getWidth() { return this.width; }
    public int getHeight() { return this.height; }
    public int getArea() { return this.height * this.width; }
}

חשוב גם לשמור על אחידות בבחירת השמות. קיימות שתי אסכולות לבחירת שמות: CamelCase ו underscore_case.
שתי האסכולות נכונות, אך אני נוטה להתחבר יותר לאסכולה השניה משום שהיא קלה יותר לקריאה. לדוגמה:

 aRatherLongSymbolName

הרבה פחות מובן מ:

 a_rather_long_symbol_name

מידע נוסף ניתן לקרוא כאן.

ניידות הקוד

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

ניטרול שגיאות

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

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

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

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

[1] Schach, Stephen R. (2011). Object-Oriented and Classical Software Engineering. New York: McGraw Hill. (P. 5)
[2] Schach, Stephen R. (2011). Object-Oriented and Classical Software Engineering. New York: McGraw Hill. (P. 11)
[3] Spolsky, Joel. (2000). The Joel Test: 12 Steps to Better Code. joelonsoftware.com/articles/fog0000000043.html