מתי כדאי להימנע מ Mock Objects?

אני משתדל בבלוג שלי להביא רעיונות חדשים ולא טריוויאלים.

במקרה הזה לא מדובר ברעיון חדש בכלל (הנה פוסט מ 2012) – אבל ככל הנראה בהחלט לא טריוויאלי.

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

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

האמת שהבעיה היא לא דווקא ב Mock Objects, כאשר אומרים "Mock Objects" הכוונה לרוב היא ל Stubs או ל Fakes – אבל ההבחנה לא חשובה. אני מדבר על השימוש המוגזם בכל סוגי ה Test Doubles.

לכאורה, כשלומדים לכתוב Unit Test מתחילים עם בדיקות פשוטות וישירות. כשרוצים "להתקדם" ומחפשים "מה אפשר לעשות מעבר?" מגיעים לעולם של Test Doubles – וה Frameworks השונים שעוזרים ליצור ולנהל אותם (כמו Mockito, SinonJS, MSW, ועוד עשרות), ונוצרת הרגשה שאנו "עושים משהו מתקדם יותר".

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

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

האם Mocks הם תמיד רעיון רע?

ברור שלא.

אני אצמד להבחנה של Uncle Bob שמאוד נכונה בעיני:

  • Mocks בין מערכות – הם חשובים, ואף נדרשים בכדי לבצע בדיקות בצורה יעילה.
  • Mocks בתוך מערכת – הם טלאי (Patch). אפשר "לתפור" טלאי פה ושם, וזה לא רע. הבעיה היא כאשר המערכת שלנו הופכת לערימה ענקית של טלאים – וזה קורה לא מעט.

Mocks בין מערכות – הם חשובים, ואף נדרשים בכדי לבצע בדיקות בצורה יעילה

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

כאשר יש לנו בארגון כ 50 מיקרו-שירותים ואנו כותבים בדיקה המפעילה מספר של מיקרו-שירותים (נקרא לבדיקה כזו "System Test") אזי:

  • Scope הבדיקה הוא גדול: בדיקה בודדת מפעילה כנראה מאות או אלפי שורות של קוד.
    • קשה מאוד להתמקד במקרי קצה בתוך ה Flow, והנטיה האנושית היא לא באמת לבדוק מקרי קצה.
    • כשהבדיקה נופלת לא ברור לרוב מה נכשל – צריך להתחיל ולחקור. כלומר: כישלון של בדיקה מוביל לעבודה משמעותית נוספת – לפני שאפשר לתקן את הקוד.
  • סביר יותר ויותר שזמני הריצה של הבדיקה יהיו גבוהים.
    • נחשיב בדיקה שאורכת יותר מ 2 שניות – כבדיקה ארוכה. 2 שניות הן המון זמן מחשוב, אולי כדאי לחשוב עליהן כ 2,000,000,000 ננושניות – ולזכור שמחשבים בימנו מבצעים בננו-שנייה פעולה.
    • כאשר יש לנו הרבה בדיקות (דבר טוב!) והבדיקות אורכות זמן רב => זמן ההמתנה לתוצאות הבדיקה אורך => תדירות הרצת הבדיקות פוחתת => גדל הזמן הממוצע מכתיבה של קוד שגוי – עד שאנו מגלים זאת => Feedback cycle ארוך יותר.
    • "סטנדרט הזהב" להרצה של בדיקות טוען שהמתנה של יותר מ 10 דקות להרצה של בדיקות אינו סביר. לאחרונה אני רואה התפשרות על המדד הזה, ויש כאלו שגם מדברים על 15 דקות של הרצה כזמן סביר / רצוי.
מכאן, אפשר לכתוב הרבה בדיקות, שירוצו הרבה זמן – ולהתדרדר ב Feedback cycle של המפתח.
הפתרון הברור (וכמעט היחידי) הוא להקדיש את רוב הבדיקות ליחידה קטנה יותר של המערכת: מיקרו-שירות בודד. כואב לי לחשוב כמה סבל אנושי מצטבר לפספוס הנקודה הזו. לעתים בדובר בשנות-אדם רבות, ברמת הארגון הבודד. אאוץ!
אי אפשר לבדוק מיקרו-שירות ברצינות בלי שהוא יקרא לשירותים שהוא תלוי בהם. לכן חייבים לכתוב Mocks שידמו את המערכת / מיקרו-שירותים האחרים שהשירות שלנו תלוי בהם – בזמן שבודקים את השירות.
ה Scope המצומצם של בדיקת מיקרו-שירות בודד – רק תשפר לנו את המדדים החשובים:
יכולת התמקדות הבדיקה במקרי קצה, זמני איתור תקלה, וזמני הריצה של הבדיקה.
כמובן שנכון לשמור גם על כמות מסוימת של System Tests שיבדקו את האינטגרציה בין שירותים שונים. לבדוק שהם ממשיכים לדבר באותה שפה.

Mocks בתוך מערכת – הם טלאי (Patch), שיש לצמצם את השימוש בו.

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

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

  • לוגיקה שהשירות מבצע – להלן "Pure Business Logic" (הכתובים כ Pure functions, כמובן)
  • לוגיקה של תקשורת עם שירותים אחרים – להלן "Integration Logic".
