2013-07-08

קוד ספרותי = סופן של ההערות בקוד?

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

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

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





טרמינולוגיה: קצת סדר

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

בפוסט זה אני רוצה להתמקד ב"קוד הספרותי" (לא לבלבל עם Literate Programming - דבר אחר לגמרי) בלבד. לא בגלל שמבנה הוא פחות חשוב (חלילה!) - פשוט אחרת לא אגמור את הפוסט.

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

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

גישת הקוד הספרותי היא נפוצה - אם כי איננה קונצנזוס. היא התבססה בעיקר בעקבות 3 ספרים "פורצי-דרך":


מכתב בשפת Ruby. מקור.


מהו "קוד ספרותי"?

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


קוד ספרותי מבוסס על שני עקרונות:

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

האמת ש"קוד ספרותי" הוא שם לא-מדויק, אולי אף מעט מטעה:
סיפור של שייקספיר (מתפלפל) או של ג'ורג .ר.ר מרטין (לא-נגמר) - הם לא המודלים אליהם אנו שואפים. המודל מדויק יותר יהיה עיתון / "קוד עיתונאי":
  1. תמציתי.
  2. ברור וחד-משמעי.
  3. מדויק.
  4. קל לקרוא קטעים ממנו.
    ניתן לקפוץ לעמוד 6' לקרוא פסקה ולהבין - מבלי שקראנו את כל העיתון. זאת בכדי שנוכל להתמקד בקטעי קוד שמעניינים אותנו כרגע, מבלי שנזדקק לקרוא מאות שורות של קוד קודם לכן בכדי להבין את הקטע המעניין.


עיקרון ב': תיעוד עצמי (Self-Documentation)
על הקוד לתאר את עצמו ולהבליט את הכוונה.
כל פעם שאנו מוסיפים הערה - זו נורת אזהרה שכתבנו קוד שלא מסביר את עצמו. עלינו לנסות להסיר את ההערה ולגרום לקוד לבטא את המסר ללא עזרה.


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



אז כיצד כותבים קוד "ספרותי"?

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


נתחיל במתן שמות:
// bad
var ic; // says nothing
function monitorTransIP() // what is IP?!
var hashUrl = "ae4a0192#erlkde"; // url of a Hash?
// good
int itemCount;
function monitorInProcessTransactions() // proper English
var urlHash = "ae4a0192#erlkde"; // no. a Hash of a URL...


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

לא קל לקלוע ישר לשמות מוצלחים. ישנן 4 "דרגות" של שם:
  1. שם סתמי - המחשה: NetworkManager
  2. שם נכון - המחשה: AgentCommunicationManager
  3. שם מדויק - המחשה: AgentUdpPacketTracker
  4. שם בעל משמעות ("meaningful") - המחשה: AgentHealthCheckMonitor*
* כמובן שהשם AgentHealthCheckMonitor הוא מוצלח רק במערכת בה שם זה מתאר בדיוק וביתר משמעות את אחריות המחלקה. נתתי דוגמאות להמחשה ממערכת שאני מכיר וחושב עליה - כמובן השמות שציינתי לא נכונים / מדויקים / בעלי משמעות באופן אוניברסלי, אלא רק למערכת הספציפית.

עצלנות ולחץ גורמים לנו להיצמד לתחתית הסקלה (1,2), בעוד הקפדה ומקצועיות דוחפים אותנו לראש הסקלה (3,4).

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


מתי מותר להשתמש בקיצורים?
קשה לטעון שהקוד הבא הוא לא קריא:
for (int i = 0; i < ObjList.length; i++){
    // doSomething
}
אף על פי ש i ואפילו objList הם לא שמות ברורים באנגלית.
מדוע אם כן אנו מצליחים לקרוא את הקוד? א. יש בו קונבנציה מאוד ברורה. ב. אנו רואים במבט אחד את אורך החיים של i וכך מבינים בדיוק מה הוא עושה.

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

דוגמה:
// bad
for (int iterationIndex = 0; iterationIndex < l.length; iterationIndex ++){ 
    // doSomething(l[iterationIndex]) - what is "l" ?!?!
}
// good
for (int i = 0; i < completedTaskList.length; i++){
    // doSomething(completedTaskList[i])
}
// better?
completedTaskList.forEach(function(task){
    // doSomething(task)
});

