CLEAN CODE – כתיבת קוד שגם בני אדם יכולים לקרוא
תכנות הוא האמנות של להגיד לאדם אחר מה הוא רוצה שהמחשב יעשה
(דונאלד קנות’)
מתכנת ממוצע מבלה הרבה יותר זמן בקריאת קוד מאשר בכתיבתו. נוטים לשכוח שהקוד שאנחנו כותבים יקרא בעתיד לכל הפחות 10 פעמים. לפעמים מתכנת קורא קוד שהוא עצמו כתב, ולפעמים הוא קורא קוד שאחרים כתבו. אבל כל מתכנת יודע כמה זה מתסכל לקרוא קוד שבו הכוונה לא מספיק ברורה או שהדרך להשגת המטרה לא ברורה.
אי בהירות של הקוד מסתירה ברוב הפעמים גם טעויות של תכנון הקוד ופוטנציאל גדול לבאגים עתידיים.
מתוך הבנת הנקודות האלה (ועוד כמה) נולדה מתודולוגיה בעולם הפיתוח שנקראת CLEAN CODE.
האיש המזוהה ביותר עם CLEAN CODE הוא רוברט מרטין המכונה ‘הדוד בוב’ בספרו בעל השם המפתיע CLEAN CODE.
ספר זה מצטרף לעוד ספרים המדברים על איכות הקוד ועל איכות המתכנת. שני ספרים טובים ומפורסמים שאני מכיר הם: The Pragmatic Programmer ו Code Complete 2.
הרעיון הכללי של CLEAN CODE הוא שקוד צריך להיות כזה שיכול להיקרא על ידי קולגות. קוד כזה הוא קל להבנה ולתחזוקה. או בלשונו של מרטין פוולר: “כל שוטה יכול לכתוב קוד שהמחשב יבין. מתכנת טוב כותב קוד שבני אדם יכולים להבין”.
למעשה, הרבה פעמים אנחנו חושבים שהעבודה מוכנה כאשר התוכנה עושה את מה שמצופה ממנה, אך אנו נוטים לשכוח שבעתיד מישהו יצטרך לתחזק את הקוד הזה, לתקן אותו או להוסיף עליו.
ולכן כאשר מקפידים על CLEAN CODE עושים את החיים קלים עבורנו ועבור מתכנתים אחרים מכיוון שלכתוב קוד זה די קל, אך לקרוא קוד זה קשה.
דרך אגב – אם מעולם לא שאלת את עצמך האם הקוד שלך נקי – כנראה שהוא לא…
מכיוון גם מתכנתים וגם סופרים הם אנשים המתפרנסים מכתיבת טקסט יצירתי באופן מקצועי. הרי שכתיבת קוד מוקפד, מקבילה לכתיבת ספרות איכותית.
מתוך הקבלה לסופרים שכותבים ספר המכיל פרקים ובו כותרות ופיסקאות, מתכנתים כותבים NAMESPACE המכילים קלאסים ובו מתודות. שמות של קלאסים, מתודות ומשתנים בעלי משמעות מובנת, חשובים כמו כותרות ושמות של פרקים בספר. מתודות באורך המתאים חשובות כמו פיסקאות קריאות שאינן ארוכות ומייגעות מדי.
אם זה לא מספיק משכנע, אז יש עוד כמה סיבות לכתיבת CLEAN CODE:
- להצדיק דברים שכתבת בעבר – לא אחת אנחנו נדרשים להסביר מדוע כתבנו קטע קוד מסויים, ומה התכוונו בקטע קוד אחר. בהקפדה על CLEAN CODE קטנים הסיכויים שנמצא את עצמנו מגמגמים בהסברים.
- עצלנות – בקטע הטוב… או כמו שאומרים בצבא – קשה בקוד קל בדיבוג.
- אין די זמן על מנת להיות רשלן. אם הקוד כתוב בצורה לא מוקפדת, יתבזבז בעתיד הרבה זמן על נסיון להבינו.
אמנם מנהלים תמיד מעדיפים מהירות על פני איכות, ומתכנתים מעדיפים איכות על פני מהירות – אבל אם מתרגלים לכתיבה נקיה ומוקפדת, צוברים מהירות, והכתיבה הנקיה מחייבת דיוק, ובכך יוצרת איכות.
- אינך רוצה להיות מזוהה עם קוד קשה לקריאה. אף אחד לא רוצה שיאמרו עליו משפטים כגון: “אוי לא… זה קוד של [YourName]”….
- CLEAN CODE הוא תשתית ויסוד למתודולוגיות תכנות אחרות כגון: SOLID, TDD, Refactoring, Automated Testing, Design Patterns.
יש כמה עקרונות כלליים לכתיבת CLEAN CODE שנתמקד עליהן מיד, ומתוכם נובעים הרבה עקרונות ספציפיים שנדון עליהם בהמשך:
העיקרון הראשון הוא בחירת הכלי הנכון לעבודה.
מתכנתים נוטים הרבה פעמים להעדיף את הכלים הכי משוכללים והכי חדשניים ככלי העבודה שלהם. לבחור את התוספים הכי חדשים ואת הטכנולוגיות הכי מתקדמות. זה אכן מושך למדי… אבל לא תמיד זה הדבר הנכון לעשות.
הכלים שאנחנו עובדים איתם צריכים להיבחר על פי מידת התאמתם למשימה שלנו ואין צורך להיות משועובדים לטכנולוגיה.
לכל טכנולוגיה יש את החסרונות שלה – גם אם מדובר בשפה אותה אנו יודעים הכי טוב. למשל: Linq-to-Sql הופכת להיות קשה לכתיבה ולקריאה בשאילתות מורכבות עם outer joins על אף שמדובר לכאורה על C# ויש לי העדפה לכתיבה בשפה הזו.
דבר נוסף שצריך לשים אליו לב בזמן בחירת כלי העבודה שלנו, הוא מה הייעוד של הכלי שלנו. לכל כלי יש שימוש מתאים ולא כדאי לערבב בין הכלים.
לא נדיר למצוא קטע JavaScript בתוך HTML או להיפך, ניתן למצוא HTML גם בתוך SQL במטרה לחולל דף בצורה דינמית.
יש אולי כמה יתרונות בשיטות האלה, אבל הרבה יותר חסרונות. קטע קוד שמתחבא בתוך הDataBase הוא קוד שקשה למחזר אותו ולדבג אותו.
מאותן הסיבות כדאי להמנע מ CSS Inline style בתוך הHTML.
הדוגמאות האלה לא כל כך נוראיות כמו הפעמים שבהם אתה מוצא בתוך C# משתנה מסוג String שמכיל SQL או JavaScript….
קטע הקוד הבא הוא דוגמא לבחירה לא נכונה של כלי:
- private void CreateScript()
- {
- string script = @”<script type=“text/javascript“>
- var _name = “kuku“;
- </script>”;
- }
הדרך הנקיה שבה היינו מממשים את הדוגמא הזו היא על ידי כתיבת הסקריפט בתוך קובץ JavaScript.
בדרך הנקיה היינו מרוויחים כמה דברים:
- הIDE שלנו מזהה שזה JavaScript ו’צובע’ את הטקסט בצבעים המקילים על הקריאה.
- קל יותר לכתוב מכיוון שיש intellisense.
- במקרה של JavaScript הקוד נשמר בCash ולא צריך לטעון אותו מחדש.
- יש בדיקת איות על שמות המשתנים והמתודות שלנו.
- כל נושא קיים באופן מופרד מהנושאים האחרים.
- אפשר למחזר את קטע הקוד בצורה נוחה ממקומות שונים בקוד.
- לא צריך להמיר את הString לJavaString.
מכל הסיבות האלה כדאי להימנע משימוש בשפה אחת על מנת לכתוב שפה אחרת דרך מחרוזות. וכמובן שבכל קובץ צריכה להיות רק שפה אחת (אם רואים שתי שפות באותו הקובץ, צריכה להידלק נורת אזהרה).
עיקרון בסיסי נוסף בCLEAN CODE הוא הימנעות מ’רעשים’. רעשי רקע הם כל דבר שמפריע לריכוז ולמיקוד של קריאת הקוד.
על מנת שלא יהיו יותר מדי הסחות דעת בעת קריאת הקוד, כדאי להקפיד על שלושה עקרונות המבוטאים בראשי התיבות TED :
- Terse – תמציתי.
- Expressive – מבטא באופן מדוייק וברור את הצורך.
- Do one thing – עושה רק דבר אחד (ועושה אותו טוב).
כאשר אנו קוראים קוד, המוח שלנו הוא הקומפיילר.
מחקרים הוכיחו שבזכרון לטווח קצר, המוח שלנו יכול להחזיק עד שבע פריטים בבת אחת (חוק השבע). חוק השבע מכתיב לנו גם איך לכתוב קוד. אנחנו צריכים לזכור שמי שיקרא את הקוד אחרינו,לא יוכל לזכור יותר מדי משתנים בבת אחת.
לדוגמא, מתודות המקבלות יותר מדי פרמטרים הן מתודות שקשות לקריאה ומכיוון שכך, קשה לדבג אותן.
דרך נוספת להימנע מרעשי רקע היא על ידי אי-חזרה על קוד. או באנגלית: DRY – Don’t repeat yourself.
אם מישהו מוצא את עצמו עושה ‘העתק-הדבק’, כנראה שיש לו בעיית ארכיטקטורה ותכנון. קוד כפול מצריך הכפלה של כל שינוי, דבר הגורר בעיות תחזוקה ומגדיל את כמות שורות הקוד.
אם הגדלנו את כמות השורות בקוד, הגדלנו את כמות הבאגים… ולכן אם רואים שורות כפולות – צריך לחפש תבנית שחוזרת על עצמה ולהיפטר מהחזרות על ידי יצירת מתודה, קלאס או כל דרך אחרת.
עיקרון אחרון לחלק זה הוא עיקרון התיעוד העצמי (Self-documenting code).
באופן כללי הבנת קוד שכתב מישהו אחר היא משימה לא כל כך פשוטה, ולכן הקוד צריך להיות כתוב באופן כזה שבו המטרה ברורה, הקוד קריא וקיימות שכבות של הפשטה על פי הצורך.
קוד נקי המתעד את עצמו חוסך כתיבת תיעוד ומסמכים חיצוניים (שכמעט אף אחד לא קורא…) ומאפשר הבנה מפורטת ומעמיקה על ידי הקוד עצמו.
שיום (Nameing)
אחד הדברים החשובים בכתיבת קוד קריא, הוא נתינת שמות משמעותיים למשתנים, למתודות, לקלאסים ולכל אלמנט בקוד. כפי שנראה מיד, יש שמות שכדאי להמנע מהם, יש שמות שמאותתים על כך שמשהו בקוד שלנו לא מתוכנן כראוי ויש שמות שהינם סתם חסרי משמעות.
נסתכל לדוגמא בקטע הקוד הבא:
- private decimal DirtyCode()
- {
- List<decimal> p = new List<decimal>() { 5.50m, 10.48m, 131.21m };
- decimal t = 0;
- foreach (var i in p)
- {
- t += i;
- }
- return t;
- }
מקריאת הפונקציה הזו לא ניתן להבין מה הקוד עושה. אם נדמה קריאת קוד לקריאת סיפור, הרי שזה כמו לקרוא סיפור על א’ שכעס על ג’ בגלל שד’ ללא הגיע לעזור לח’ (לא מהשב”כ…).
אמנם למחשב זה לא משנה והקוד הזה יתקמפל ויבצע את המשימה, אך מתכנתים אחרים שיקראו את הקוד (ואף לך) זה משנה מאוד.
באופן נקי יותר היינו כותבים את הקוד הזה כך:
- private decimal CleanCode()
- {
- List<decimal> prices = new List<decimal>() { 5.50m, 10.48m, 131.21m };
- decimal total = 0;
- foreach (var price in prices)
- {
- total += price;
- }
- return total;
- }
כך יותר טוב!
קלאסים
יש שמות שהם כל כך כלליים שמאוד קל להשתמש בהם בזמן הכתיבה, אך אחר כך לא יודעים לשייך אותם למקום הנכון ולהבין את המטרה המקורית שלהם.
בין השמות שמומלץ להימנע מהם ניתן למצוא את:
- WebsiteBO
- Utility
- Common
- MyFunction
- Manager / Processor
שמות צריכים להיות מדוייקים וספציפים כגון:
- User
- AClean Codeount
- QueryBuilder
- ProductRepository
נוכל לסכם כמה כללים בבחירת שמות לקלאסים:
- קלאס הוא שם עצם, דבר מסויים. שם של קלאס לא מבטא פעולה או מתאר משהו, אלא הוא השם של הדבר עצמו.
- שם צריך להיות ספציפי ולא כללי. ולכן, אם קשה למצוא שם ספציפי לקלאס, ככל הנראה הדבר נובע מכך שיש הרבה מתודות בתוך הקלאס הזה ואין שם אחד שמצליח לתאר את כולם. מצב כזה זהו איתו לכך שככל הנראה כדאי להפריד את הקלאס הזה לשני קלאסים נפרדים.
- Single Responsibility – קלאס צריך לעשות דבר אחד – והדבר הזה שהקלאס עושה, יבוא לידי ביטוי בשם שלו.
- הימנע מסופיות גנריות. Product זהו שם מצויין. אין צורך לקרוא לקלאס ProdunctManager. הסופית ‘Manager’ לא מוסיפה שום ערך לשם של הקלאס. נוכל להבין זאת אם נשאל את עצמנו מה תהיה הכוונה כאשר ניצור מופע חדש של הקלאס הזה. האם זהו מופע של Product או של ProductManager.
מתודות
גם בשמות של מתודות נרצה להמנע משמות כגון:
- Get
- Process
- Pending
- Start
שם של מתודה אמור לבטא את תוכן המתודה כך שכאשר נקרא את הקוד נוכל להבין מה המתודה עושה, גם בלי לקרוא אותה. מתודה בשם Get היא מבחינתנו נעלם שאין לנו מושג על מה שמתרחש בתוכו.
באופן כללי נעדיף שמות של מתודות בסגנון כזה:
- GetRegisteredUsers
- IsValidSubmission
- ImportDocument
- SendEmail
כאשר למתודה יש שם בעל משמעות מדוייקת ומובנת, מי שיקרא את הקוד, ידע על פי שם המתודה אם היא הדבר שאותו הוא מחפש או שעליו להמשיך הלאה.
עוד כמה טיפים לשיום נכון של מתודות:
- אם מקפידים על כך שכל מתודה תעשה אך ורק דבר אחד (Single Responsebily Principle), קל מאוד לתת שם למתודה. אם שם המתודה שלי מכילה מילים כגון And, If, Or וכו’, זה סימן מובהק לכך שצריך לפצל את המתודה לשתי מתודות נפרדות.
לדוגמא: מתודה שנקראת CheckPasswordAndRegisterUser ככל הנראה צריכה להתפצל לשתי מתודות.
- על בסיס אותו ההגיון צריך להיזהר מפעולות לוואי של מתודות. CheckPassword לא אמורה להעביר משתמש החוצה מהמערכת. ValidateProcess לא אמורה לעשות Save וכו’.
- אם מתקשים במציאת שם מתאים למתודה מומלץ להגיד את השם ואת הרעיונות בקול רם לחבר (ואם אי אפשר אז בכל זאת לפעמים לא מצליחים למצוא שם מתאים ואז הדרך הנכונה היא להגיד את השם בקול רם לחבר, או לעצמך.
- באופן כללי לא צריך לחשוש משמות ארוכים. אין בעיית אחסון ויש השלמה אוטומטית, כך שאין צורך להתקמצן על אותיות. לא צריך לקרוא למתודה RegUsr כאשר אפשר לקרוא לה RegisterUser.
שמות משתנים
משתנים בוליאנים
משתנים בוליאנים צריכים להישמע כאילו הם שואלים שאלה שהתשובה עליה היא true/false.
. ולכן לא נרצה להשתמש בשמות כגון:
- Open
- Start
- Status
- Login
ובמקומם נעדיף:
- isOpen
- done
- isActive
- loggedIn
למי שיקרא את הקוד תהיה יותר ברורה השורה
if (loggedIn)
מאשר
if (login)
סימטריות
לפעמים משתמשים בשני משתנים בוליאנים אשר מתארים מצבים הופכיים. צריך להקפיד על סימטריות בשמות המשתנים.
Clean | Dirty |
on/off | on/disable |
fast/slow | quick/slow |
lock/unlock | lock/open |
תנאים
תנאים הם צמתים בקוד שבהם ישנם כמה מסלולי התקדמות. ולכן זוהי נקודה מובהקת לכתיבת קוד קריא.
כאשר כותבים תנאי צריך להקפיד שהמטרה של התנאי תהיה ברורה – לפעמים אכן התנאי מציב שתי אפשרויות, אבל כלל לא ברור למה הן אלטרנטיבות אחת לשניה. או שלפעמים התנאי כל כך מורכב שהקורא הממוצע יתקשה להבין את כוונת המשורר.
תנאי צריך להיות כתוב באופן המזכיר שפה מדוברת.
if (loggedIn==true)
מובנת פחות משורה כזו:
if (loggedIn)
ההבדל בין שתי השורות האלה הוא לא רק בכך שהשורה התחתונה קרובה יותר לשפה מדוברת, אלא שהשורה העליונה מוסיפה ‘רעש’ – עוד נתונים שהקורא צריך לשים לב אליהם ושמסיחים את דעתו.
ניקח דוגמא אחרת:
Dirty
- bool goingToLunch;
- if (cashInWallet > 6.00)
- goingToLunch = true;
- else
- goingToLunch = false;
ונשווה לקוד הזה:
Clean
- bool goingToLunch = cashInWallet > 6;
לקוד הנקי יש כמה יתרונות:
- פחות שורות קוד (יותר שורות == יותר באגים).
- אין משתנים שצריכים להיות מוצהרים מראש.
- בקוד ה’מלוכלך’ יש משתנה שחוזר 3 פעמים – דבר המגדיל את הפוטנציאל לטעויות.
- הקוד הנקי דומה יותר לשפה מדוברת.
חשוב חיובי
באופן כללי ניתן לומר שיש שני סוגי תנאים. תנאי חיובי (If(loggedIn)) ותנאי שלילי (If(!loggedIn)). אמנם לא תמיד, אבל בדרך כלל תנאי שלילי נוצר בדיעבד תוך כדי טיפול בבאג. משהו לא עובד כמצופה ומתברר שהתנאי לא נכון, ואז כפיתרון פשוט הופכים את אופי התנאי.
במצב כזה אמנם הדברים מסתדרים, אבל הקוד פחות קריא. בשפת הדיבור שלנו אנחנו בדרך כלל מדברים בצורה חיובית (“אם יתאפשר – אגיע” ולא “אם לא יתאפשר – לא אגיע”).
עוד אפשר להקל על קריאת קוד של תנאים על ידי שימוש ב Imidiate If:
במקום לכתוב כך:
- int salary;
- if (isSpeaker)
- salary = 1000;
- else
- salary = 500;
נכתוב כך:
- int salary = isSpeaker ? 1000 : 500;
בכתיבה מקוצרת כזו נחסוך מהקורא את הצורך לזכור באיזה שלב הוא נמצא ומה הערך הנוכחי של כל משתנה. הוא צריך לזכור רק משתנה אחד שקיבל ערך באופן מיידי.
- כמו לכל דבר בחיים יש כמה חסרונות לשיטת כתיבה כזו, בעיקר בזמן דיבוג או בזמן שיש המון תנאים ביחד, ולכן צריך לשקול כל מקרה לגופו (ממש כמו כל שאר הכלים שבהם אנחנו בוחרים להשתמש).
שימוש בEnum
שפות שהן Strongly Type כמו c# מאפשרות לבדוק משתנים באופן מדוייק – מבחינת הסוג אך לא מבחינת התוכן.
השימוש בEnum יכול לחסוך לנו כמה בעיות.
דוגמא:
Dirty
- if (employeeType == “manager”)
Clean – Enum
- if (employeeType == EmployeeType.Manager)
על ידי שימוש בEnum נרוויח כמה יתרונות:
- אין שגיאות כתיב.
- שימוש בהשלמה אוטומטית.
- תיעוד עצמי – הקוד מסביר את עצמו.
- קל לחיפוש.
“מספרי קסם”
מספרי קסם הם מספרים שהמשמעות שלה ידועה לכותב הקוד ברגע כתיבת הקוד. וזהו…
Dirty
- if(age > 21)
למה התכוון המשורר במספר 21? הקורא צריך לשער ולנחש. הניחוש היה נמנע בקוד כזה:
Clean
- const int legalDrinkingAge = 21;
- if(age > legalDrinkingAge)
דוגמא נוספת:
Dirty
- if (status == 2)
Clean – Enum
- if (status == Status.Active)
על ידי שימוש בConst או ב Enum מטרת התנאי ברורה ואין “מספרי קסם”.
תנאים מסובכים
גם תנאים שהתחילו בקטן יכולים להסתעף, להסתבך ולגדול.
מה דעתכם על התנאי הבא:
Dirty
- if (Car.Year > 1980 &&
- (Car.Make == “Ford” || Car.Make == “Chevrolet”) &&
- Car.Odometer < 10000 &&
- (Car.Vin.StartsWith(“V2”) || Car.Vin.StartsWith(“IA3”)))
קיימות מספר דרכים לניהול תנאים ארוכים ונציג שתיים:
- Intermediate variables
- Encapsulate via function
Intermediate variables
הרעיון הוא להעביר את כל התנאי אל תוך משתנה בוליאני, ואז לשאול את שאלת התנאי על המשתנה הזה.
אם ננסה לשפר מעט את הדוגמא הקודמת, נעשה זאת כך:
Clean
- bool requestedCar = (Car.Year > 1980 &&
- (Car.Make == “Ford” || Car.Make == “Chevrolet”) &&
- Car.Odometer < 10000 &&
- (Car.Vin.StartsWith(“V2”) || Car.Vin.StartsWith(“IA3”)));
- if (requestedCar)
שאל את עצמך – על איזה שאלה התנאים האלה מנסים לענות.
Encapsulate via function
דרך חזקה יותר לטיפול בתנאים ארוכים היא לכתוב מתודה המכילה את התנאים ולקרוא לה בif.
במקום ככה:
Dirty
- //Check for valid file extensions. Confirm admin or active
- if ((fileExtension == “mp4” || fileExtension == “mpg” || fileExtension == “avi”) &&
- (isAdmin || isActiveFile))
(כפי שנראה בהמשך, אחד העקרונות של CLEAN CODE, הוא להעדיף קוד המבטא את המטרה באופן מדוייק, על פני כתיבת הערות.
בדוגמא הזו הצורך בכתיבת הערות קוד נבע מכך שהקוד לא הצליח לתעד את עצמו ולא היה ברור מספיק.)
ניקח את ההערה הזו עצמה ונהפוך אותה לשם של מתודה וכך נקבל קוד כזה:
Clean
- if (ValidFileRequest(fileExtension, isActiveFile, isAdmin))
- private bool ValidFileRequest(string fileExtension, bool isActiveFile, bool isAdmin)
- {
- return (fileExtension == “mp4” || fileExtension == “mpg” || fileExtension == “avi”)
- && (isAdmin || isActiveFile);
- }
נוסיף שיפור קל נוסף למתודה:
- private bool ValidFileRequest(string fileExtension, bool isActiveFile, bool isAdmin)
- {
- var validFileExtensions = new List<string>() { “mp4”, “mpg”, “avi” };
- bool validFileType = validFileExtensions.Contains(fileExtension);
- bool userIsAllowedToViewFile = isActiveFile || isAdmin;
- return validFileType && userIsAllowedToViewFile;
- }
השורה האחרונה במתודה שבה אנחנו מחזירים תוצאה, כתובה באופן שמבהיר מאוד מהי מטרת המתודה.
כאשר קוראים קוד כזה, אפשר לעבור מהר על הקוד ולהגיע בזריזות לנקודה הרצויה.
אמנם צריך לזכור שEnum אינו הפיתרון הבלעדי, והוא טוב למקרים קונקרטיים. במקרה שבו נצטרך לבחור בין כמה התנהגויות שונות (למשל כשנשתמש בSwitch), כדאי להשתמש בפולימורפיזם ולכמס את ההתנהגות הרצויה לנו בתוך האובייקט עצמו.
דוגמא:
Dirty
- private void LoginUser(User user)
- {
- switch (user.Status)
- {
- case Status.Active:
- //logic…
- break;
- case Status.InActive:
- //logic…
- break;
- case Status.Locked:
- //logic…
- break;
- }
- }
אם נשתמש בכמה עקרונות של Object Oriented נוכל למשל לכתוב קוד כזה:
Clean
הקריאה למתודה תיראה כך:
- private void LoginUser(User user)
- {
- user.Login();
- }
ונממש קלאס אבסטרקטי עם ירושה באופן הבא:
- abstract class User
- {
- public string FirstName;
- public string LastName;
- public Status Status;
- public int AcountBalance;
- public abstract void Login();
- }
- class ActiveUser : User
- {
- public override void Login()
- {
- //do
- }
- }
- class InactiveUser : User
- {
- public override void Login()
- {
- //do
- }
- }
- class LockedUser : User
- {
- public override void Login()
- {
- //do
- }
- }
בדרך הזו לא צריך להשתמש בSwitch לאורך הקוד, וכל קלאס יודע מהי הדרך שבה הוא מתמודד עם לוגין.
- בדרך כלל כשמשתמשים בגישה הזו יוצרים גם Factory כדי לקבל מופע של User רצוי.
הבע כפי יכולתך
כתיבה בסגנון מסויים יכול להחשב נקי ואלגנטי בשפה אחת, אך בשפה אחרת שבה יש כלים משוכללים יותר נשאף לכתוב נקי יותר.
דוגמא:
Dirty
- List<User> matchingUsers = new List<User>();
- foreach (var user in users)
- {
- if (user.AcountBalance < minimumAcountBalance && user.Status == Status.Active)
- {
- matchingUsers.Add(user);
- }
- }
בשפות מסויימות הקוד הזה יהיה בסדר גמור, אבל מכיוון שב C# אפשר לכתוב ב linq נעדיף לכתוב כך:
Clean
- users.Where(user => user.AcountBalance < minimumAcountBalance && user.Status == Status.Active);
פחות שורות, יותר ברור.
אם תנסו לקרוא את שתי הדוגמאות בקול רם. תוכלו לראות שהאופציה השניה מבטאת את הרעיון בצורה בהירה יותר.
לפעמים הקוד אינו התשובה
רצף ארוך של תנאים יכול להצביע על כך שהקוד בודק נתונים רבים ועל פי זה קובע את ההתנהגות. במקרים כאלו כדאי לשקול את שמירת הנתונים בטבלת Database ושליפת הנתון הרצוי.
דוגמא:
Dirty
- if (age > 20)
- return 100;
- if (age > 30)
- return 120;
- if (age > 40)
- return 130;
- if (age > 50)
- return 160;
Clean
InsuranceRatetable
Rate | maximumAge | Insurance RateId |
100 | 20 | 1 |
120 | 30 | 2 |
130 | 40 | 3 |
160 | 50 | 4 |
ושליפת הנתונים:
- return Repository.GetInsuranceRate(age);
בדרך הזו הרווחנו כמה דברים:
- כאשר יהיו שינויים בנתונים, לא נצטרך לעדכן את הקוד אלא רק להוסיף שורה בDatabase.
- לא נכתוב נתונים משתנים בתוך הקוד.
- נכתוב פחות שורות קוד.
פונקציות
מתי ליצור פונקציה? פונקציה דומה לפסקאות בספר. קל לקרוא פיסקה כאשר היא מתרכזת בנושא אחד ולא מתפזרת. וכן קשה לקרוא ספר ללא פיסקאות.
הסיבות ליצירת פונקציות יכולות להיות מגוונות. למשל:
- כפילויות – כפילויות הן מתכון לצרות. אם משנים משהו במקום אחד, צריך לתחזק הרבה מקומות, וזה פוטנציאל לבאגים.
חפש אחר תבניות בקוד שלך שחוזרות על עצמן. לפעמים רואים תבניות בעין בלי להיכנס לקריאת הקוד. דרך חביבה לזכירה היא בעזרת הקיצור: Don’t Repeat Yourself.
- אחד מעקרונות כתיבת Object Oriented (כחלק מSOLID) היא Single Responsebily Principle. כל פונקציה צריכה לעשות רק דבר אחד, ולעשות אותו טוב. אם רואים שפונקציה עושה שני דברים נפרדים, כדאי לדאוג לכך שיהיו שתי פונקציות.
- מטרה לא ברורה – בסבך הקוד קל לפספס את המטרה של כמה שורות באמצע, אך אם השורות האלה יושבות בפונקציה נפרדת (בעלת שם משמעותי כמובן), זה מבהיר את המטרה.
- הזחות (INDENTATION) – כאשר יש יותר מדי הזחות
(שורות שמתחילות ימינה יותר מאשר השורות שמעליהן),
סימן שהקוד מסובך
מדי ואפשר להוציא כמה קטעים
לפונקציות.
למצב של הזחה מופרזת קוראים לפעמים: Arrow Code, בגלל שהקוד מתחיל להיראות כמו חץ. ככל שהלוגיקה גדלה, רואים יותר ‘חץ’ וזה איתות לכך שהקוד מסובך מדי ושיש יותר מדי נתיבים שבהם הקוד יכול להתקדם. כאשר יש הרבה נתיבי התקדמות קשה להחזיק בראש את כל האפשרויות בבת אחת. מחקרים הוכיחו שיעילות הקוד יורדת לאחר שלושה תנאים מקוננים (nested if).
יש שלושה דרכים להימנע מהזחה מופרזת:
- קריאה למתודות.
- ‘לחזור’ מוקדם – Return Early.
- ‘ליפול’ מהר – FailFast.
- Extract Method – כאשר יש הרבה הזחות מומלץ להתחיל מהנקודה הכי פנימית ולהוציא אותה למתודה.
כפי שראינו, מתודות הן כמו הערות שוליים בספר. ולכן במהלך קריאת הקוד, אם זה יש צורך אפשר לגשת להערה / למתודה ולקרוא אותה.
- לחזור מוקדם – הרעיון הוא שאם אין לך עוד משהו לעשות בקוד – תחזור.
- חייבים לציין שיש גישות שחולקות על זה ומעדיפות שיהיה רק מופע אחד של RETURN בקוד.
ניקח לדוגמא את הקוד הבא:
- private bool DirtyReturnEarly(string userName)
- {
- bool isValid = false;
- const int minUsernameLength = 6;
- if (userName.Length >= minUsernameLength)
- {
- const int maxUsernameLength = 25;
- if (userName.Length >= maxUsernameLength)
- {
- bool isAlphaNumeric = userName.All(Char.IsLetterOrDigit);
- if (isAlphaNumeric)
- {
- if (!ContainsCurseWords(userName))
- {
- isValid = IsUniqueUserName(userName);
- }
- }
- }
- }
- return isValid;
- }
ניתן לראות בבירור את צורת ה’חץ’.
עתה ניראה קטע קוד שעושה את אותו הדבר, אך דואג לחזור מוקדם:
- private bool CleanReturnEarly(string userName)
- {
- const int minUsernameLength = 6;
- if (userName.Length >= minUsernameLength) return false;
- const int maxUsernameLength = 25;
- if (userName.Length <= maxUsernameLength) return false;
- bool isAlphaNumeric = userName.All(Char.IsLetterOrDigit);
- if (isAlphaNumeric) return false;
- if (ContainsCurseWords(userName)) return false;
- return IsUniqueUserName(userName);
- }
את הקוד השני ניתן לקרוא בקלות רבה יותר מכיוון שלא צריך להחזיק בראש את כל ההתפתחויות האפשריות בכל רגע נתון. ובסה”כ ניתן לומר שהדרך הזו משקפת אנו מקבלים החלטות בחיים האמיתיים – ברגע שאנחנו יודעים את התשובה, אנחנו מפסיקים את השאלות.
ונסכם את הנושא בציטוט:
“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. אם הגיעה שגיאה – אפשר להיפרד מהנקודה שבה היינו.
ניראה דוגמא פשוטה:
- private void FailFastDirty(string userName, string password)
- {
- if (!string.IsNullOrWhiteSpace(userName))
- {
- if (!string.IsNullOrWhiteSpace(password))
- {
- //register user here.
- }
- else
- {
- throw new ArgumentException(“Password is required.”);
- }
- }
- else
- {
- throw new ArgumentException(“Username is required.”);
- }
- }
אפשרות טובה ונקייה יותר ניראית כך:
- private void FailFastClean(string userName, string password)
- {
- if (string.IsNullOrWhiteSpace(userName))
- throw new ArgumentException(“Username is required.”);
- if (string.IsNullOrWhiteSpace(password))
- throw new ArgumentException(“Password is required.”);
- //register user here.
- }
דוגמא נוספת:
- private void FailFast (User user)
- {
- switch (user.Status)
- {
- case Status.Active:
- //logic for active users
- break;
- case Status.Inactive:
- //logic for inactive users
- break;
- case Status.Locked:
- //logic for locked users
- break;
- default:
- throw new ApplicationException(“Unknown user status: “ + user.Status);
- }
- }
אם לא היה default בקוד היה קשה לגלות למה לא מצליחים להכנס. לכן כל switch צריך אפשרות ליפול הכוללת כמה שיותר פרטים רלוונטים.
משתנים
הרבה פעמים אנו חושבים שכדאי לאסוף את כל המשתנים בתחילת המתודה. זה נראה מסודר ויפה. אך החיסרון של השיטה הזו היא שהקורא צריך לזכור את כל המשתנים לאורך כל המתודה. כתיבה כזו סותרת את עיקרון השבע (המוח יכול לזכור רק 7 דברים בבת אחת), ומייצרת ‘רעש’ המפריע לקריאת הקוד.
למשתנים לוקאלים צריך להיות אורך חיים של פרפרים… צריך להגדיר אותם רק ברגע שהם נצרכים.
ודרך אגב – כאשר יש פונקציות שעושות רק דבר אחד, ממילא יהיו משתנים שרלוונטים רק למשימה הזו.
הנה דוגמא (שלילית) שממחישה את הצורך:
- private void MayflyParameters()
- {
- bool a = false;
- int b = 0;
- string c = string.Empty;
- bool d = true;
- //code
- //…..
- //…..
- //…..
- //…..
- a = SomethingIsTrue();
- if (a)
- {
- if (c.Length > b)
- {
- //code
- //…..
- //…..
- //…..
- //…..
- d = c.Substring(0, 3) == b.ToString();
- }
- }
- }
פרמטרים
השאיפה צריכה להיות לכך שמתודה מקבלת בין אפס לשני פרמטרים. כך הקוד מובן יותר וטסטבילי (מלשון 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 statement… it’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 לשלושה סוגים:
- Unrecoverable – יש חריגים שבהם יש בעיה עקרונית ואי אפשר להמשיך הלאה. אלו החריגים הכי שכיחים וכאשר הם מגיעים אנו מעוניינים שהקוד יפול. למשל: Null reference, File not found, AClean Codeess denied.
- Recoverable – יש חריגים המעידים על בעיה, אך יתכן והבעיה נובעת מנתון שיכול להשתנות ולכן אולי אפשר לנסות שוב (בתנאי שמודעים לכך שמנסים שוב ויודעים מתי להפסיק את הנסיונות). במצבים כאלו ננסה למשל: Retry connection, Try different file, Wait and try again.
- Ignorable – יש חריגים שאפשר להתעלם מהם ולהמשיך כרגיל בלי שהמשתמש ירגיש (ולרשום ללוג).
כמה כללים לטיפול בחריגים:
- בדרך כלל הדרך הנכונה להתמודד עם שגיאות היא להפיל את התוכנית.
- אפשר ‘לבלוע’ חריג רק אם מבינים את המשמעות של החריג ויודעים מה לעשות עם הבעיה שהחריג חשף, אבל אין שום הגיון בעטיפת קטע קוד בtry-catch רק בשביל שהתוכנית לא תיפול. אם לא יודעים איך להתמודד עם החריג, אל ‘תבלע’ אותו.
- תן לשגיאה לבעבע כלפי מעלה
- אם אתה לא יודע איך להתמודד עם השגיאה אזי יש בעיה אמיתית בתוכנה שלך ועדיף ליפול מאשר לחולל בעיות נוספות הנובעות ממשגיאה שנבלעה ולא טופלה.
- כל try/catch צריך לעמוד בפני עצמו כדי של-catch תהיה משמעות. אם יש הרבה קוד בתוך הtry אז כדאי לשקול להוציא לפונקציה, בשביל נוחות הקריאה.
קלאסים
לקלאסים יש תפקידים רבים בקוד שלנו, אך אם מדברים על קוד קריא, הרי שקלאסים הם כמו כותרות בספר. כאשר קורא מחפש משהו בספר, הוא מסתכל על הכותרות ובסריקה מהירה הוא מוצא את מבוקשו.
מתי ליצור קלאס?
- קונספט חדש – כאשר אנו מתעסקים עם נושא שעוד לא קיבל מימוש בקוד, ניצור קלאס.
- לכידות נמוכה בקלאס שלנו היא סימן לכך שכנראה צריכים להיות שני קלאסים.
- אם חלק מהקלאס שלנו יכול להיות בשימוש בכמה מקומות בקוד שלנו, כדאי לקחת את החלק הזה ולהפוך אותו לקלאס בפני עצמו.
- הפחתת סיבוכיות בקריאה – כאשר קוראים קוד ומגיעים לקלאס חדש, הקורא יכול לסמוך על זה שאיפשהו שם מאחור, יש קוד שעושה את מה שצריך והוא לא צריך לקרוא את כל הקוד.
- כאשר צריך להעביר פרמטרים רבים למתודה, יתכן ואפשר לאגד אותם לקלאס, ולהעביר למתודה מופע של הקלאס. בדרך הזו הפרמטרים ברורים מכיוון שהם מאוחדים על ידי זהות אחת.
דוגמא:
אם אנו רואים חתימה של מתודה שנראית כך:
- private void SaveUser(string firstName, string lastName, string eyeColor, string email, string phone)
אנו יכולים להבין שיש פה גורם מאחד ושלמעשה המתודה צריכה להיראות כך:
- private void SaveUser(User user)aveUser(string firstName, string lastName, string eyeColor, string email, string phone)
בדרך הזו אנו מרוויחים כמה דברים:
- הקורא מבין בקלות את את הקונספט ולא צריך לזכור הרבה פרמטרים.
- הופכים הבנה ‘משתמעת’ (מובנת בגלל ההקשר) להבנת מפורשת.
- זה מכמס (Encapsulation) נושא ובכך מונע מלהכניס ‘על הדרך’ נושאים שאינם קשורים.
לכידות גבוהה
לכידות גבוהה היא דבר חשוב בתפקוד תקין של קלאס. אם נותנים אחריות מבוזרת מדי לקלאס והוא מטפל ביותר מדי דברים, אזי נפגעת הלכידות שלו. הדברים שהקלאס מטפל בהם צריכים להיות קשורים זה לזה בצורה מובהקת.
כמה יתרונות לקלאס בעל לכידות גבוהה:
- קלאס שהלכידות בו גבוהה, הוא קלאס שקל לקרוא אותו ושמבינים בקלות מה הוא אמור לעשות.
- אם יש לכידות גבוהה אז קל לתת שם וממילא קשה להכניס דברים שלא קשורים. קל להבין זאת על ידי דוגמא הפוכה: אם לקלאס קוראים manager אז כנראה שנמצא שם המון דברים שאין ביניהם קשר.
על מנת להמנע מיצירת קלאסים בעלי לכידות נמוכה:
- המנע ממתודות שאין להם קשר לשאר הקלאס.
קלאסים בעלי לכידות נמוכה נראים כמו אוסף של דברים שאינם קשורים זה לזה.
- המנע משדות שבשימוש של מתודה אחת בלבד. שדות אמורים להיות בשימוש של כמה גורמים בקלאס. ואם הן לא – אז כנראה שהמתודה המדוברת לא קשורה באמת לקלאס.
- בSource Control אפשר לראות עד כמה משתנה קלאס. קלאס שמשתנה יותר מדי יכול לרמז על כך שהוא בעל לכידות נמוכה.
דוגמא:
קלאס בעל לכידות נמוכה:
- 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, יש פה חזרה על אותו הדבר. וכן הן מוסיפות ‘רעש’ – זה מוסיף טקסט שצריך לקרוא, אך הקריאה לא מוסיפה ידע (אפשר להניח שהקורא יודע לקרוא).
הערות הסבר
סוג נוסף של הערות שכדאי להמנע ממנו הוא הערות שמסבירות פרטים בקוד.
דוגמא:
- //Assure 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 וגם כי הנוכחות של קוד כזה מוסיפה הרבה רעש לקריאה. רואים את הקוד על המסך והוא מסיח את הדעת. הדבר דומה לקריאת עיתון בגרסת העורך – עם כל הקישקושים, ההערות, הגרסאות הקודמות וכו’.
נזכיר שוב שקוד ייקרא בעתיד כמה וכמה פעמים, כך שאין צורך לסבך את העסק.
בנוסף לזה קטע קוד שסומן בהערה יוצר עמימות ביחס למטרה. הקורא עלול לשאול את עצמו במהלך דיבוג – אולי היה צריך את הקוד שבהערה? אולי להסיר את ההערה? מה בעצם המטרה של הקוד? אם מתכנת רוצה לשפר את הקוד, ההערות האלה מפריעות לו מכיוון שהוא לא יודע האם הקוד שבהערה אמור לחזור בעתיד והאם השיפור שהוא עושה יתאים לקוד שבהערה.
כמה שאלות שכדאי לשאול את עצמנו לפני שנכניס קטע קוד להערה:
- מתי, אם בכלל, הקוד הזה יצא מההערות? אם לא ניתן לענות על השאלה הזו על ידי תאריך ספציפי, כנראה שצריך למחוק.
- האם אני יכול לשלוף בעתיד את הקוד הזה מה Source Control?
- האם זו עבודה לא גמורה שכדאי להוציא אותה לbranch חדש ב Source Control
- האם זה פיצ’ר שכדאי ליצור קונפיגורציה שמדליקה או מכבה אותו?
הערות כותרת
לפעמים יש הערות שנוצרו על מנת לתת כותרות משנה ולחלק את הקוד ליחידות קטנות וקריאות יותר. אין בזה צורך – הפיתרון העדיף הוא ליצור מתודות.
מקרה דומה הוא הערות שנועדו להסביר מה קורה בתוך קטע ארוך מאוד בין סוגריים מסולסלות. גם כאן נכון יותר להוציא את קטע הקוד הזה למתודה.
הערות יצירתיות
סוג נוסף של הערות שניתן למצוא פה ושם זה כותרות מנופחות לקלאס. כותרות המכילות המון פרטים על הקוד ועטופות בקישוטים וכוכביות.
משהו כזה:
//****************************
//* Class Name: MyService.cs
//*
//****************************
כמובן שזה מיותר למדי. כל המידע שבהערות הוא מידע הידוע ממילא.
הערות מנהליות
יש הערות המכילות מידע על הבאג שבגללו התווספה שורה מסויימת (כולל מספר הבאג ומה היתה הבעיה).
גם הערות כאלה מפריעות לקריאה השוטפת והמקום הנכון לזה הוא ב Source Control.
האם אין הערות נחוצות?
מתי בכל זאת נכתוב הערות?
- ToDo – השאיפה לשפר את הקוד קיימת תמיד, אך לא תמיד יש זמן לכך. לכן הערות מסוג ToDo יכולות לעזור. בVisual Studio יש אפשרות לראות רשימה ובה כל ההערות המתחילות במילים TODO, HACK, UNDONE.
בכל אופן גם בהערות כאלה צריך להקפיד על סטנדארט אחיד בצוות, להיזהר מהערות שמתחילות במילה TODO אבל למעשה מכילות אזהרות, התנצלויות וכו’. ולתקן כעת אם זה אפשרי – אל תדחה לעתיד הלא נודע.
- הערות סיכום – יש הערות המכילות סיכום של פונקציונליות מסויימת ממבט-על המאפשר להבין מטרה כללית בצורה רחבה יותר מאשר מקריאת הקוד עצמו. או הערות המכילות הבהרת מטרות עסקיות שקלאס אמור למלא.
בכל אופן צריך להיזהר שהערות סיכום לא יהיו תחליף לשמות משמעותיים המביעים מטרה.
- מסמכים – לפעמים משתמשים בקטע קוד של גורם שלישי, וניתן להוסיף הערה עם לינק לדוקומנטציה של אותה חברה.
עקרונות כלליים
לסיכום נעבור על כמה עקרונות כלליים בCLEAN CODE.
- אחרי שמתנסים בCLEAN CODE בשעת כתיבה ומתוודעים ליתרונות שלו, רוצים לשנות גם קוד קיים ולשפר אותו. אך כמאמר הקלישאה ‘אם זה לא שבור – אל תתקן את זה’.
- קוד שעובדים איתו – הרעיון של CLEAN CODE נועד לשפר קריאות קוד. אבל אין צורך לנקות קוד קיים עבור קוראים עתידיים שאולי יקראו את הקוד הזה ואולי לא. אם יש קוד קיים שרץ כבר 15 שנה באופן הזה, אל תסתכן בשינוי הקוד רק לצרכים אסתטיים.
- עלות-תועלת– אם יש קוד שקשה לך להבין אותו ואתה צריך לתקן בו באג – תקן את הבאג והמשך הלאה. אל תבזבז זמן יקר.
צריך לשקול את זמן שיפור הקוד לעומת זמן תיקון הבאג. אם הבאג דורש שינוי של שורה אחת, אך לעומת זאת שיפוץ הקוד דורש יצירה של כמה מתודות, אולי זה לא כדאי.
- כיסוי טסטים – לפני שאתה משפץ קוד קיים, וודא שיש לך כיסוי מספק של טסטים על הקוד. אם אין מספיק טסטים, אתה מסתכן ברגרסיה.
- בלי ‘חלונות שבורים’ – כאשר אנשים הולכים ברחוב ורואים בית עם חלונות שבורים, אין להם יותר מדי עכבות מליידות אבן נוספת על החלון או להיכנס בלי רשות לבית.
כאשר מתכנתים רואים קוד שאין לו ‘כבוד עצמי’ ושהוא כתוב בצורה מרושלת, גם הם לא מתאמצים יתר על המידה בכתיבת קוד נקי ומוקפד.
על מנת לתחזק קוד נקי צריך לתחזק גם ‘גאוות יחידה’ וכבוד מקצועי. אין לתת לטעויות שנעשו בעבר להיות פתח לטעויות חדשות.
- Code Review – קריאת הקוד על ידי מישהו אחר (לאו דווקא על ידי מישהו בכיר יותר), היא דרך טובה ויעילה להמנע מהמנטליות של ‘חלונות שבורים’. כאשר מתכנת יודע שבסוף היום יקראו את הקוד שלו, הוא באופן טבעי מתנסח בצורה נקיה ומדוייקת יותר.
- עבודה בזוגות (Pair Programing) – אמנם עבודה בזוגות לא כל כך מקובלת ברוב החברות בגלל תחושת בזבוז המשאבים, אך כדאי להתנסות בזה אפילו לפרק זמן קצר. לתכנות זוגי יש כמה יתרונות, אך אפילו מנקודת המבט של CLEAN CODE מרוויחים כמה דברים:
- code review בזמן אמת.
- שיפור באיכות – קל לוותר לעצמך, קשה שיוותרו לך.
- קל יותר לתת שמות משמעותיים כאשר אומרים את השמות בקול רם ומתאמצים לנסח באופן מדוייק לאדם אחר.
- קוד לא קריא ומסובך (המכונה קוד ספגטי) לא נוצר בבת אחת. אם לא מתרגלים לכתוב CLEAN CODE ייתכן ולא נראה בעיות, אך לאורך זמן הקוד יהפוך לבלתי קריא, מסובך וארוך.
- · “השאר את הקוד שאתה עובד עליו, מעט טוב יותר משמצאת אותו” (רוברט מרטין, על פי מייסד תנועת הצופים באדן פאואל: “השאר את העולם הזה מעט טוב יותר משמצאת אותו”)