ההפרדה הזו קלה בעת כתיבת קוד חדש – וכמעט בלתי אפשרית על גבי קוד קיים שכתוב כך.
כאשר עושים את ההפרדה – קל לכתוב בדיקות יחידה בלי Mocks.
כאשר לא עושים את ההפקדה – קשה מאוד לכתוב בדיקות יחידה, ואז מגיע שימוש מופרז ב Mocks.
ככל אצבע, אני מחשיב שימוש ב Mocks כמופרז אם יותר מ 10% מבדיקות היחידה שלנו משתמשות ב Mocks.
אני לא מתכוון להמליץ פה לקחת קוד קיים ולבצע הפרדה בין הקוד. זו מלאכה קשה, ארוכה – ולא מתגמלת.
אני ממליץ בחום לכתוב את כל הקוד החדש שלכם עם כזו הפרדה. זה נכון כמעט לכל סיטואציה.
העבודה הנוספת בהפרדה בין לוגיקה עסקית ללוגיקה של אינטגרציה:
  • דורשת מודעות ותשומת לב.
  • מוסיפה מעט עבודה בעת הקידוד (נאמר: 10-15%)
אבל:
  • משפרת את המודולוריות (ומכאן – ה Design) של הקוד
  • מאפשר לבדוק אותו בצורה יעילה הרבה יותר, הן מבחינת עומק הבדיקות, והן מבחינת זמן שמושקע בכתיבת בדיקות.

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

מה הבעיה בשימוש ב Mocks בבדיקות -יחידה?

הנה דוגמה טיפוסית ל Heavily mocked test, ראיתי אינספור כאלו בחיי – ואראה כנראה (אולי הפוסט יעזור?) עוד אינספור בעתיד:
מה הבעיה בבדיקה הזו?
  • היא רצה ומצליחה!
  • אם מחקתי כמה שורות קוד בפונקציה הנבדקת ()doSomething – היא נכשלת. כלומר: היא בודקת משהו.
  • השתמשתי ב mocks frameworks בצורה יעילה – וחסכתי המון קוד לו הייתי כותב את ה Mocks בעצמי.
מה עוד אפשר לבקש?!
יש בבדיקה הזו, או בדפוס של הבדיקה הזו כמה בעיות חמורות. לרוע המזל – אלו לא בעיות שיצוצו מחר, אלא טיפוסי יותר שיצוצו עוד שנה – לאחר שכתבנו עוד מאות בדיקות כאלו, והתחפרנו / קיבענו חזק יותר – את בעיה.
 
בעיה: לא ברור מה בדיוק נבדק, מה הצלחת הרצה של הבדיקה – באמת אומרת.
כשאני קורא את קוד הבדיקה, גם בלי obfuscation ושמות משמעותיים – אני מבין שבוצעה פעולה, אבל אני לא יכול לדעת מה חלקה של הפונקציה ()doSomething בעבודה – ומה חלקם של ה Mocks שלה.
הדרך היחידה שלי להבין מה החלוקה, ומה באמת ()doSomething עושה לאחר שמסירים ממנה את ה Mocks – היא להיכנס לקוד ולקרוא אותו. לפי מספר ה mocks אפשר לנחש כמה זה יהיה קל. הרבה פעמים קריאה שטחית – מפספסת חלק מהעניין.
גם כאשר אני כותב בדיקה בתצורה הזו והיא הגיונית, לאורך זמן ושינויים (refactorings במערכת) – יש סיכוי שהיא תאבד את המשמעות שלה.
שוב ושוב ושוב נתקלתי בבדיקות מהסוג הזה שהיו קליפת שום ריקה – שלא בדקו שום דבר. זה נראה מצחיק ומגוחך שכל שאני יוצר Mock עם ערך x ואז מריץ בדיקה ששולפת את x ומראה ש x == x, אבל זה קורה גם לאנשים חכמים שמבינים קוד.
כאשר עושים refactoring במערכת – אי אפשר להבין אלו בדיקות Mock Heavy עומדות לאבד את ערכן.
כאשר הבדיקות הללו נשברות ומתקנים אותן כחלק משינוי – קשה מאוד לוודא שאנחנו משמרים את הערך שלהם. הכלל בגלל שמה שנבדק הוא משתמע ואינו גלוי.
לכן, זו היא בעיה בתהליך / בתבנית – ולא בקוד הספציפי.
בעיה: הבדיקה בודקת איך דברים קרו (מבנה), לא מה קרה (התנהגות).
בעצם הבדיקה בודקת שכאשר מפעילים את ()doSomething נקראות פונקציות כאלו וכאלו במערכת, עם פרמטרים מסוימים ו/או ערכים מסוימים ו/או לא נקראות פונקציות אחרות.
לא ברור לנו אם בסוף, קצה לקצה, הלקוח קיבל את ההנחה שרצינו.
בקלות, אפשר לשמור את סדר הקריאות (המבנה), אבל להיכשל בתוצאה (התנהגות).
"האא! הבדיקות לא גילו את זה כי זה היה באג ב SQL" – הוא סוג התירוץ שאנו מספרים לעצמנו במקרים האלו. "אולי כדאי להוסיף גם בדיקה גם על מבנה השאילתא" (בבקשה: לא!)
כאשר:
  • משתנה התנהגות במערכת – אולי נצטרך לשנות את הבדיקה ואולי לא.
  • משתנה מבנה המערכת – כמעט בטוח שנצטרך לשנות את הבדיקה, ואולי עוד רבות אחריה.