הדוגמה אחרונה אכן מקרבת אותנו לשפה טבעית ("forEach") וגם מקצרת את הקוד, אולם יש בה גם נקודה חלשה: היא שברה במעט את רצף הקריאה. באנגלית אנו נוהגים לומר: "...for each completed task" בעוד דוגמת הקוד דומה יותר ל "...with completed tasks, for each" (סוג של: "אבא שלי, אחותו ..." במקום "אחות של אבי") - שפה קצת מקורטעת.
ספציפית בג'אווהסקריפט יש תחביר של for... in ששומר אפילו טוב יותר על רצף הקריאה, אבל מציג כמה pitfalls משמעותיים - ולכן אני נמנע ממנו.
בסופו של דבר אנו מוגבלים לאופציות הקיימות בשפת התכנות, ועלינו להחליט איזו אופציה אנו מעדיפים. כדאי לשקלל את כל המרכיבים לפני שבוחרים.

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



ג'ורג' אורוול. סופר ועיתונאי, מודל "לכתיבה עיתנואית":
"Never use a long word where a short one will do"



שמירה על רצף קריאה

הנה כמה דוגמאות כיצד ניתן לחזק את רצף הקריאה:
// not very good
if (node.children() && node.connected()) {
  // doSomething
}

// better
if (node.hasChildren() && node.isConnected()) {
  // doSomething
}

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


בשפת Java, מקובל לכתוב:
if ("someValue".equals(myString)) { ... }

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

if (myString.equals("someValue")) { ... }

עומס טקסט כמובן גם משפיע לרעה על קלות הקריאה. הייתי שמח לו הייתי יכול לכתוב בג'אווה:
if (myString == 'someVale') { ... }
ג'אווה היא שפה מרבה-במילים (verbose), תכונה המעמיסה טקסט על המסך ומקשה על הקריאה הקולחת.

באופן דומה, עבור הקורא:
if (myString.isEmpty()) { ... }
יותר קולח מקריאה של 
if (myString.equals("")) { ... }
למרות שהתבנית מאוד מוכרת.


הנה עוד דוגמה קטנה לכתיבה מעט שונה, אך קולחת יותר:
// switch => reader has to remember 'statusCode' = the context
switch (statusCode) {
  case 169 : // return Something();
  case 201 : // return Something();
  case 307 : // return Something();
  default: // return SomeOtherStuff();
}

// Better: each line is a complete sentence 
switch (true) {
  case statusCode == 169 : // return Something();
  case statusCode == 201 : // return Something();
  case statusCode == 307 : // return Something();
  default: // return SomeOtherStuff();
}
במקום שהעין תקפוץ כל הזמן ל statusCode להיזכר בהקשר (בדומה למשפטי "with"), כל משפט הופך למשפט שלם. כל זאת - בעזרת אותו סט כלים זמין בשפה.

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


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




"תיעוד עצמי" - סיפורו של מתכנת

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


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

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




העברת הערות לקוד


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

בואו ננסה!

ראשית ננסה להיפטר מ"מספר קסם" (Magic Number). מספר קסם הוא מספר שפתאום מופיע בקוד ולא ברור כיצד החליטו עליו. קסם.
/*= Huh?! =*/
totalHeight = $el.height + 14;


/*= Better =*/
totalHeight = $el.height + 6+6+1+1;


/*= Even Better =*/
// two times the border (6) + two times the margin (1)
totalHeight = $el.height + 6+6+1+1;


/*= Introduce constant; Even Better =*/
var BORDER_WIDTH = 6, MARGIN = 1;
totalHeight = $el.height + 2 * BORDER_WIDTH + 2 * MARGIN;


הערה: השתמשתי בהערות מסוג /*= =*/ כמטה-הערות בהן אני משתמש להעיר על הקוד / ההערות.

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


בואו נראה דוגמה נוספת:
/*= Why do we need this comment ? =*/
// remove "http://" from the url
str = url.slice(7);


/*= Introduce constant; Slightly better =*/
var HTTP_PREFIX_LENGTH = 7;
str = url.slice(HTTP_PREFIX_LENGTH);


/*= comment -> code; Better =*/
str = url.slice('http://'.length);


הצלחנו לבטל את ההערה, ולהפוך אותה לחלק מהקוד - קוד קריא. נהדר!


פירוק של ביטויים לא ברורים לאיבר נוסף עם שם ברור אינה שמורה רק למשתנים. הנה טיפול ב"ביטוי קסם":
/*= Why do we need this comment ? =*/
// check if document is valid
if ((aDocument.isAtEndOfStream() && !aDocument.hasInputErrors()) &&
    (MIN_LINES <= lineCount && lineCount <= MAX_LINES)) {
    print(aDocument);
}


/*= extract method; comment -> code =*/
if (isDocumentValid(docStream, lineCount)) {
    print(aDocument);
}


