מתי כדאי להימנע מ 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 לכל דבר – כך שכבר לא נראה לי שניתן לייחס לו בגרסה עתידית משמעות אחרת.

מדוע אנו זקוקים ל Software Design?

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

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

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

אם אתם תוהים על \"חישובי עלות-תועלת ב Design\" – קראו את הפוסט הקודם שלי בנושא.

להלן סדרת בעיות התוכנה, ש Design יכול למתן:

בעיית ההתמצאות (Orientation)

כשאנו רוצים לבצע שינוי התנהגות בתוכנה (\"להוסיף פיצ\'ר\") – כיצד אנו יודעים היכן בקוד יש לבצע את השינוי?
ללא ארגון מתודי של התוכנה, כנראה שנאלץ:
א. להתייעץ את המתכת היחיד שמכיר את הקוד (נקווה שהוא עוד עובד בחברה).
ב. נבצע debug ונטייל את הקוד עד שנמצא את המקום.

דרכים אלו גוזלות זמן ומהוות waste שנרצה להימנע ממנו.

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

בעיית ההקשר (Context) / הגבולות הברורים (Boundaries)
תת-בעיה של בעיית ההתמצאות.

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

כאשר ההקשר ברור (למשל: \"אנו באזור ה Business Logic של ניהול ההזמנות, שלא נוגע ב DB או ב UI\") – קלות קריאת הקוד משתפרת, גם ללא שינוי הקוד עצמו. נוכל לקרוא קטע קוד באופן עצמאי, מבלי לקרוא את הקוד שמסביבו.

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

בעיית \"נגעת – שברת\" (Software Fragility)

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

האם אפשר לתכן תוכנה אחרת, כך שהסיכוי \"לגרום לשבירה\" יפחת?

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

בעיית הדומינו (Software Rigidity)

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

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

בעיית ה Development Scalability

בראשית ימי התוכנה היה executable אחד. כל קוד התוכנה היה מתקפל ליחידה אחת.
אחר כך נוסף עוד ומפתח… ועוד מפתח… עד שמגיעים למצב הבא: כאשר מפתח אחד \"מקלקל את המערכת\" – המפתחים האחרים \"תקועים\" עד שהיא תתוקן.

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

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

לבסוף, אנו רוצים שאינטגרציות יכללו מינימום \"התנגשויות\" – ומכאן יצטמצמו פעולות ה merge ברמת ה source control. בשביל ה productivity.

בעיית ה Deployment

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

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

לעדכון של מערכת גדולה יש עלות משמעותית: אפשר לעבוד ימים, אם לא שבועות בכדי לעדכן מערכת גדולה ומורכבת.
כאשר יש Persisted Data, שינוי במבני הנתונים יכול לדרוש תהליך של migration: תהליך שיכול לארוך שעות של downtime ולא-תמיד הוא נקי מתקלות.

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

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

בעיית הפריון (Productivity)

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

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

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

בעיית הסיבוכיות

זוהי מטא-בעיה שמסכמת את הבעיות שציינו:

  • בעיית ההתמצאות
  • בעיית ההקשר
  • \"נגעת-שברת\"
  • בעיית הדומינו
  • בעיית ה Development Scalability
  • בעיית ה Deployment
היא נגרמת מכמה סיבות שגרתיות:
  • גדילת המערכת
  • שינויי דרישות – הנובעים מלמידה טובה יותר של המציאות, לקוחות חדשים או פשוט מציאות משתנה של הלקוחות.
  • טיפול בריבוי תסריטים (טיפול במקרים רבים יותר)
  • בעיות קשות יותר (מקביליות, דרישות ביצועים גבוהות, דרישות זמינות וכו\')
  • טיפול ב Legacy ותאימות-לאחור 
  • ועוד

סיכום ביניים

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

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

אני לא יכול שלא להזכיר בנושא זה סוג נוסף של \"בעיה\":

השאיפה לשימוש-חוזר (Reusability)

שימוש-חוזר (Reusability) הוא אחד הדברים שהכי קל \"למכור\" בצוות / בארגון: \"למה לכתוב אותו הקוד פעמיים? בואו נכתוב פעם אחת ונשתמש בו בשני המקומות!\"

יש לי הרבה מה לומר בנושא, אך אנסה לקצר בעזרת 2 חוקים בנושא, חוקי ה 3×3 של השימוש-החוזר:

  1. כתיבת רכיב המתאים לשימוש חוזר היא פי 3 יותר קשה מכתיבת רכיב לשימוש יחיד [א].
  2. רק לאחר שרכיב בשימוש ע\"י 3 מערכות שונות – הוכחה היכולת שלו להתאים לשימוש חוזר.
אמנם יש מקרים בהם שימוש-חוזר הוא פשוט יותר, אבל לרוב מדובר במצב בו רכיב בודד צריך לטפל בכפליים תסריטים וכפליים מאפייני איכות – מאפיינים ברורים המגדילים את הסיבוכיות שלו. התסריטים ומאפייני האיכות הנוספים לא תמיד צצים בבחינה ראשונית של הנושא, במיוחד אם זו בחינה בגובה \"10,000 רגל\", שלא נכנסת לפרטים.

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

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

סיכום

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

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

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

[א] אין כמובן דרך למדוד \"פי 3 יותר קשה\", במיוחד כהכללה. הכוונה היא כמובן לומר: \"הרבה יותר קשה\".

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

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

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







שאלות על Object Oriented Desgin

לפני כשבוע נתקלתי בוויכוח הבא:

במערכת כלשהי, באזור לו נקרא \"Sub-Project 3\", מחלקה A (בג\'אווה) קראה למתודה במחלקה B, אשר דרשה כפרמטר איזה ערך. הערך יכול להיות אחד מ 3 ערכים קבועים – ועל כן המפתחים יצרו enum (נקרא לו ENUM_X). עד כאן – יפה וטוב.

מפתח אחר גילה שבדיוק אותו enum (נקרא לו \'ENUM_X) מוגדר במקום אחר בפרויקט, ודרש שמחלקות A ו B ישתמשו ב enum המקורי – כך שלא יתוחזק \"קוד כפול\".

המפתח אשר כתב את הקוד במקור טען: \"חבל לייצר reference לעוד תת פרויקט ב Build בשביל כזה דבר קטן. שיהיו שני enums זהים – לא יקרה שום דבר.\"

– \"אבל אם תשנה אחד ותשכח את השני?! – מה אז?\"

הוויכוח התלהט והגיע לראש הקבוצה (!).

מה דעתכם? במי אתם הייתם מצדדים?

כיצד לסיים את הוויכוח?

לפני שאספר לכם מה הייתה הצעתי (שנדחתה פה-אחד ע\"י 2 הצדדים, כדרך אגב) ארחיב את הדילמה:

מי שקצת בקיא ב\"תיאוריה\" של הנדסת תוכנה או Object Oriented Design (בקיצור OOD) – יכול לטעון:
\"שכפול קוד הוא אם כל רוע\". \"יש עיקרון חשוב שאומר שאין לשכפל קוד: כל שינוי קונספטואלי צריך להתרגם בדיוק לנקודה אחת בקוד בה עושים שינוי\". \"עקרון זה נקרא Don\'t Repeat Yourself Principle (בקיצור DRY) – וזהו עיקרון ידוע.\"
קל להתחבר לטיעון הזה: אותו מכירים אותו, כנראה, מקורס התכנות הראשון שלנו.

האם זהו הטיעון המנצח שיפתור את הדיון?

הממ… לא בטוח.
הנה טיעון מלומד אחר:

\"אסור למודול להיות תלוי בחלקי-ממשק שאין לו בהם שימוש\". במקרה שלנו יצרנו תלות לא רצויה בכל \"Sub-Project 7\" – כלומר בהרבה מחלקות וממשקים שאין לנו בהם שימוש. הממ… נשמע חמור!
עיקרון זה נקרא The Interface Segregation Principle.

האם ייתכן שעקרונות ה OOD סותרים זה את זה?

כמה שאלות

  • האם יכול אדם, המכיר את 2 העקרונות והוא בעל כושר שכנוע, להחליט באופן רגשי במי הוא מצדד וכל פעם לשלוף את \"הטיעון התאורטי המתאים\" בכדי להנחית \"טיעון מנצח\"? האם הוא יכול לעשות זאת מבלי להיות מודע לכך ולהאמין שהוא \"רק פועל ע\'\'פ התאוריה\"?
  • בהינתן שחוקי ה OOD סותרים לעתים אחד-את-משנהו, האם ישנם חוקים \"חזקים יותר\" שיש להעדיף?
  • נניח ונוותר על אחד החוקים או שניהם – איזה \"נזק\" יתרחש? מה ההשלכות של \"לא לציית לחוקים\"? האם המאמץ הנוסף שבציות לחוקי ה OOD – משתלם?
  • האם OOD היא מתודולוגיה מוצלחת? האם, לאחר כל השינויים בשיטות העבודה שחלו בעשור האחרון – היא עדיין יעילה או רלוונטית?
עסקתי הרבה בחיי ב Object Oriented Design: למדתי, למדתי עוד, ניסיתי, יישמתי, שאפתי ליישום \"מושלם\", הנחיתי אחרים במתודולוגיה וכו\'.
עדיין, ברגע זה, כשאני עומד ושואל את עצמי את השאלות הנ\"ל – אין לי תשובה ברורה.
במשך שנים, פעלתי ע\"פ כללי הנדסת-תוכנה שלמדתי. פעלתי? – נלחמתי בחירוף נפש, אפשר לומר.
ניסיתי להעמיק כמה שיותר ולעשות את המירב.
כיום אני יודע לומר שנצמדתי במידה רבה, לזרם בתוכנה שנקרא \"Defensive Programming\". זרם ששפת ג\'אווה ו JEE היו אולי רגע השיא שלו. הוא מתבטא ברעיונות כגון:
  • \"על המתכנת צריך להגן על התוכנה בפני המפתחים – כולל הוא עצמו\".
  • עשה כל מה שתוכל כדי להפחית סיכונים לבאגים. 
גישה זו יצרה הרבה משמעת (discipline), אך גם הובילה להמלצות כגון כתיבת מחלקה singleton בג\'אווה בתוך enum על מנת להבטיח singleton \"שפשוט אי אפשר לקלקל\" [א].
מאז, נחשפתי לזרמים אחרים, אולי כמעט הפוכים – שגם הם יכולים לעבוד יפה. הבנתי (פעם נוספת) שאין אמת אחת.

עקרונות ה OOD – למבחן!

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

חלוקה הקוד למחלקות או מודולים

  • (The Single Responsibility Principle (SRP
  • (Don\'t Repeat Yourself Principle (DRY
  • Encapsulation
  • High-Cohesion / Low-coupling Principle
  • The Common Closure / Reuse Principle

הגדרת הפשטות (abstactions) / אינטראקציה בין מחלקות

  • The Open-Closed Principle
  • The Liskov Substitution Principle
  • The Release-Reuse Equivalency Principle
  • The Stable Abstraction Principle
ניהול תלויות (מחלקות עד מודולים במערכות)
  • The Interface Segregation Principle + גרסת הקוד שלו
  • (Single Layer Of Abstraction Principle (SLAP
  • The Dependency Inversion Principle
  • The Acyclic Dependencies Principle
  • The Stable Dependencies Principle
עדכון: עקרונות אחרים של תכנון מערכת:

עדכון 2: הנה איזו רשימה דומה של בחור אחר.

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

נראה לי שאבחר כמה מהעקרונות הנ\"ל – ואתחיל לנתח אותם יותר לעומק.
הערות ומחשבות יתקבלו בשמחה.

ליאור

נ.ב: האא כן – איזה פתרון אני הצעתי לדילמה שהצגתי בתחילת הפוסט? אני הצעתי פשוט להשתמש במחרוזת (string) במקום enum וכך להימנע בכלל מתלויות. מה הייתרון של ENUM על מחרוזת פשוטה? Type Safety? ובכן… ה speller ב IDE ימצא את רוב שגיאות הכתיב. בדיקות-היחידה אמורות לתפוס את השאר…

—-

[א] בספר Effective Java 2nd Edition, פריט מס\' 3, ע\"מ 18. אפשר להתייחס לכל סדרת ספרי ה … Effective – כספרים של זרם ה \"Defensive Programming\".