מצב איום שאפשר להגיע אליו, הוא שכאשר אנחנו רוצים לעשות Refactoring משמעותי במערכת – רבות מהבדיקות הללו ישברו. ייקח לנו זמן רב לתקן את כולן, מעין "יום עבודה לבצע Refactoring – ושבועיים עבודה לתקן את כל בדיקות".
כאשר נבצע שינוי מבנה, הבדיקות לא ישרתו אותנו בבדיקת רגרסיה של התנהגות – כי הן נשברו בגלל שינוי המבנה.
הבדיקות הללו מעבירות אותנו סדנאת חינוך איומה: לא כדאי לשנות את מבנה המערכת. המערכת הזו "בדוקה היטב" (חחחח), אך היא לא אוהבת שינויים.
קוד שלא מתחדש – הוא קוד גוסס. דפוס הבדיקות הללו עוזר לקוד לגסוס זמן קצר לאחר שנכתב לראשונה.
בעיות נוספות
בעיות נוספות הן:
  • מוטיבציה נמוכה לבדיקת מקרי קצה – כי כתיבת כל מקרה קצה דורשת עדכון (ותחזוקה לעתיד) של עוד ועוד Mocks.
  • צורך בתחזוקה שוטפת של ה Mocks: כל הוספה של פרמטר או שכבה לוגית – דורשת של עדכון של עוד ועוד בדיקות.
  • זמני ריצה ארוכים יותר של הבדיקות
  • נטיה לכתוב קוד בדיקה מתוחכם ("Mocking Sophistication") שמקשה על קריאת קוד הבדיקה.
כל אלו הן בעיות אמיתיות, אבל הן מחווירות מול הנזק שבכתיבת קוד שאינו נבדק לעומק, ומקשה על ביצוע שינויי עומק במערכת. שוכחים מכיב קיבה – כשיש סרטן.
לגיקים שבינינו: הכוונה ל Port = "נמל". לא IP Address port 🙂

סיכום

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

הרשו לעצמכם קצת אי-סדר בקוד (דעה)

כשמדברים על סדר ו״ניקיון״ קוד – יש סקאלה של מצבים אפשריים:

  • בקיצוניות אחת: ״בנה ותקן״ בו כותבים את הקוד המיידי ביותר האפשרי בכדי להפעיל את הפיצ׳ר הבא, מגלים באגים ואז הולכים לתקן אותן (וב 20% מהפעמים או יותר יוצרים באג חדש). כל העניין של סדר הוא ״למרובעים״ או ״אנשים בעלי זמן פנוי״.
  • בקיצוניות שניה: פדנטיות קוד, בה כל קבוצת שורות של קוד עוברות refactoring וסידור על בסיס יומי. מקסימום סדר, מקסימום encapsulation, שום גרם מיותר של אי-סדר בקוד.

לכאורה פדנטיות הקוד היא המצב ״הטוב״ ו'בנה ותקן' הוא המצב ״הרע״ ולכן רובנו שואפים לפדנטיות קוד – אבל אף פעם לא מגיעים לשם: מאוד קשה להגיע לשם, ובמיוחד כאשר יש deadlines ו deliveries.

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

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

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

כמו מהירות האור שהיא יעד בלתי-מושג לגוף עם מאסה, כך גם ״סדר קוד מופתי״ הוא יעד בלתי אפשרי לגוף בעל תפיסה-ביקורתית*.

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

* עבור גופים חסרי-ביקורתיות, כל קוד נחמד נתפס כ״בעל סדר מופתי״ – כך שאין כאן מאמץ ניכר. כמו אור שנע במהירות ה… אור – בלי בעיה.

מדע בידיוני, בינתיים.

מדוע בכלל לחתור ל״סדר מופתי״ בקוד?

לקוד של תוכנה יש בעיה גדולה של נראות (visibility): לערמה גדולה של קוד אין צבע, אין ריח, אין תחושה שניתן למשש או מרקם שניתן לראות.
כאשר דג במסעדה נרקב אזי הוא מעלה ריח, צבעו הופך דהוי, המרקם משתנה וגם תחושת המגע. יש לנו ,כהומו-ספיאנס, מגוון חושים המאפשרים לנו לזהות בקלות מצב של ריקבון.
כאשר בסיס קוד נרקב – אין להומו-ספיאנס שום חוש או מנגנון טבעי לדעת זאת. הקוד הנרקב מרגיש, במבט-על, בדיוק כמו כל קוד אחר, אולי אפילו כמו הקוד הטוב ביותר שנכתב על כדור-הארץ, מעולם.
מגוון כלים אוטומטים, שהתעשייה והאקדמיה, ניסו לבנות לאורך השנים בכדי לזהות ריקבון של קוד [2] – כשלו במבחן המעשה:
  • הם יתריעו על המון בעיות גם בבסיסי-קוד טובים, וגם בבסיסי-קוד לא טובים.
  • תיקון הבעיות והגעה למצב ״טוב״ (ע״פ הכלי) היא בד"כ השקעה כלכלית לא יעילה: הרבה השקעה – ומעט ערך.