הוצאנו (extract) פונקציה, נתנו לה שם ברור - וביטלנו את הצורך בהערה!


עד כאן טוב ויפה. יש מקרים שבהם נדמה לרגע שאין לנו דרך סבירה לבטל את ההערות, שהן פשוט נדרשות:
/*= We need these comments to highlight sections, don't we? =*/
function foo(ObjList){
    var result = [], i;

    // first fill objects
    for (i = 0; i < Objlist.length; i++){
        // doSomething
    }

    // then filter disabled items
    for (i = 0; i < result.length; i++){
        // doSomething
    }

    // sort by priority
    result.sort(function(a, b) {
        // apply some rule
    });

    return result;
}


/*= Extract Methods; comments -> code =*/
function foo(ObjList){
    var result = [];

    result = fillObjects(Objlist);
    result = filterDisabledItems(result);
    result = sortByPriority(result);

    return result;
}


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

האם גם בדוגמה הבאה ניתן לוותר על ההערה?
function calcRevenue(){<
    /*= Walla! This comment is Absolutely Irreplaceable! =*/

    // Order Matters!
    calcMonthlyRevenue();
    calcQuartrlyRevenue();
    calcAnnualRevenue();
}


function calcRevenue(){
    /*= Hmmm... better luck next time =*/

    var lastMonthRevenue = calcMonthlyRevenue();
    var lastQuarterRevenue = calcQuartrlyRevenue(lastMonthRevenue);
    calcAnnualRevenue(lastQuarterRevenue);
}


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

אני מניח שבשלב זה הרעיון כבר ברור. הייתי רוצה לקנח בדוגמה לא מפתיעה, אך חשובה: מבני נתונים
/*= Custom data structure: working, but not descriptive =*/
var row = new Array[2]; // team's performance
row[0] = "Liverpool";
row[1] = "15";


/*= Comments -> code; but what are 0 & 1? =*/
var teamPerformance = new Array[2];
teamPerformance[0] = "Liverpool";
teamPerformance[1] = "15";


/*= Introduce Class =*/
var tp = new TeamPerformance();
tp.name = "Liverpool";
tp.wins = "15";




ביקורת

ישנן גם התנגדויות לגישת "הקוד הספרותי". הנה ביקורת מפורסמת (וצבעונית) שהתפרסמה. הנה התגובה של דוד בוב.

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

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



סיכום

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

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

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

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

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

איך מתחילים לכתוב "קוד ספרותי"?
הכי פשוט להיצמד לכלל הצופים (The Boy Scout Rule): "השאר את השטח נקי יותר ממה שקיבלת אותו".
כל פעם שאתם נוגעים בקוד ומבצעים שינוי - שפרו מעט את הקריאות וקדמו מעט את הקוד לעבר "קוד ספרותי". עם הזמן - השינוי יהיה ניכר ויגיעו גם התוצאות.



שיהיה בהצלחה!

