תגיות: , ,
2 תגובות

"תכנות הוא האמנות של להגיד לאדם אחר מה הוא רוצה שהמחשב יעשה" (דונאלד קנות')

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

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

מתוך הבנת הנקודות האלה (ועוד כמה) נולדה מתודולוגיה בעולם הפיתוח שנקראת CLEAN CODE.

האיש המזוהה ביותר עם CLEAN CODE הוא רוברט מרטין המכונה 'הדוד בוב' בספרו בעל השם המפתיע CLEAN CODE.

ספר זה מצטרף לעוד ספרים המדברים על איכות הקוד ועל איכות המתכנת. שני ספרים טובים ומפורסמים שאני מכיר הם: The Pragmatic Programmer ו Code Complete 2.

123

 

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

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

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

דרך אגב – אם מעולם לא שאלת את עצמך האם הקוד שלך נקי – כנראה שהוא לא…

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

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

אם זה לא מספיק משכנע, אז יש עוד כמה סיבות לכתיבת CLEAN CODE:

 

יש כמה עקרונות כלליים לכתיבת CLEAN CODE שנתמקד עליהן מיד, ומתוכם נובעים הרבה עקרונות ספציפיים שנדון עליהם בהמשך:

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

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

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

לכל טכנולוגיה יש את החסרונות שלה – גם אם מדובר בשפה אותה אנו יודעים הכי טוב. למשל: Linq-to-Sql הופכת להיות קשה לכתיבה ולקריאה בשאילתות מורכבות עם outer joins על אף שמדובר לכאורה על C# ויש לי העדפה לכתיבה בשפה הזו.

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

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

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

מאותן הסיבות כדאי להמנע מ CSS Inline style בתוך הHTML.

הדוגמאות האלה לא כל כך נוראיות כמו הפעמים שבהם אתה מוצא בתוך C# משתנה מסוג String שמכיל SQL או JavaScript….

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

   1: private void CreateScript()

   2: {

   3:     string script = @"<script type=""text/javascript""> 

   4:     var _name = ""kuku""; 

   5:         </script>";

   6: }

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

בדרך הנקיה היינו מרוויחים כמה דברים:

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

עיקרון בסיסי נוסף בCLEAN CODE הוא הימנעות מ'רעשים'. רעשי רקע הם כל דבר שמפריע לריכוז ולמיקוד של קריאת הקוד.

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

Terse – תמציתי.

Expressive – מבטא באופן מדוייק וברור את הצורך.

Do one thing – עושה רק דבר אחד (ועושה אותו טוב).

כאשר אנו קוראים קוד, המוח שלנו הוא הקומפיילר.

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

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

דרך נוספת להימנע מרעשי רקע היא על ידי אי-חזרה על קוד. או באנגלית: DRY – Don't repeat yourself.

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

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

עיקרון אחרון לחלק זה הוא עיקרון התיעוד העצמי (Self-documenting code).

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

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

 

שיום (Nameing)

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

נסתכל לדוגמא בקטע הקוד הבא:

   1: private decimal DirtyCode()

   2: {

   3:     List<decimal> p = new List<decimal>() { 5.50m, 10.48m, 131.21m };

   4:     decimal t = 0;

   5:     foreach (var i in p)

   6:     {

   7:         t += i;

   8:     }

   9:     return t;

  10: }

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

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

באופן נקי יותר היינו כותבים את הקוד הזה כך:

   1: private decimal CleanCode()

   2: {

   3:     List<decimal> prices = new List<decimal>() { 5.50m, 10.48m, 131.21m };

   4:     decimal total = 0;

   5:     foreach (var price in prices)

   6:     {

   7:      total += price;

   8:     }

   9:     return total;

  10: }

כך יותר טוב!

 

שמות קלאסים

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

בין השמות שמומלץ להימנע מהם ניתן למצוא את:

 

שמות צריכים להיות מדוייקים וספציפים כגון:

נוכל לסכם כמה כללים בבחירת שמות לקלאסים:

 

שמות מתודות

גם בשמות של מתודות נרצה להמנע משמות כגון:

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

באופן כללי נעדיף שמות של מתודות בסגנון כזה:

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

עוד כמה טיפים לשיום נכון של מתודות:

 

שמות משתנים

משתנים בוליאנים

משתנים בוליאנים צריכים להישמע כאילו הם שואלים שאלה שהתשובה עליה היא true/false.

ולכן לא נרצה להשתמש בשמות כגון:

ובמקומם נעדיף:

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

if (loggedIn)

מאשר

if (login)

סימטריות

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

Clean

Dirty

on/off

on/disable

fast/slow

quick/slow

lock/unlock

lock/open

 

תנאים

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

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

תנאי צריך להיות כתוב באופן המזכיר שפה מדוברת.

שורה כזו:

if (loggedIn==true)

מובנת פחות משורה כזו:

if (loggedIn)

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

ניקח דוגמא אחרת:

Dirty

   1: bool goingToLunch;

   2: if (cashInWallet > 6.00)

   3:     goingToLunch = true;

   4: else

   5: goingToLunch = false;

ונשווה לקוד הזה:

Clean

   1: bool goingToLunch = cashInWallet > 6;

לקוד הנקי יש כמה יתרונות:

 

חשוב באופן חיובי

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

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

עוד אפשר להקל על קריאת קוד של תנאים על ידי שימוש ב Imidiate If:

במקום לכתוב כך:

   1: int salary;

   2: if (isSpeaker)

   3:     salary = 1000;

   4: else

   5:     salary = 500;

נכתוב כך:

   1: int salary = isSpeaker ? 1000 : 500;

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

 

שימוש בEnum

שפות שהן Strongly Type כמו c# מאפשרות לבדוק משתנים באופן מדוייק – מבחינת הסוג אך לא מבחינת התוכן.

השימוש בEnum יכול לחסוך לנו כמה בעיות.

דוגמא:

Dirty

   1: if (employeeType == "manager")

על ידי שימוש בEnum נרוויח כמה יתרונות:

– אין שגיאות כתיב.

– שימוש בהשלמה אוטומטית.

– תיעוד עצמי – הקוד מסביר את עצמו.

– קל לחיפוש.

 

"מספרי קסם"

מספרי קסם הם מספרים שהמשמעות שלה ידועה לכותב הקוד ברגע כתיבת הקוד. וזהו…

Dirty

   1: if(age > 21)

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

Clean

   1: const int legalDrinkingAge = 21;

   2: if(age > legalDrinkingAge)

דוגמא נוספת:

Dirty

   1: if(status == 2)

CleanEnum

   1: if(status == Status.Active)

על ידי שימוש בConst או ב Enum מטרת התנאי ברורה ואין "מספרי קסם".

 

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

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

מה דעתכם על התנאי הבא:

Dirty

   1: if (Car.Year > 1980 && (Car.Make == "Ford" || Car.Make == "Chevrolet") && Car.Odometer < 10000 && (Car.Vin.StartsWith("V2") || Car.Vin.StartsWith("IA3")))

קיימות מספר דרכים לניהול תנאים ארוכים ונציג שתיים:

1. Intermediate variables

2. Encapsulate via function

 

Intermediate variables

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

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

Clean

   1: bool requestedCar = Car.Year > 1980 && (Car.Make == "Ford" || Car.Make == "Chevrolet") && Car.Odometer < 10000 &&

   2:         (Car.Vin.StartsWith("V2") || Car.Vin.StartsWith("IA3"));

   3: if (requestedCar)

שאל את עצמך – על איזה שאלה התנאים האלה מנסים לענות.

 

Encapsulate via function

דרך חזקה יותר לטיפול בתנאים ארוכים היא לכתוב מתודה המכילה את התנאים ולקרוא לה בif.

Dirty

   1: //Check for valid file extensions. Confirm admin or active

   2:  if ((fileExtension == "mp4" || fileExtension == "mpg" || fileExtension == "avi") && (isAdmin || isActiveFile))

כפי שנראה בהמשך, אחד העקרונות של CLEAN CODE, הוא להעדיף קוד המבטא את המטרה באופן מדוייק, על פני כתיבת הערות.

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

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

Clean

   1: if (ValidFileRequest(fileExtension, isActiveFile, isAdmin))

   2:        

   3: private bool ValidFileRequest(string fileExtension, bool isActiveFile, bool isAdmin)

   4: {

   5:     return (fileExtension == "mp4" || fileExtension == "mpg" || fileExtension == "avi") && (isAdmin || isActiveFile);

   6: }

נוסיף שיפור קל נוסף למתודה:

   1: private bool ValidFileRequest(string fileExtension, bool isActiveFile, bool isAdmin)

   2: {

   3:     var validFileExtensions = new List<string>() { "mp4", "mpg", "avi" };

   4:     bool validFileType = validFileExtensions.Contains(fileExtension);

   5:     bool userIsAllowedToViewFile = isActiveFile || isAdmin;

   6:  

   7:     return validFileType && userIsAllowedToViewFile;

   8: }

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

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

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

דוגמא:

Dirty

   1: private void LoginUser(User user)

   2: {

   3:     switch (user.Status)

   4:     {

   5:         case Status.Active:

   6:             //logic…

   7:             break;

   8:         case Status.InActive:

   9:             //logic…

  10:             break;

  11:         case Status.Locked:

  12:             //logic…

  13:             break;

  14:     }

  15: }

אם נשתמש בכמה עקרונות של Object Oriented נוכל למשל לכתוב קוד כזה:

Clean

הקריאה למתודה תיראה כך:

   1: private void LoginUser(User user)

   2: {

   3:     user.Login();

   4: }

ונממש קלאס אבסטרקטי עם ירושה באופן הבא:

   1: abstract class User

   2: {

   3:     public String FirstName;

   4:     public string LastName;

   5:     public Status Status;

   6:     public abstract void Login();

   7: }

   8:  

   9: class ActiveUser : User

  10: {

  11:     public override void Login()

  12:     {

  13:         //do

  14:     }

  15: }

  16:  

  17: class InactiveUser : User

  18: {

  19:     public override void Login()

  20:     {

  21:         //do

  22:     }

  23: }

  24:  

  25:  

  26: class LockedUser : User

  27: {

  28:     public override void Login()

  29:     {

  30:         //do

  31:     }

  32: }

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

 

הבע כפי יכולתך

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

דוגמא:

Dirty

   1: list<User> matchingUsers = new list<User>();

   2: foreach (var user in users)

   3: {

   4:     if (user.Status == Status.Active)

   5:     {

   6:         matchingUsers.Add(user);

   7:     }

   8: }

בשפות מסויימות הקוד הזה יהיה בסדר גמור, אבל מכיוון שב C# אפשר לכתוב ב linq נעדיף לכתוב כך:

Clean

   1: users.Where(user.Status == Status.Active);

פחות שורות, יותר ברור.

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

 

לפעמים הקוד אינו התשובה

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

דוגמא:

Dirty

   1: if (age < 20)

   2: {

   3:    return 100;

   4: }

   5: if (age < 30)

   6: {

   7:    return 120;

   8: }

   9: if (age < 40)

  10: {

  11:    return 130;

  12: }

  13: if (age < 50)

  14: {

  15:    return 160;

  16: }

Clean

InsuranceRatetable

Rate

maximumAge

Insurance RateId

100

20

1

120

30

2

130

40

3

160

50

4

ושליפת הנתונים:

   1: return Repository.GetInsuranceRate(age);

בדרך הזו הרווחנו כמה דברים:

· כאשר יהיו שינויים בנתונים, לא נצטרך לעדכן את הקוד אלא רק להוסיף שורה בDatabase.

· לא נכתוב נתונים משתנים בתוך הקוד.

· נכתוב פחות שורות קוד.

 

פונקציות

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

הסיבות ליצירת פונקציות יכולות להיות מגוונות. למשל:

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

חפש אחר תבניות בקוד שלך שחוזרות על עצמן. לפעמים רואים תבניות בעין בלי להיכנס לקריאת הקוד. דרך חביבה לזכירה היא בעזרת הקיצור: Don't Repeat Yourself.

2. אחד מעקרונות כתיבת Object Oriented (כחלק מSOLID) היא Single Responsebily Principle. כל פונקציה צריכה לעשות רק דבר אחד, ולעשות אותו טוב. אם רואים שפונקציה עושה שני דברים נפרדים, כדאי לדאוג לכך שיהיו שתי פונקציות.

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

4. הזחות (INDENTATION) – כאשר יש יותר מדי הזחות

                       (שורות שמתחילות ימינה יותר מאשר השורות שמעליהן),

                                                                                                סימן שהקוד מסובך

                                                                                                                 מדי ואפשר להוציא כמה קטעים

                                                                                                                                                        לפונקציות.

למצב של הזחה מופרזת קוראים לפעמים: Arrow Code, בגלל שהקוד מתחיל להיראות כמו חץ. ככל שהלוגיקה גדלה, רואים יותר 'חץ' וזה איתות לכך שהקוד מסובך מדי ושיש יותר מדי נתיבים שבהם הקוד יכול להתקדם. כאשר יש הרבה נתיבי התקדמות קשה להחזיק בראש את כל האפשרויות בבת אחת. מחקרים הוכיחו שיעילות הקוד יורדת לאחר שלושה תנאים מקוננים (nested if).

יש שלושה דרכים להימנע מהזחה מופרזת:

א. קריאה למתודות.

ב. 'לחזור' מוקדם – Return Early.

ג. 'ליפול' מהר – FailFast.

 

א. Extract Method – כאשר יש הרבה הזחות מומלץ להתחיל מהנקודה הכי פנימית ולהוציא אותה למתודה.

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

 

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

ניקח לדוגמא את הקוד הבא:

   1: private bool DirtyReturnEarly(string userName)

   2:  {

   3:      bool isValid = false;

   4:  

   5:      const int minUsernameLength = 6;

   6:      if (userName.Length >= minUsernameLength)

   7:      {

   8:          const int maxUsernameLength = 25;

   9:          if (userName.Length <= maxUsernameLength)

  10:          {

  11:              bool isAlphaNumeric = userName.All(Char.IsLetterOrDigit);

  12:              if (isAlphaNumeric)

  13:              {

  14:                  if (!ContainsCurseWords(userName))

  15:                  {

  16:                      isValid = IsUniqueUserName(userName);

  17:                  }

  18:              }

  19:          }

  20:      }

  21:      return isValid;

  22:  }

ניתן לראות בבירור את צורת ה'חץ'.

עתה ניראה קטע קוד שעושה את אותו הדבר, אך דואג לחזור מוקדם:

   1: private bool CleanReturnEarly(string userName)

   2: {

   3:     const int minUsernameLength = 6;

   4:     if (userName.Length >= minUsernameLength) return false;

   5:     

   6:     const int maxUsernameLength = 25;

   7:     if (userName.Length <= maxUsernameLength) return false;

   8:     

   9:     bool isAlphaNumeric = userName.All(Char.IsLetterOrDigit);

  10:     if (isAlphaNumeric) return false;

  11:     

  12:     if (ContainsCurseWords(userName)) return false;

  13:     

  14:     return IsUniqueUserName(userName);

  15: }

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

ונסכם את הנושא בציטוט:

"Use a return when if it enhances readability… In certain routines, once you know the answer… most returning immediately means that you have to write more code."

(Steve MClean Codeonnell, "Code Complete")

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

חבל להתקדם עוד ועוד בקוד כאשר יש סבירות שבשלב כלשהו נקבל Exception. אם הגיעה שגיאה – אפשר להיפרד מהנקודה שבה היינו.

ניראה דוגמא פשוטה:

   1: private void FailFastDirty(string userName, string password)

   2: {

   3:   if (!string.IsNullOrWhiteSpace(userName))

   4:   {

   5:       if (!string.IsNullOrWhiteSpace(password))

   6:       {

   7:           //register user here.

   8:       }

   9:       else

  10:       {

  11:           throw new ArgumentException("Password is required.");

  12:       }

  13:   }

  14:   else

  15:   {

  16:       throw new ArgumentException("Username is required.");

  17:   }

  18: }

אפשרות טובה ונקייה יותר ניראית כך:

   1: private void FailFastClean(string userName, string password)

   2: {

   3:     if (string.IsNullOrWhiteSpace(userName))

   4:         throw new ArgumentException("Username is required.");

   5:     if (string.IsNullOrWhiteSpace(password))

   6:         throw new ArgumentException("Password is required.");

   7:  

   8:     //register user here.

   9: }

דוגמא נוספת:

   1: private void FailFast (User user)

   2: {

   3:   switch (user.Status)

   4:   {

   5:       case Status.Active:

   6:           //logic for active users

   7:           break;

   8:       case Status.Inactive:

   9:           //logic for inactive users

  10:           break;

  11:       case Status.Locked:

  12:           //logic for locked users

  13:           break;

  14:       default:

  15:           throw new ApplicationException("Unknown user status: " + user.Status);

  16:   }

  17: }

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

 

משתנים

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

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

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

הנה דוגמא (שלילית) שממחישה את הצורך:

   1: private void MayflyParameters()

   2: {

   3:     bool a = false;

   4:     int b = 0;

   5:     string c = string.Empty;

   6:     bool d = true;

   7:  

   8:     //code

   9:     //.....

  10:     //.....

  11:     //.....

  12:     //.....

  13:  

  14:  

  15:     a = SomethingIsTrue();

  16:     if (a)

  17:     {

  18:         if (c.Length > b)

  19:         {

  20:             //code

  21:             //.....

  22:             //.....

  23:             //.....

  24:             //.....

  25:             d = c.Substring(0, 3) == b.ToString();

  26:         }

  27:     }

  28: }

 

פרמטרים

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

 

דגל שחור

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

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

 

פונקציה ארוכה מדי

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

– רווחים והערות – מתכנתים נוהגים להפריד ברווחים קטעי קוד שעושים דברים שונים.

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

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

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

 

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

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

The maximum length… is inversely proportional to the complexity and indentation level of that function. So, if you have a conceptually simple function that is just one long (but simple) case statementit's okay to have a longer function… If you have a complex function… adhere to the limits all the more closely.

(Linux style guide)

 

Exceptions – חריגים

באופן כללי אפשר לחלק את הexceptions לשלושה סוגים:

1. Unrecoverable – יש חריגים שבהם יש בעיה עקרונית ואי אפשר להמשיך הלאה. אלו החריגים הכי שכיחים וכאשר הם מגיעים אנו מעוניינים שהקוד יפול. למשל: Null reference, File not found, Access denied.

2. Recoverable – יש חריגים המעידים על בעיה, אך יתכן והבעיה נובעת מנתון שיכול להשתנות ולכן אולי אפשר לנסות שוב (בתנאי שמודעים לכך שמנסים שוב ויודעים מתי להפסיק את הנסיונות). במצבים כאלו ננסה למשל: Retry connection, Try different file, Wait and try again.

3. Ignorable – יש חריגים שאפשר להתעלם מהם ולהמשיך כרגיל בלי שהמשתמש ירגיש (ולרשום ללוג).

 

כמה כללים לטיפול בחריגים:

 

קלאסים

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

מתי ליצור קלאס?

 

דוגמא:

אם אנו רואים חתימה של מתודה שנראית כך:

   1: private void SaveUser(string firstName, string lastName, string eyeColor, string email, string phone)

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

   1: private void SaveUser(User user)

בדרך הזו אנו מרוויחים כמה דברים:

לכידות גבוהה

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

כמה יתרונות לקלאס בעל לכידות גבוהה:

 

דוגמא:

קלאס בעל לכידות נמוכה:

Vehicle

– Edit vehicle options

– Update pricing

– Schedule maintenance

– Send maintenance reminder

– Select financing

– Calculate monthly payment

 

כמה קלאסים בעלי לכידות גבוהה:

Vehicle

– Edit vehicle options

– Update pricing

VehicleMaintenance

– Schedule maintenance

– Send maintenance reminder

VehicleFinance

– Select financing

– Calculate monthly payment

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

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

קלאס בעל לכידות נמוכה יאופיין בדרך כלל בשמות כלליים כגון WebsiteBO, Utility, Common, MyFunctions, Manager. אלו קלאסים שנוטים לגדול הרבה ולמי שקורא את הקוד אין מושג מה הקלאסים האלה אמורים לעשות. ממילא ניתן להבין שאם מקפידים על שמות מדוייקים, משיגים קלאסים קטנים יותר ובעלי לכידות גבוהה יותר.

שימו לב!!

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

סימנים לכך שהקלאס קטן מדי:

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

כמה כללים נוספים בבניית קלאסים (לא תמיד קל ליישם באופן מוחלק בגלל הנסיבות המשתנות אבל כדאי להשתדל):

 

הערות בקוד

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

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

 

הערות מיותרות

ניתן לראות לפעמים הערות שהן ממש מיותרות כגון:

int i = 1; //Set i = 1

 

//if is swipe

if(isSwipe)

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

 

הערות הסבר

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

דוגמא:

//Assuer terminal is active

if(terminal.Status == 2)

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

בהיר יותר היה לכתוב כך (ללא הערה):

if(terminal.Status==Status.Active)

בדוגמא המתוקנת הקוד מדבר בעד עצמו ומבהיר בצורה מפורשת מה התנאי בודק.

לא רק Enum יכול להחליף מספרי קסם אלא גם קבועים (Constants) יכולים להיות רעיון מוצלח.

 

התנצלויות

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

משהו כעין זה:

//Sorry, this crashes a lot so I'm just swallowing the exception.

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

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

 

אזהרות

דוגמא:

//Do Not change this Value!!!

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

 

קוד 'זומבי'

'זומבים' הם לכאורה מתים. אבל רק לכאורה…

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

איך מגיעים למצב כזה?

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

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

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

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

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

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

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

כמה שאלות שכדאי לשאול את עצמנו לפני שנכניס קטע קוד להערה:

 

הערות כותרת

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

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

 

הערות יצירתיות

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

משהו כזה:

//****************************

//* Class Name: MyService.cs

//*

//****************************

כמובן שזה מיותר למדי. כל המידע שבהערות הוא מידע הידוע ממילא.

 

הערות מנהליות

יש הערות המכילות מידע על הבאג שבגללו התווספה שורה מסויימת (כולל מספר הבאג ומה היתה הבעיה).

גם הערות כאלה מפריעות לקריאה השוטפת והמקום הנכון לזה הוא ב Source Control.

 

האם אין הערות נחוצות?

מתי בכל זאת נכתוב הערות?

 

עקרונות כלליים

לסיכום נעבור על כמה עקרונות כלליים בCLEAN CODE.

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

 

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

הוסף תגובה " class="ir icon-in">linkedin twitter email