גם השימוש בחוש הריח של קבוצת מפתחים – לא הוכיח את עצמו לאורך השנים. האקט של איגוד כמה מפתחים על מנת ש ״ירחרחו״ בסיס קוד – מניב תוצאות לא מדויקות בעליל.
כמטאפורה, מפתחים אוהבים את ריח-הגוף שלהם, ושונאים כל ריח-גוף אחר. אם הקוד דומה מאוד לקוד שלהם (למשל: הם כתבו אותו) – התשובה תהיה: ״סבבה!״.
אם זה קוד זר ושונה – הם יחזרו מהרחרוח עם מבט מיוסר ומיואש – ושלל תיאורים עד כמה הקוד רקוב ומסוכן.
הדרך האמינה לזהות בסיס קוד רקוב בצורה דיי ודאית, היא מצב הריקבון המתקדם: צפיפות גבוהה של באגים + עלות גבוהה לתקן כל באג / לבצע שינויים.
הבעיה היא שמצב כזה הוא כבר מאוחר מדי – ובד״כ יש צורך בשלב כזה לכתוב את בסיס-הקוד מחדש. באסה!!.
הפתרון המקובל לבעיה הוא רחוק ממושלם, אך הוא הגרוע-פחות מהאלטרנטיבות המקובלות האחרות. מהותו: יצירת תרבות המקדשת איכות הקוד כערך עליון.
זה נשמע לכאורה כמו בזבוז אדיר, כי ברור ש״קוד מושלם״ איננו נקודת אופטימום כלכלית / ליצירת התוכנה הטובה ביותר.
המזל הטוב מאיר בכך שרוב האנשים נשברים באמצע הדרך ל״קוד המושלם״ ובעצם לא משקיעים את כל המאמצים האדירים האפשריים. "החולשה האנושית" – מאפשרת לחתירה ל"קוד מושלם" להיות בעלת תוחלת כלכלית סבירה יחסית.
לשאיפה ל״קוד מושלם״ יש גם תופעות לוואי. פער הציפיות בין הרצוי למצוי גורם לתחושות תסכול מאי-ההגעה למטרה.
יש סברה, למשל, שזו הסיבה שאין כמעט חברות תוכנה יפניות מצליחות. בתרבות היפנית – הפער והתסכול במקרה הזה הם בלתי-נסבלים. בתרבות ההודית, למשל – אין בעיה כזו.
.
היכן נמצאת ישראל? איפשהו באמצע. התסכול קיים, ושונה בין מפתח למפתח – כאשר עבור רוב המפתחים זהו תסכול נסבל, ורק למיעוט דקדקן – נגרם סבל ממשי ועמוק.
התסכול / היעד הבלתי האפשרי הוא מחיר קיים שהתעשייה שלנו משלמת על מנת להתמודד עם חוסר היכולת לאמוד בצורה ברורה איכות של בסיסי-קוד. "יורים חזק ומקווים שיהיו מספיק פגיעות". אם תשאלו גנרלים בצבאות שונים שהיו בסכסוכים – תגלו בוודאי שהפרקטיקה הזו אינה זרה להם.

מדוע אי אפשר פשוט לקום ולהחליט: ״בואו נעשה just enough הנדסת תוכנה / איכות קוד״ – על מנת להיות בנקודת אופטימום כלכלי?!
– כל עוד אין לנו שום נקודת אחיזה אובייקטיבית ויעילה לגבי איכות הקוד, והאם היא מעל או מתחת לנקודת האופטימום (אפילו בקירוב) – לא ניתן באמת לנקוט בגישה ההגיונית הזו.

כוונות מול מציאות

מה עושים?ֿֿֿ

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

האם אני מציע לוותר על ״איכות קוד״ כערך עליון?

זו דילמה קשה!

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

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

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

  • הגנה בפני מקרי-קצה תאורטיים, שלעולם לא יתרחשו. 
    • לעתים מדובר בימים של עבודה רק כדי ש״הקוד יהיה מסודר״.
  • הרחבת ה Framework / מבנה הקוד למקרים עתידיים – שלא עומדים להתרחש.
  • ביצוע סידור גדול בקוד – רק על מנת לגלות חודש מאוחר יותר שהוא מגביל / לא נכון – וצריך לסדר מחדש.
  • פתרון באגים שלא מפריעים לאף אחד או לפחות לא יגרמו לשום impact עסקי שניתן למדוד.
  • שיפורי UI חסרי-חשיבות:
    • למשל התאמה של מסך אחד – שהיה עקבי יותר עם מסך אחר, אבל בעצם משתמשים שונים משתמשים בשני המסכים – אז כמעט אף אחד לא ישים לב.
  • שיפור ביצועים – במקומות שלא זקוקים להם. 
    • ראיתי להרבה שיפורי ביצועים בחיים – שלעולם לא היו זקוקים להם, ורבים מהם בכלל לא היה ניתן למדוד מרוב שהם היו זניחים.

אז מה, בכל זאת, עושים?