14 תגובות:

  1. פוסט מעולה (וספרותי) ליאור!

    בהקשר אליו, אני רוצה לציין את "פיתוח-מונחה-התנהגות" או BDD.
    זהו תהליך שבו כותבים Unit Tests שמתארים ובודקים את הקוד בו-זמנית.

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

    יישר כח!
    שי מסיסטרנו.

    השבמחק
  2. סה"כ אני מאוד מסכים
    אני נתקל בכל מיני קיצורים כמו
    int ind;
    for(ind = 0; ind < 10; ind++)
    וזה מטריף לי את השכל ..

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

    // Better: each line is a complete sentence??
    switch (true) {
    case statusCode == 169 :
    // doSomething();
    break;
    case statusCode == 201 :
    // doSomething();
    break;
    case statusCode == 307 :
    // doSomething();
    break;
    default: // doSomeOtherStuff();
    }

    השבמחק
    תשובות
    1. היי,

      אני מסכים לגבי הקיצורים המעצבנים. ind נראה קיצור מעצבן במיוחד !! :)

      לגבי הביטוי למטה: (switch)
      "טבלאות קפיצה" הן אופטימיזציה אופציונלית. אם הקומפיילר לא מצליח לעשות זאת (כמו במקרה הנ"ל) - אז הוא פשוט יקמפל את הקוד למשפטי if else. הקוד רץ.

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

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

      ליאור

      מחק
  3. עדכון:

    מסיבה כלשהי הכללתי בדוגמה של ה switch תחביר עם breaks.
    אני בעצמי נמנע מלהשתמש בתחביר של switch כאשר נדרשים breaks: הוא מסורבל יותר ושכחה של break יחיד - יכולה לגרום לבעיות. במקרים כאלו: עדיף להשתמש בשרשרת if-else.

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

    תודה על התגובות.

    השבמחק
  4. עופר פלדמן9/7/13 10:50

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

    השבמחק
    תשובות
    1. היי עופר,

      ייתכן ואתה צודק. אני חשבתי על HTTP Codes (סתם זרקתי 169), וקשה לי לחשוב על שם משמעותי עבור 200 או 404 יותר מ 200 ו 404 :)

      צריך לראות את דוגמת הקוד הספציפית ולהחליט.

      ליאור

      מחק
  5. אנונימי12/7/13 00:09

    מעולה, כרגיל. :)

    יש פה, עם זאת, בעיה שתמיד מציקה לי כשאני עושה הרבה Extract כדי לתת לקטעי קוד שמות משמעותיים וכדי שכל פונקציה תעשה דבר אחד בלבד. לא אכפת לי שאין רצף קריאה, אבל מאד מטריד אותי שיש המון פונקציות שאין להן שימוש בשום מקום אחר (- ו*אין* להן שימוש במקום אחר, וגם לא יהיה), והן חשופות לSCOPE הפנימי (private וכד'). לדוגמה: יש שתי פונקציות אחיות (נניח, באותה מחלקה) - A ו-B. את התוכן של A פירקתי ל-X,Y,Z. עשכיו בכל פעם שאני ארצה לערוך את B, הitellisense יציע לי את X,Y,Z שאין להן שום שימוש ב-B. שכתוב של הקוד כדי לשמור על הקריאות שלו בדרך הזו, מזהם את הסביבה. אם היתה דרך לכתוב את הפונקציות המשניות (X,Y,Z) בתוך הראשית (A) זה היה טוב.

    למעשה, יש דרך לפחות בחלק מהשפות. נניח ב-JS אפשר להשים פונקציה אנונימית למשתנה פנימי בעל שם משמעותי ולקרוא לה, וגם ב-C# אפשר להשים lambda expression למשתנה פנימי בעל שם מסוג DELEGATE ולקרוא לה. הבעיה היא שאז גם המשתנים של A חשופים ל-X,Y,Z; דבר שאנחנו רוצים להמנע ממנו. זה גם נראה מוזר (אולי מתרגלים עם הזמן?). ב-C# יש לזה גם השלכות מרגיזות על דיבאג (אי אפשר לשנות מתודה שיש בה למבדה אקספרשן תו"כ ריצה).

    השבמחק
    תשובות
    1. היי אנונימי,

      אני מזדהה עם הדילמה :)

      ראשית: אלו כבר צרות של עשירים.

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

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

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

      ליאור

      מחק
    2. אני מסכים עם ליאור בגישה הזאת.
      ממולץ לשמור על העקרון SRP (Single responsibility principle) ולשם כך המחלקות צריכות להיות קטנות בדרך כלל ללא הרבה פונקציות "ספרותיות".

      מחק
    3. אנונימי6/6/17 21:12

      אני האנונימי.

      רציתי לעדכן שבc#7 הקשיבו לצרות שלנו, ועשו פונקציות מקומיות, בתוך מתודות, שמתוגרמות בקימפול להיות מתודות של המחלקה. :-)

      כל מיני דברים על זה:
      https://asizikov.github.io/2016/04/15/thoughts-on-local-functions/

      מחק
  6. אנונימי13/7/13 13:30

    מצויין ומעניין.
    תודה רבה!

    השבמחק
  7. הבעיה שלי בחלק של ה switch case זה שנראה במבט מהיר שיש פה שכפול קוד לא נעים...

    השבמחק
  8. משה סייג20/7/13 08:51

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

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

    2. אני לא רואה בעיה בשימוש ב-switch כל עוד הוא קצר וכל case מכיל בדיוק קריאה אחת שמטפלת בו.

    נ.ב.
    "שפה טבעית" אינו מושג חדש. הוא נמצא בשימוש נרחב בתחומי מו"פ מסויימים, למשל ב"עיבוד שפות טבעיות" (Natural language Processing או NLP) העוסק בניתוח והבנה של שפה אנושית ע"י מחשב.

    נ.ב.ב
    Java היא אכן שפה "וורבוזית" מאוד אך זה הולך להשתפר בצורה משמעותית בקרוב עם תחילת השימוש ב-Java8 עם ה-closure המקוצר וה-type inference המשופר, שצפויה לצאת ב-3/2014.

    השבמחק
    תשובות
    1. היי משה,

      תודה על התוספות!

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

      ליאור

      מחק