טוב. אני מקווה שהצלחתי להעביר את הדילמה.
גישה אחת היא להמשיך בדרך המקובלת יותר בתעשייה – הצבת איכות כערך עליון של הארגון.
גישה נוספת שאני רוצה להציע היא ניהול דינאמי של מצב איכות הקוד:
  1. ננהל רשימה "חיה" של היבטים לא אופטימליים בקוד. את הרשימה כדאי לאסוף ממגוון חברים בארגון.
    1. אם תנהלו את הרשימה הזו ברצינות – מהר מאוד תגיעו לעשרות פריטים ברשימה, ויותר.
    2. אם לא הגעתם – סימן שלא חיפשתם מספיק טוב. זה כמו לשאול את עצמכם ״במה אני פחות טוב?״ – ולא למצוא שום דבר.
  2. מתוך הרשימה – מצאו פריטים בעלי Impact: עדיף impact עסקי, אבל גם impact טכנולוגי – פחות בעיות, יתר קלות לקוד שנוסף למערכת, בקרה טובה יותר על המערכת וכו׳.
    1. סביר שייקח לכם כמה סיבובים על מנת להבין היכן באמת נמצא ה Impact – רעיונות עלולים להישמע טוב, אך להיות חסרי impact לחלוטין בפועל.
  3. תזמנו זמן עבודה מוגדר לצורך שיפורים יזומים ופתרון הפריטים החשובים ביותר ברשימה. ה benchmark הבריא שאני מכיר הוא כ 20% מזמן העבודה
    1. פרשנויות שונות מה עושים בזמן הזה (כמו: ״באגים הם ברשימה״) – יכולים להפוך אותו ללא רלוונטי.
  4. שווה לעבוד בצעדים קטנים. אם יש בעיה גדולה, הרשו לעצמכם למצמצם אותה ולראות מה ההשפעה העסקית שנובעת מכך. למשל: תהליך ה deploy אורך 15 דקות? נסו לבצע שיפור (נניח: המקצר אותו ל8 דק') ולראות מה ההשפעה בפועל. זה יותר טוב מלהשקיע פי כמה עבודה בכדי להביא אותו ל 2 דקות – ולגלות שה impact לא כ"כ משמעותי.
  5. חשוב לערב את אנשי הצוות בהחלטות ובעשיה.
    1. זה לא רק עניין של engagement ושותפות-גורל. זה גם עניין של ״חלונות שבורים״: לחדד ולהזכיר לכולם שקוד טוב הוא ערך חשוב – ושאנחנו כן משקיעים בו. ושהשקעה בו נושאת פרי.

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

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

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

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

  • פירוק של טבלה גדולה ב DB לכמה טבלאות – ע״פ דפוסי שימוש שונים.
  • העברת נתונים מבסיס הנתונים ל Redis.
  • שינוי הייצוג של הנתונים – למבנה קל יותר לעבודה.
  • פירוק שרת Redis לשני שרתים ע״פ דפוס שימוש: כזה ל cache וכזה לנתונים שחשוב שיישמרו. הפירוק אפשר לנו לנקות את ה caches ביעילות מבלי גרימת נזק.
  • כתיבת מודול או microservice מחדש. צריכות להיות סיבות טובות למדי – לכתיבה מחדש (ופעמים רבות – יש כאלו).
  • שינוי מבני של flow מורכב במערכת.
  • ניקוי קוד ישן / נתונים ישנים מבסיס הנתונים.
  • הוספת כלי ניטור על מצבים חשובים במערכת – שחסרה נראות שלהם (דברים מאוד ספציפיים)
  • Key Security Items
  • Key Performance Items
  • סידור נתונים ל BI
ברור שהיה עדיף למנוע מראש את היווצרות של המצבים הללו. בהינתן הדינמיקה הארגונית / הלחצים / וחוסר ידיעת העתיד – עד כמה זה באמת אפשרי?
ברור שזו רשימה ספציפית בהקשר ספציפי – שלא אוכל להעביר בפוסט.

הנה כמה פריטים שהיו ברשימות שכאלה, ולעולם לא הגיעו לידי מימוש:
  • סידור flows מסוימים בקוד בכדי שיהיו פשוטים וברורים יותר (הרבה מקרים).
  • HTTP being used internally (ולא https)
  • מקרים מסוימים בהם כשל במיקרו-שירות אחד – גורר כשל במיקרו-שירות אחר
  • Schemas שונים בבסיס הנתונים שנמצאים ב encodings שונים: אחד בשוודית והשני ב ISO.
  • ניקוי ה git repository והסרת 25MB מיותרים.
  • Various Security Items
  • Various Performance Items
  • סידור נתונים ל BI
  • וכו׳

האם הפריטים הללו ראויים לרשימה?!

בוודאי שכן!:
  • הם מהווים נקודת reference חשובה – מה יותר חשוב ממה. מה יגרום ליותר impact ממה. את הרשימה נרצה כל הזמן למיין כאשר הפריטים בעלי ה impact הגבוה ביותר נמצאים בראש – ועוברים תדיר לעבודה.
    • נ.ב.: עדיף פריטים קטנים שנכנסים כל הזמן לעבודה – מפריטים גדולים שנכנסים לעבודה רק פעם בחצי-שנה או רבעון. זהו מסר חינוכי ומתמרץ, וגם הזדמנות להכניס יותר שיפורים משמעותיים.
  • עצם מעבר על הרשימה עם הצוות יוצר את ההבנה שאלו דברים לא טובים, שיש להימנע מהם בעתיד. יש פה למידה, ובניית קונצנזוס – דברים חשובים גם כן.
  • עם הזמן, בעיות מחמירות / גדלות בחשיבות – ואז פריטים שהיו בתחתית הרשימה – עוברים לראשה. חשוב לעקוב ולהבין את הבעיות על מנת לאתר אותן ולטפל בהן מספיק מוקדם.
    • בכלל, יצירת בסיס-ידע של בעיות במערכת שקרו ואפשר ללמוד מהן – הוא דבר שימושי. מה שקורה במערכת שלנו והוכח כבעייתי – הוא לקח הרבה יותר חזק ומשמעותי ממקרים תאורטיים, המתוארים בספרות כתובה כלשהי.

סיכום

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

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

אני זוכר את התקופה שעבדתי ב SAP והיינו פותחים Critical Bug (הרמה הגבוהה ביותר) על כל מקרה בקוד בו עשו [3] catch Throwable (מעל ה JVM). היום אני מתעלם ממקרים כאלו – ואפילו כותב catch throwable בעצמי – אם זו הצורה המקובלת לתפוס Exception בבסיס הקוד בו אני עובד.

היה לנו איזה כלי בשם Sonar (ניתוח סטטי של קוד) שניסה לחשב את ה Technical Debt של בסיס-קוד שהוא סרק בצורה מספרית / דולרית. לכל Catch Throwable הוא נתן תג מחיר של  כמה עשרות דולרים (לא זוכר בדיוק). יכולתי לפתוח tickets חסרי impact לחלוטין, ברמת הדחיפות הכי גבוהה – ולהרגיש טוב עם עצמי שאני חוסך כסף לחברה. צעירות!

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

[2] על בסיס מדדים כמו cyclomatic complexity או מספר הפרמטרים המועברים לפונקציה.

[3] למרות שהייתה כוונה ברמת ה JVM יום אחד לעשות שימוש ב Throwable למטרה מעט אחרת – כבר עברו 23 שנה, ולא נעשה בו כל שימוש. כ״כ הרבה מפתחים השתמשו כבר ב Throwable כאילו הוא Exception לכל דבר – כך שכבר לא נראה לי שניתן לייחס לו בגרסה עתידית משמעות אחרת.

כללי התיכנותיקה – 19 כללים של הנדסת תוכנה שלא ניתן להתחמק מהם

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

מכיוון שאין להם שם מוכר, נתתי להם שם: \"כללי התיכנותיקה\" (הלחם לשוני של \"תכנות\" ו\"פיסיקה\").

מקור: http://potluckcomics.com/links-laws-of-physics

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

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

הנה הם לפניכם:



Conway’s Law ארגוני ארכיטקטורה
\"מבנה התוכנה והקשרים בין חלקיה, סופם שיהיו שיקוף של המבנה הארגוני של הארגון בה היא מפותחת\" (רפרנס)
הסבר: המבנה הארגוני הוא כוח חזק מאין כמוהו, שלא ניתן להתעלם ממנו. למשל: אם אתם מתכננים קומפיילר בין 4 שלבים, אך יש לכם 3 צוותי פיתוח – סביר שבסופו של דבר אחד השלבים יהיה אנמי, ובפועל יהיה לכם קומפיילר עם 3 שלבים. הפתרון – להכיר במציאות: או שתתכננו קומפיילר בין 3 שלבים, או שתארגנו את האנשים ב 4 צוותים.


עיקרון פארטו כללי
\"במקרים רבים, 80% מההשלכות (effects) נובעות מ 20% מהגורמים הפעילים (causes)\"
רקע: ידוע גם בשם \"כלל 80-20\", והוא כלל שנוסח על ידי הכלכלן האיטלקי וילפרדו פארטו, לאחר שראה את פיזור העושר באיטליה במאה ה-19 (כיום, פיזור העושר הוא קיצוני הרבה יותר, ואולי לא משקף כבר \"התפלגות טבעית של סיבה ותוצאה\").
וריאציה א: מוצר
\"80% המערך של מוצר, ניתן להשיג ב 20% מההשקעה\"
וריאציה ב: קוד
\"80% מהבאגים המשמעותיים, נובעים מ 20% מאזורי הקוד\"
וריאציה ג: ארגוני
\"כ 66% מהעשייה המשמעותית, נעשית על ידי כ 33% מהמתכנתים\", מה שמתבטא גם ב:




כלל הכישרון ארגוני טכנולוגיה
\"האלמנט שמנבא בצורה הטובה ביותר הצלחה או כישלון של תוכנה הוא לא הטכנולוגיה, שפת התכנות, או ה Framework – אלא המתכנתים שכותבים את התוכנה\"



עקרון הדורות המקוצרים (short generations) כללי
\"כל עשור יהיה על איש התוכנה ללמוד 50% מהמקצוע מחדש\"
וריאציה: \"כל עשור, 100% מהטכנולוגיות שבשימוש – יתחלפו\".
מי שלא מוכן / מבין את ההשקעה הנדרשת בכדי להישאר במקצוע – ייפלט מהתחום במשבר הראשון לאחר מעבר של \"דור\".


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


עקרון ה Not Invented Here (בקיצור: NIH) קוד הטיה אנושית
\"הדשא של השכן ירוק יותר, אך הקוד של השכן הוא גרוע, לא מובן, ולעתים פשוט מטופש\"

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


העיקרון הביולוגי של הקוד (הדוד בוב) קוד

\"Code Rots\"
כלומר: קוד שלא עובר חידוש (refactoring) – הולך ו\"נרקב\" ואיכותו פוחתת. השקעה רק בפיצ\'רים, ללא השקעה בחידוש הקוד תביא את הקוד למצב שאיננו ניתן יותר לתחזוקה, ויש לכתוב אותו מחדש. Guideline מקובל לאורך החיים של תוכנה עד לשלב בו יש לכתוב אותה מחדש (אם לא בוצעו פעולות refactoring, \"הזרמת חמצן\") הוא 3 עד 5 שנים.
היבט נוסף הוא העיקרון ש\"קוד שלא נקרא כחצי שנה – הופך לקוד זר ולא מוכר\" (ידוע כ Eagleson\'s Law). רמת הקריאות של הקוד, ועוצמת המסרים והעקרונות המשתקפים מהקוד – משפיעים רבות על קצב ה\"ריקבון\" של הקוד.


Lubarsky\'s law of Cybernetic Entomology קוד
\"תמיד יש עוד באג אחד במערכת\"
למשל: לאחר 11 שנה של שימוש המוני, נתגלה באג באלגוריתם החיפוש הבינארי של ספריית Java הסטנדרטית. (באג מאוד פינתי, כמובן).


לחלופין: Linus’s Law – הופרך כמיתוס
\"Given enough eyeballs, all bugs are shallow\".
כלומר: בהינתן מספיק עיניים בוחנות – כל הבאגים יימצאו.
הכלל נקרא על שם לינוס טורבאלדס, יוצר הלינוקס, מכיוון שהוא מבטא סוג של הנחה סמויה של קהילת הקוד הפתוח. הכלל לא הוגדר ע\"י לינוס, אלא על ידי אדם אחר, אריק ריימונד.

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



ללא שם (Michael Nygard) פרודשיין
\"משתמשים מגבירים בצורה בלתי-מוגבלת את אי-יציבות המערכת\"
בנוסח אחר: \"לעולם לא ניתן ליצור סביבת stating/testing שתחווה את כל הבעיות של סביבת production\".
זו גם סוג של עקיצה למי שכותב תוכנה ללא משתמשים, ונמצא ב\"אשליה\" שהקוד שלו יציב, scalable, וכו\'.
הכלל הוזכר לראשונה בספר (המצוין!) \"!Release It\"

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


Sturgeon’s Revelation כללי
\"90% מכל דבר זה שטויות\"

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


The Hype Cycle טכנולוגיה

הסבר: טכנולוגיות חדשות נוטות לחזור אחר אותו מחזור של Hype שטכנולוגיות עבר עברו:

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

הנה דוגמה למיפוי של טכנולוגיות עכשוויות, והמיקום שלהן ב Hype Cycle:

לחצו להגדלה. מקור: Gartner.


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


כלל ההקשר השגוי מתודולוגיה
\"אין רעיון טוב שלא ניתן להשתמש בו בצורה שגויה לחלוטין\" (ידוע גם כ Flon’s Axiom)



חוק ה 90-90 ניהול זמנים / פרויקטים
\"90% מהקוד – ייכתב ב 90% מהזמן. יתר עשרת האחוזים מהקוד – ייכתבו ב 90% נוספים מהזמן.\" (רפרנס)
יש כמה כללים דומים שמבטאים רעיון דומה:

  • \"הערכת חסר תינתן, גם כאשר אנו יודעים שאנו נוטים לתת הערכות חסר.\" (Hofstadter\'s Law)
  • \"הזמן המוערך לסיום פרויקט תוכנה הוא פונקציה מונוטונית עולה.\"
  • \"הזמן שנותר לסיום הפרויקט – הוא תמיד קבוע\". (Hartree\'s Law)

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



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


Brook’s Law ניהול זמנים / פרויקטים
\"הוספת אנשים לפרויקט מאחר ובשלב מתקדם – רק תגרום לפרויקט לאחר יותר\" (רפרנס)
הסיבות:

  • יש זמן ramp-up עד שאנשים משתלבים בפרויקט ונהיים יעילים בו: זמן למידה וזמן הסתגלות של הסביבה אליהם.
  • התפוקה השולית הפוחתת של כל עובד נוסף בפרויקט – יורדת. בהגדרה: פרויקטים קטנים יותר (מעט אנשים) הם יעילים יותר מפרויקטים גדולים.

צורה אחרת: \"The bearing of a child takes nine months, no matter how many women are assigned\"


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

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


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



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

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

כ 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): \"השאר את השטח נקי יותר ממה שקיבלת אותו\".
כל פעם שאתם נוגעים בקוד ומבצעים שינוי – שפרו מעט את הקריאות וקדמו מעט את הקוד לעבר \"קוד ספרותי\". עם הזמן – השינוי יהיה ניכר ויגיעו גם התוצאות.

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







4 כללים למדידת פשטות של תוכנה

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

אחת ההגדרות שאני אוהב היא של בחור בשם J.B. Rainsberger, הגדרה של ״מבנה פשוט של תוכנה״.

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

ע״פ ריינסברגר (עוד), ישנם ארבעה אלמנטים (עם סדר עדיפויות ברור) המובילים לתוכנה פשוטה:

  1. כל הבדיקות עוברות. 
  2. צמצום כפילויות קוד. 
  3. הקוד מתאר כוונה (clarity). 
  4. צמצום  מספר האלמנטים בקוד למינימום האפשרי  אלמנטים שאינם משרתים את מטרות 1-3.

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

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

הנה דוגמה של קוד \"ללא כפילויות\" שהפריע לי והפכתי לכפול:

אני מסכים, זה לא קוד \"מושלם\", וספריית templating הייתה יכולה להפוך אותו ליפה יותר, אך זו דוגמה אמיתית מהחיים.

הנה הקוד לאחר השינוי:

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

קוד המתאר כוונה
אם מזקקים את הכלל של \"כתיבת קוד המתאר כוונה\", לרוב עיקר העבודה היא מסביב לשמות: שמות של פונקציות או משתנים, או שמות חדשים הנוספים ע\"י פעולות \"extract method\" או \"introduce variable\".

ישנן 4 \"דרגות\" של שם:

  1. שם סתמי
  2. שם נכון
  3. שם מדויק
  4. שם בעל משמעות (\"meaningful\").
עצלנות ולחץ גורמים לנו להיצמד לתחתית הסקאלה (1,2), בעוד הקפדה ומקצועיות דוחפים אותנו לראש הסקאלה (3,4).
מאוד נהניתי, כשקראתי את ריינסברגר, כשהוא מתאר בדיוק רב הרגל שגם אני רכשתי: כאשר אני רואה קוד כפול אני מבצע extract method לשורות הכפולות, גם מבלי להבין לעומק מה המשמעות שלהן. פעולה טכנית גרידא.
אני נותן לפונקציה שנוצרה את השם \"foo\". מבלי לחשוב. תוך כדי עבודה אני מבין מה הפונקציה עושה ומשנה את שמה. לאחר 10 דקות עבודה ייתכן והשם שונה כבר שלוש או ארבע פעמים, אך אני מרגיש בבירור שאלו עליות דרגה ברמה של השם. לעתים פשוט צריך לנסות איזה שם ו\"לחיות\" איתו כמה דקות על מנת למצוא שם מוצלח יותר.
צמצום אלמנטים שאינם משרתים את מטרות 1-3. 
למי שנתקל ברעיונות אג\'יליים – אני מניח שעקרון זה הוא מובן מאליו: Eliminate Waste. עדכון המטרה בעיקרון זה היא להימנע ממרכיבים בקוד שלא משרתים את הפונקציונליות, לא מונעים קוד-כפול ולא מסבירים את התנהגות המערכת, כל מיני \"הכנות למזגן\"[ג].

מי שעובד ב (Test Driven Development (TDD נהנה באופן מובנה מעקרון 1 (\"כל הבדיקות עוברות\") ועקרון 4 (\"צמצום אלמנטים לא-חיוניים\"). זו הדרך המהירה ליצירת קוד פשוט, שגם יישאר פשוט לאורך זמן.
מכאן נותרו רק 2 פעולות לשים לב אליהן:
צמצום כפילויות קוד [ב] ו כתיבת קוד המתאר כוונה / קוד ברור. המשיכו לעשות את אלו בצורה תמידית – והמבנה של הקוד שלכם, או ה \"design\" של הקוד שלכם – יהיה פשוט. זו הדרך היעילה ביותר שאני מכיר.
זה אולי נשמע קצת פשוט מדי: אולי ציפיתם לשימוש במחשבון של Cyclomatic complexity וטכניקות של ספירת Weighted Micro Function Points (ממש כמו ספירת קלוריות). 
צר לי לאכזב אתכם: כמה אנשים טובים השקיעו שנים ביצירת מודלים מתמטיים לתיאור סיבוכיות של תוכנה – אך בפועל כמה עקרונות פשוטים הם אלו שיגרמו לקוד שלכם להיות פשוט יותר, וקל יותר לתחזוקה.

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

—-

[א] יש המשתמשים במילה Design על מנת לתאר מצב של תוכנה חיה, ולא רק את התוכניות. לדוגמה: \"Refactoring: Improving the Design of Existing Code\". אם הרעיון הזה ברור לכם – הרגישו חופשיים להשתמש במילה design ולא ב structure.

[ב] אזהרה!: יש המבלבלים בין צמצום כפילות קוד בתוך המערכת, לבין שימוש-חוזר בקוד (code re-usability) או \"צמצום כפילות קוד בעולם האנושי\". בעוד ביטול כפילות-קוד בתוך הקוד שאתם כתבתם הוא כמעט-תמיד דבר טוב, שימוש חוזר בקוד (\"חיצוני\") הוא נושא מורכב עם הרבה ייתרונות וחסרונות. אני מתכנן לעסוק בנושא זה לעומק בפוסט נפרד.

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