יסודות: גרסאות בתוכנה

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

גִּרְסָאוּת (Versioning) היא טכניקה שנועדה לצמצם מורכבות של "העולם" הנובעת משינויים, בלתי-תלויים, של אלמנטים שונים המשפיעים על המערכת.

כשאנחנו מתחזקים מערכת-תוכנה לאורך זמן, החלקים השונים שלה עוברים שינויים מגוונים: שינויים בקוד המערכת, בספריות צד/שלישי (היום: בעיקר Open Srouce), או שינויים בחומרה ובסביבת הריצה עליה המערכת רצה (מערכת הפעלה, Virtual Machine, ענן, וכו').

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

החוקים הלא כתובים של הגרסאות

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

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

  • Immutability – שינוי ברכיב בעל הגרסה (להלן: הרכיב), יוביל לשינוי הגרסה. זה לא תמיד נכון, אבל זו הציפיה המקובלת.
    • Git Hash (סוג של גרסה), למשל, נובע מתוך התוכן, כך שכל שינוי קוד, אפילו לא משמעותי (למשל: הוספת שורת רווח) – יתבטא בשינוי ב Hash.
    • אין דבר שמונע מכותב ספריית צד-שלישי לשנות את הקוד, מבלי לעדכן את הגרסה. זו לא הציפיה – אך זה יכול לקרות (אולי יש היום כלי שמתריעים בפני כזה מצב, אני לא מכיר).
  • Orderability – יש דרך מובחנת להבחין איזו גרסה חדשה יותר מגרסה אחרת. לרוב הגרסה ממוספרת כמספר, אז מספר גבוה יותר – גרסה מאוחרת יותר.
    • פעם היה מקובל שמספר גרסה ביטא את גודל ההתקדמות. למשל: אחרי Windows NT 3.1 הגיעה גרסאת 3.5 בכדי לבטא שזה שינוי משמעותי. כנ"ל ב MacOS.
    • כמה הפצות של לינוקס משתמשות בתאריך ההפצה בפורמט YY-MM כגרסה. אז 16.10 אכן מאוחרת יותר מ 16.04, אבל לפעמים מפתיע שזו גרסה גדולה, ולא עדכון קטנטן, שיכול להשתמע מהמספרים העשרוניים. כמו כן, אין גרסאות אמצע. אין גרסה 16.05.
    • GNU בחרו בגרסאות במספרים מאוד גדולים. למשל: 5001, 5002. לא ברור לי למה, או מדוע אני נטפל דווקא למערכות הפעלה…
    • אנשי שיווק לא תמיד מקפידים על כללי ה Orderability בגרסאות. נסו לסדר את הגרסאות השונות ע"פ הסדר הנכון: Xbox One, Xbox 360, Xbox X, Xbox S, Xbox one S, ו Xbox one X.
נשמע כמו טקסט של ד"ר סוס…

Semantic Versioning

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

  • שינויי Patch – מבטיחים תאימות לפנים (forward-compatibility) ולאחור (backward-compatibility). אתם יכולים לשנות לעבור מגרסה 1.4.1 ל 1.4.3 של ספרייה הלוך ושוב – ולצפות ששוב התנהגות לא "תשבר" לכם.
    • כמובן שגרסה מבטאת שינוי ברכיב. ההתנהגות הצפויה אמורה לא להשתנות, אבל אולי "באג" נוסף בגרסה 1.4.1 ושינה את ההתנהגות הצפויה. התיקון שלו בגרסה 1.4.3 החזיר את ההתנהגות בפועל להתנהגות הצפויה. כלומר: גרסת Patch לא תשנה את ההתנהגות הצפויה, אך היא עלולה לשנות את ההתנהגות בפועל (בעקבות באגים).
  • שינויים Minor מבטיחים תאימות לאחור, אך לא תאימות לפנים. כלומר: אם הקוד שלי עבד עם גרסה 1.2.1, הוא צפוי לעבוד גם עם גרסה 1.3.5 – אך לא להיפך. אני לא יכול לצפות שקוד שעבד עם גרסה 1.3.5 יעבוד עם גרסה 1.2.1.
    • הסיבה העיקרית לזה היא שגרסאת Minor משמשת לתוספות התנהגות, למשל: APIs חדשים.
    • זו גם הסיבה מדוע כלי ניהול-תלויות הנתקלים בקונפליקט בגרסאות minor – בוחרים את הגרסה המאוחרת יותר בצורה אוטומטית. זו הציפיה מ"חוזה" הגרסה. נדבר על זה בהמשך.
  • שינויים Major לא מבטיחים שום סוג של תאימות. במעבר מגרסה 1.3.5 לגרסה 2 כלשהי (למשל 2.0.1) – מצופה ממני לקרוא בקפדנות את ה release notes, ולבצע את כל הבדיקות הנחוצות אם ההתנהגות העדכנית מתאימה לי. גרסאות Major שמורות לשינויי התנהגות, שכל מערכת "חייה" צריכה לעשות מדי-פעם.
    • כמובן שאם אני רוצה לשמור על קהל הלקוחות שלי אנסה לצמצם שינויים כלשהם, גם בגרסאות Major – למינימום. לפעמים השינויים בגרסאת Major הם גדולים מדי, והלקוחות שלי פשוט ייעצרו מלעדכן גרסאות. למשל: שינויים שוברי-התנהגות בפייטון 3 עיכבו את הקהילה כעשור, בממוצע, באימוץ החידושים.

ל Semantic Versioning יש כמה הרחבות, שקצת יותר פתוחות לפשרנות:

  • מקף אחרי מספר הגרסה – הוא מקום לתאר Pre-Release
    • ההגדרה המסורתית היא ש Alpha הוא שלב בתוכנה שעדיין מוסיפים / חסרים פיצ'רים עיקריים, בעוד ב Beta כל הפיצ'רים שם – רק לא עובדים עד הסוף.
      • כיום ההגדרה הרבה פחות מדויקת, ולא פעם נטען שמוצרים משוחררים (GA = General Availability), כשבעצם ברמת האיכות הם מתאימים יותר לשלב בטא. בחודשים אחרי השחרור הרשמי – יתבצעו התיקונים. הגישה הזו מאוד מתיישרת עם רעיונות ה Lean Startup.
    • אין גאמה😃, אבל אחרי שלב הבטא נראה לא פעם RC – קיצור של Release Candidate. המשמעות: המוצר כמעט מוכן, ורוצים לבדוק אותו ממש לפני שחרור. לרוב יהיו כמה RCs שימוספרו: RC1, RC2, וכו'.
    • לפעמים משתמשים במספר 0 (אפס) כ Major version לתאר pre-release. למשל: 0.3.1.
    • "SNAPSHOT" כ pre-release הוא סימן מקובל בעולם ה JVM שאנחנו מעדכנים את הרכיב, בלי לעדכן את מספר הגרסה. כלומר: תמיד הגרסה תישאר 1.0.0-SNAPSHOT, למרות שהקוד השתנה. שימוש זה הוא רק בזמן פיתוח – ולא לגרסאות ששוחררו "לעולם".
  • סימן + אחרי ה pre-release הוא מקום סטנדרטי להוספת Build metadata.
    • זה יכול להיות מספר סידורי של הבילד (עולה בכל פעם שעושים Build למוצר השלם). למשל: 3601. המספר גבוה כי בונים כמה פעמים ביום.
    • זה יכול להיות ערבוב גם של מספרים ואותיות, כל מידע אינפורמטיבי שיעזור למפתחים שמדבגים, בעיקר.
    • לא פעם משתמשים ב build number כב Patch version. למשל: 2.13.3601, כאשר 3601 הוא מספר ה build.

גרסאות של ספריות

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

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

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

כאשר שינוי הגרסה הוא minor או patch – אנחנו לא צופים אי-תאימות, אבל היא יכולה לקרות, בשל באג או בגלל שכותב הספרייה לא צפה שתהיה אי-תאימות.

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

בעיה שהייתה נפוצה בעבר, הוא כאשר ספריות הותקנו במערכת ההפעלה לשימוש כל האפליקציות (aka DLL Hell or Jar Hell). אפליקציה א' השתמשה בספרייה X בגרסה 1.0.0, אבל אז הותקנה אפליקציה ב' שהתקינה את ספרייה X בגרסה 2.0.0 (דרסה את 1.0.0) – וכך גרמה לאפליקציה א' להפסיק לעבוד.

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

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

למשל ב Case 1 המערכת שלנו משתמשת, טרנזיטיבית, בשתי גרסאות שונות של Library C: גרסה 3.1.0 וגרסה 3.2.0.

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

  • Shared Libraries – כל המערכת / אפליקציה תשתמש בגרסה יחידה לכל ספרייה (להלן: ספרייה C).
    • יתרון אחד הוא Deployable קטן יותר של המערכת. במערכות גדולות יש לעתים מאות תלויות טרנזיטיביות, ולא נדיר למצוא אותה ספרייה ב 10 גרסאות שונות ויותר. לעתים ההבדל בין Shared Libraries ל Isolated Libraries יכול להגיע ל Deployable גדול פי 2-3 כאשר אנחנו משתמשים ב Isolated Libraries.
    • Deployable קטן יותר – משמע פחות זיכרון (RAM) שנדרש. פחות הכפלה של Singleton classes (למשל: או State שיושב על הגדרות המחלקות, ה Classes). אם ישנו בספרייה Cache – יהיה מאגר אחד לכל האפליקציה, ולא כמה Caches כפולים, ע"פ גרסאת הספרייה המדויקת.
    • בשפות JVM ה Class Loader יסרב לטעון שתי מחלקות עם אותו השם (נניח: מגרסאות שונות של ספרייה C). בשפות Strongly Typed אחרות, ייתכן ויהיה אפשר – אך בזמן ריצה מבני נתונים לא תואמים (עם אותו השם, אבל למשל טיפוסים שונים המגיעים מגרסאות שונות של הספרייה) – יגרמו לשגיאות בזמן ריצה, שגיאות שלעתים מאוד קשה לשחזר ולתקן.
    • גם בשפות שאינן Strongly typed צפויות בעיות, הם יצוצו מאוחר יותר – ויהיו קשות יותר למציאה. כאשר המערכת משתמשת לסירוגין פעם אחת באויבקט של גרסה 3.1.0 ולעיתים של גרסה 3.2.0 (ואין type safety לשים לב להבדל) – יכולים לצוץ באגים קשים ומבלבלים.
  • Isolated Libraries – כל ספרייה נארזת עם ספריות המשנה שלה – בגרסה שהיא ביקשה (ובדקה). אמנם ה Deployable שלנו יהיה גדול יותר, ויצרוך יותר זיכרון – אך לא נצטרף לפתור קונפליקטים של גרסאות, כמו: "באיזו גרסה של Library C עלינו להשתמש". ספרייה B תשתמש בגרסה 3.1.0 וספרייה A תשתמש בגרסה 3.2.0.
    • אמנם חסכנו התמודדות עם קונפליקטים בטווח הקצר, אבל עדיין עם אובייקטים בזיכרון, של ספרייה C בגרסאותיה השונות יעברו לסירוגין בקוד של ספריות A ו B – צפויים באגים מוזרים וקשים לאיתור במערכת.

למרות שאפשר לבחור ב Isolated Libraries, מקובל הרבה יותר לבחור ב Shared Libraries גם בשל החיסכון בזיכרון, אבל בעיקר בכדי להתמודד עם קונפליקטים בין גרסאות של ספריות מוקדם ככל האפשר ("Fail Fast") – בשלב ה Build וכמה שפחות בזמן ריצה בפרודקשיין.

על ה JVM למשל, נראה:

  • Isolated Libraries כ Fat Jar (נקרא גם Uber Jar) – כלומר אריזה של כל הספריות התלויות כ jars מקוננים בתוך jar יחיד (להלן: כל אפליקציה מספקת את כל הספריות שהיא זקוקה להן, ולא מניחה שהן מותקנות כבר במערכת ההפעלה).
  • Shared Libraries כ Shadow Jar – כאשר אנחנו אורזים לתוך jar גדול (ולכן מתבלבלים לעתים כאן עם השם Uber Jar) את כל הספריות שנדרשות – אבל עותק אחד מכל אחת. בתהליך יצירת ה Shadow Jar ייתכן וישונה ה bytecode של הספריות בכדי לתאום ל package name יחיד.

אפשר גם להשתמש בגישה מעורבת בה יש Shadow Jar של ספריות משותפות, שנבחרו ידניות להיות כאלו, הנארז בתוך Uber Jar בו כל שאר הספריות יכולות להיות בגרסאות כפולות. כלי ה Build מוודא שאין ספריות שהן גם Shared וגם כפולות. הגישה הזו פחות נפוצה לדעתי – מעולם לא נתקלתי ארגון שהשתמש בה.

נחזור לתרשים למעלה: איך פותרים את הקונפליקט?

ב Case 1 – לרוב כלי ה build יפתור את הקונפליקט אוטומטית (למשל בעולם ה JVM: מייבן וגריידל יפתרו אוטומטית, Ivy – לפי דעתי יזרוק שגיאה וידרוש התערבות ידנית). ההנחה כאן היא שחוקי הגרסאות הסמנטית נשמרים – ולכן שינוי גרסה שהוא minor, הוא backward compatible – ולכן בטוח לקחת את הגרסה המאוחרת יותר. * ההנחה הזו לרוב עובדת, אבל לא תמיד.

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

שווה לציין שמנהלי תלויות שונים עשויים להתנהג בצורה שונה. למשל:

  • במייבן הכלל המוביל הוא "Nearest first", אז ב Case 2 תבחר גרסה 3.1.0 של ספרייה C כי היא רק "קשת אחת" מהאפליקציה.
  • בגריידל הכלל המוביל הוא "Latest first" ולכן תמיד תיבחר גרסה 3.2.0 של ספרייה C (כלל יותר הגיוני ועקבי).
    • במייבן אגב, אם המרחק שווה (כמו ב Case 1), תבחר הגרסה של התלות שהופיעה ראשונה בקובץ ה POM.XML. כלומר: אם כתבנו את התלות ב Library A קודם – אז תבחר גרסה 3.2.0 של ספרייה C, ואם כתבנו קודם את התלות של Library B קודם – אז תבחר גרסה 3.1.0 של ספרייה C. כלומר: סידור קובץ ה POM.XML (למשל: לפי א"ב) – עלול לשבור לכם את האפליקציה. מאוד מבלבל.

בואו נבחן מקרה קשה מעט יותר. מה נעשה כאשר הקונפליקט בין הגרסאות הוא בין Major versions?

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

בכל מקרה, יהיו מקרים בהם הגרסה נבחרה אוטומטית (ואז הבילד נכשל בקומפיליציה/בדיקות) או שהמפתח התערב ידנית – אבל הקוד לא עובד: ספרייה B דורשת את גרסה 4.1.0 וספרייה A לא מסוגלת לעבוד עם גרסה 4.1.0. מה עושים אז? האם אנחנו חייבים להחליף את השימוש בספרייה A או ספרייה B (מקרה הקיצון 😱) לספריות חלופיות – או שיש לנו ברירה אחרת?

  • ברירה נפוצה אחת היא לעשות downgrade לספרייה B. הרי לפני העדכון האחרון שלה, היא עבדה עם גרסה 3 כלשהי, והמערכת עבדה. כלומר: נדחה את העדכון של B עד שספרייה A תתמוך בגרסה 4 של ספרייה C. לעתים זה יכול לקחת שנה ויותר – וזו בהחלט פשרה.
  • עוד ברירה נפוצה, וזו שלפעמים בה פותחים, הוא לנסות ולהכריח את ספרייה B לעבוד עם גרסה ישנה יותר של Library C. אולי 3.2.0, ואולי גרסה מאוחרת יותר 3.4.3 (האחרונה בגרסה 3, למשל).
    • קיים סיכון שהקומפילציה תצליח, אבל רק לאחר שבועות נגלה בעיות ב Production. אם יש לנו סט בדיקות מקיף, וניטור טוב של פרודקשיין – האופציה הזו הופכת ליותר רלוונטית. ניסוי וטעייה.
דוגמה לנעילת ("force") גרסה ספציפית של תלות ב Gradle. במקרה הזה נעלנו טווח בין 3.9 עד 4.0 (לא כולל).
בגרדייל יש אפילו הגדרת because כתיעוד להסביר מדוע הנעילה נעשתה.
שווה לציין ש Gradle סט יכולות רחב, ויש מספר דרכים שונות לבצע נעילות של גרסה של ספרייה.
  • אפשרות קיצונית יותר אך אפשרית הוא לעבור להשתמש בספרייה אחרת (במקום ספרייה A או ספרייה B). האפשרות הזו סבירה יותר ככל שהספרייה קטנה יותר, והתלות שלנו בה קלה יותר.

סיכום

סקרנו את הבסיס של ניהול גרסאות, בדגש על גרסאות של ספריות שהמערכת שלנו תלויה בהן. גִּרְסָאוּת רלוונטית גם לפרוטוקולים, APIs, נתונים גולמיים, ועוד.

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

לפעמים תיקון באג, יקלקל לנו את המערכת. טעות נפוצה היא לחשוב שהוספת Validations בקוד הם backward compatible changes – אבל קריאות שעד לפני השינוי עברו – יתחילו להיכשל. כלומר: Validation נוסף הוא דוגמה חמקמקה לשינוי שסמנטית אינו Backward compatible בהכרח.

לעתים השבירה בין גרסאות היא אפילו יותר חמקמקה – ותלויה בשילוב בין שתי ספריות. למשל: שתי הספריות מגדירות משתנה גלובאלי בשם זהה – שדורס אחד את השני (בעיקר בשפות דינאמיות), או אולי שומרות הגדרה במשתנה סביבה באותו השם. תוספת תמימה לחלוטין של ספרייה X שנראית לחלוטין backward compatible יכולה להתנגש עם ספרייה אחרת Y – ולגרום לקונפליקט.

שווה להזכיר לרגע את האירוע בו מתכנת JavaScript הסיר את הספרייה הפצפונת left-pad ממנהל החבילות NPM – ושבר את האינטרנט, בתור תזכורת לכמה תלויות קיימות בין ספריות שאנחנו לא מודעים אליהן. כל פעם שאני נכנס בפרויקט לתיקיית ה cache של ה package manager ורואה בכמה ספריות המערכת תלויה בפועל – אני מתפלא.

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

לינקים רלוונטיים:

לחשוב Developer eXperience (DX)

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

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

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

זה עבד! ומאז המטאפורה הזו התפרסמה כדוגמה.

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

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

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

סקר של מקינזי בקרב "מומחים"/מנהלים בכירים מה משפיע על הפריון של מפתחים

Code-level DX

אני רוצה להתחיל בשימוש במטאפורה של DX לרמת הקוד, בכדי לעזור לנו לכתוב קוד טוב יותר.
נתחיל בדוגמה קצת קשוחה… אובייקט ה java.util.Date (שהוחלף לחלוטין בג'אווה 8)

  println(new Date(2020, 5, 35)) // Result: Mon Jul 05 00:00:00 IDT 3920
  println(new Date(120, 4, 1)) // Result: Fri May 01 00:00:00 IDT 2020

כאשר אני מאתחל את אובייקט ה Date בג'אווה ל 35 במאי 2020, אני מקבל את התאריך 5 ביולי 3920.
למה?? איך?!
אובייקט ה Date הציג Interface שהוא פשוט זוועה, במיוחד בגרסאות הראשונות שלו (ה constructor בו השתמשתי הוא deprecated):

  • את השנה מצפים שאספק מ 1900 (על משקל Java Epoch 🤯), כך כ 2020 היא 120.
  • את החודש מצפים שאספק באינדקס-0, כך ש 5 הוא יוני (כי 0 הוא ינואר… לא ברור?)
  • אם יש גלישה של ימים (35 ביוני) אז לא תיזרק שגיאה, אלא יוסיפו את הימים כבר לחודש הבא (ההפך מ Fail Fast) – וכך הגענו ל 5 ביולי.

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

אם אתם רוצים לעשות DX טוב אתם צריכים לכתוב API/Interface שמפתחים ייהנו לעבוד אתו. לא… לא "יצליחו לעבוד" אתו – ייהנו לעבוד אתו!
השאיפה הזו להנאה עשויה להציב את הרף הנכון לרמת הקוד שאנו מבקשים, כפי ש"כל הפארק הוא במה" עזר לעובדים של דיסני להבין את רף החוויה שהם מצופים היו לתת למבקרים בפארקים של דיסני.

הנה דוגמה הרבה פחות קיצונית:

fun DataPoints.plus(other: DataPoints): DataPoints {
    val intersectKeys = this.keys.intersect(other.keys)
    val intersectEntries = (this.entries + other.entries).filterKeys { it in intersectKeys }
    return DataPoints(intersectEntries)
}

אני משתמש בפונקציה בשם plus, שבעצם מבצעת חיתוך של איברים – כלומר: אני מסיים עם פחות איברים.
יכול להיות שבהקשר של Data Points זה הגיוני (מי יודע?! – נניח שאני לא מכיר את הדומיין), אבל זה לא אינטואיטיבי לי כאדם ולשכל הישר שלי. האם אני כותב את הקוד כ Domain Expert או כאדם?

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

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

דוגמה אחרונה, הסתכלו על ה interface הבא:

interface NoFun {

  fun updateCustomerDataPoints(customerId: CustomerId, dataPoints: List<DataPoint>)
  
  data class DataPoint(
    val type: DataPointType, 
    val value: DataPointValue, 
    val effectiveDate: LocalDate
  )

}

במקרה הזה אנו מעדכנים את הלקוח עם DataPoints מסוימים, אבל בתסריטים מסוימים חשוב לעדכן את ה effective Date על כל ה Data Points. ברור למומחה העסקי מדוע זה נכון, אבל אני מקווה שברור לכם הקוראים – מדוע לצרכן של ה API זה לא אינטואיטיבי, וקל לשכוח את זה – שלא לומר שלעבור על רשימה של איברים שאני שולח ולשנות אותם עלול להרגיש כמו משהו לא נכון או hacky שאני כמשתמש עושה. פעולה כזו לא מתאימה ל"שימושיות גבוהה"

שינוי קטן ב API (הוספה של שדה אופציונלי של effectiveDate) יכול לשדרג את השימושיות שלו בצורה משמעותית:

fun updateCustomerDataPoints(customerId: CustomerId, dataPoints: List<DataPoint>, effectiveDate: LocalDate? = null)

המימוש של ה API יעבור על ה Data Points ויעדכן את ה effective date שלהם – זו פעולה פשוטה. אבל עצם זה שהאופציה ניתנת לי בחתימת ה API: א. עוזרת לי לזכור אותה, ב. נוסכת בי בטחון שאני עושה את הדבר הנכון – ולא איזה hack שתוצאותיו לא ברורות. נראה שחסר כאן תיעוד שמסביר כיצד ומתי בדיוק לשלוח ערך ב effective date – זה עדיין לא טריוויאלי מספיק מתוך החתימה.

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

הטיעון ש "אנחנו רוצים לתת API שכיף ונוח לעבוד אתו" (בלי קשר לרמת המומחיות של הצרכן) עשוי להכריע את הכף לטובת השימושיות הגבוהה / יישום POLA. אולי לא כולם יסכימו שה API הראשון מבלבל / מכשיל את המשתמש, אבל כולם יסכימו בוודאי שהוא לא "מפנק" ואפשר לעשות אותו נוח ו"כיפי" יותר לשימוש. זה מספיק. UX גבוה הוא קונספט מוכר ומוסכם, ואף אחד לא יצפה ש Gmail יבקש ממני להגדיר את ה HTTP Headers של ההודעה – רק בגלל שאני מומחה ואני יכול, נכון?

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

DX ברמת ארגון הפיתוח

DX הוא מטאפורה מצוינת למפתחים, שיעזור להם ליצור APIs, Intefaces, ובכלל קוד טוב וקריא יותר. השאלה "האם מי שישתמש בקוד שלי עומד ליהנות?" – תוביל למסקנה שונה (והרבה פעמים: טובה יותר) מאשר השאלה "האם הקוד הזה בסדר / תקין / מסודר?"

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

תלונות כמו:

  • "ה Build אטי"
  • "ה Security Tools והדרישה להכניס סיסמה כל פעם – פשוט מטרידים"
  • "לפתוח באג בג'ירה זה פשוט סיוט… למה זה כ"כ מסובך?"

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

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

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

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

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

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

קראתי כמה מאמרים שטענו ש DX גבוה יותר (איך מודדים?) משפיע בצורה משמעותית על הצלחת הארגון ("פי 4-5") ועל שביעות הרצון של העובדים – אבל לא מצאתי נתונים משמעותיים ששווה להציג. רק דיבורים.

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

העיקרון הראשון בשיפור ה DX למפתחים ברמת הארגון הוא להקצות בצורה בלתי-מפשרת משאבים איכותיים ומשמעותיים לנושא. אלו לא נושאים קלים.

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

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

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

כמו בארכיטקטורה של מערכת, כדי להיות יעילים הרבה פעמים שווה להשקיע בלהפוך בעיות ברמת חומרה "High" לבעיות ברמת חומרה "low" או אפילו "רק medium" ולעבור לבעיה הבאה – מאשר לנסות להעלים את הבעיה לגמרי. Build Pipeline איטי מציק אם הוא אורך 30 דקות, אבל כנראה ש 10 דקות זה בסדר. המאמץ להביא אותו ל 2 דקות – עשוי להיות אדיר, ולא משתלם.

נתקלתי בפונקציות לא טכניות של Developer Enablement – מישהו בארגון שמזמין קורסים, קונה כלים, מארגן כנסים וסקרים בקרב המפתחים בכדי לשפר את החוויה וההווי שלהם. קשה לי להאמין שזה מודל יעיל ש"יפגע" במקומות הקשים והכואבים. כנ"ל לגבי מישהו מצוות ה Operations (או "DevOps") שאחראי על העניין. בטוח שיש להם מה לתרום, אבל עדיין לא ראיתי את האדם שאינו מפתח – ומצליח להבין עד הסוף את הכאבים של המפתחים, ולהביא פתרונות עומק למה שבאמת כואב.

אני מאמין ש DX גבוה יכול להגיע רק מתוך שיתוף פעולה פרואקטיבי עם המפתחים וגם מניהול של העניין ע"י מישהו שמגיע מעולמות הפיתוח. לא Operations ולא תפעול.

סיכום

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

האם המטאפורה העדיפה (בשאיפה) – תוביל לתוצאות טובות יותר? אתם תגידו – אבל יש לי אופטימיות שייתכן וכן.

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

הצצה לפיתוח משחקי-מחשב

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

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

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

ברור!

פעם נתקלתי בהרצאה על הארכיטקטורה של משחק רשת המוני (אולי זה היה Second Life?) שלמרות שלא שיחקתי בו – הייתה מאוד מפתיעה: תארו שם ארכיטקטורה של Fat Client/Thin Server בה רוב ההחלטות של הסביבה המשותפת לכמה שחקנים מחושבות בצד הלקוח (המשחק המותקן על המחשב של השחקן) ואז הן נשלחות לשרת רק לצורך אימות (שזה לא Cheat, שאין חריגה מהכללים). באופן הזה הצליחו להעביר הרבה מעלויות החישוב לחומרה של השחקנים, ולפשט את ארכיטקטורת השרת.

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

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

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

  • Unreal Engine – ה High End למשחקי תלת-מימד עם גרפיקה מלוטשת. מודל התמחור הוא כ 5% מהכנסות החברה שמעל $1M – כלומר: ממש אחוזי מרווחי החברה.
  • Unity – אולי המנוע הפופולארי ביותר למשחקי תלת-מימד, וגם פופולארי למשחקי דו-מימד. הוא פשוט יותר מ Unreal וזול יותר (רישיון שנע בין חינם ל $200 – תלוי ברווחי החברה).
  • GameMaker Studio – מנוע פופולארי מאוד למשקי דו-מימד. מגיע עם מגוון רשיונות בין $39-$1500 למפתח.
במהלך העבודה עם הבן שלי, נתקלתי בפלטפורמה מעניינת נוספת בשם גודוט (Godot) שהיא סביבת פיתוח משחקים מבוססת קוד-פתוח וקהילה, שמתחרה ישירות ב Unity ואף צומחת ומתחילה לאיים עליה. למרות שאנחנו מצליחים להתקדם בגודוט בצורה מהירה יותר, לבן שלי עדיין חשוב להיות "מפתח Unity" – שם אנחנו משקיעים את רוב המאמץ….
תעשיית המשחקים הולכת וצומחת בהתמדה ועומדת כרגע על כ $200B בשנה – כפול מתעשיית הקולנוע.
ב"משחק סטודיו" מושקעים לעיתים אף עשרות מיליוני דולר, ועובדים עליהם צוותים שגם יכולים להגיע למאות אנשים (הרבה אנשי גרפיקה ואנימציה, הרבה פחות מתכנתים. בדקתי).
Shift שהתעשייה הזו עוברת בשנים האחרונות היא צמיחה של המובייל כפלטפורמה המרכזית / הרווחית למשחקים. פלטפרמת המובייל מספקת הזדמנויות מחודשות למשחקי דו-מימד עם גרפיקה פשוטה יותר, ולמשחקי "אינדי". למשל: AmongUs פותח ומתוחזק ע"י 3 אנשים, ו Stardew Valley פותח ועוצב ע"י אדם יחיד.
אם הצד העסקי של משחקי "אינדי" מעניין אתכם – אני ממליץ לצפות בקישורים. אלו סיפורים על סטארט-אפים עם כל ההזדמנויות לעשות כל טעות אפשרית / להיכשל (בעיקר: עסקית).

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

חווית פיתוח המשחקים – מה היא שונה מעבודה ב IntelliJ?

זו שאלה שלפני כמה חודשים יכולתי רק לנחש לגביה, אבל אני חושב שמעניין לשתף בכמה מילים.
סביבת העבודה המרכזית, בסביבות פיתוח של משחקים, היא ה Scene Editor (תלת-מימדית, במקרה של Unity) בה ניתן לערוך סצנות: להציב אובייקטים, להוסיף להם טקסטורות / אנימציות, ומאפיינים (גודל, התנהגויות). את הקוד כותבים בסקריפטים קצרים יחסית, המקושרים לאובייקטים או לאירועים עליהם – למשל: ()OnCollisionEnter – טיפול באירוע בו חפץ אחר במרחב נכנס ל"מרחב ההתנגשות" של האובייקט אחר. מודל הפיתוח הזה מאוד דומה ל Code Behind המקובל ב Forms.NET (למי שמכיר) בה אנו קודם כל "מציירים" את המסך (Form) ומציבים עליו פקדים (UI Controls) – ואז מוסיפים פיסות קוד לאירועים של הפקדים. למשל: לחיצה על כפתור.
כמו ב Forms. NET – ניתן להכין ב Drag&Drop סצינות ("מסכים") למשחק פשוט, אך ככל שהמשחק יהיה מורכב יותר, אנו נעביר יותר שליטה על יצירת / הגדרות / ומיקום האובייקטים – לקוד.
את הקוד כותבים לרוב ב #C (מסתבר שזו שפה פופולרית בקרב מנועי-משחק), או בשפת סקריפטים ייעודית. רוב הכלים תומכים ביותר משפה אחת – אבל יש תמיד את השפה שהיא ה 1st Class Citizen שכדאי לדבוק בה.
בנוסף יש קונספט מקובל של "Visual Scripting" בו ניתן לתאר התנהגויות ללא כתיבת קוד:
המוטיבציה היא להנגיש "הגדרה של לוגיקה" ללא כתיבת קוד. בפיתוח משחקים יש הרבה מיומנויות נדרשות – וכתיבת קוד היא רק אחת מהן, שאולי יותר קשה להתעמק בה. ככל שהמשחק דינמי ומורכב יותר – אני מניח שיהיה פחות ופחות שימוש בכלים ויזואליים מסוג זה.
הקטגוריה האחרונה של כלים שזמינים בסביבות הללו הם עורכי גרפיקה / טקסטורות / אנימציה / סאונד – שבהם אפשר להשקיע המון זמן ועבודה.
למי שמעדיף להשקיע את הזמן בתכנון ותכנות המשחק, יש מגוון של assets גרפיים / סאונד שניתן למצוא ברשת בחינם – אני והבן מעדיף אותם, במיוחד לאור כשרון גרפי מוגבל.
את המשחק עצמו, ניתן "לארוז"/"לקמפל" לסביבות שונות. ל Windows/Mac/Linux, מובייל (iOS/Android) ואולי גם קונסולות ו/או HTML5 (בעיקר למשחקי דו-מימד). כלומר: המנועים מספקים סביבה שהיא באמת Multi-platform אלא אם כתבתם או צירפתם קוד ספציפי-לפלטפורמה (לרוב C++/C) – מה שסביר יותר במשחקים גדולים / מורכבים.

ניהול תנועה: הבסיס

ניתן לדמיין את מנוע פיתוח המשחקים ככלי Drag & Drop מלא – בו ניתן לבנות משחקים פשוטים ללא כתיבת קוד. בפועל, לא נראה לי שניתן לכתוב משחק כלשהו – בלי לכתוב קוד. אתגר בסיסי וראשון: דמות השחקן (אם יש כזו) – צריכה לנוע, ומנוע המשחק לא מסוגל לספק פתרון מלא לבעיה הזו.
למה לא? כי יש שונות גדולה בין משחק למשחק, על אף הדמיון.
  • כיצד מיוצגת דמות השחקן במשחק? – יש מקום לבחירות שונות.
  • יש המון אפשרויות כיצד לדייק את ההתנהגות של דמות השחקן. נראה שאין "ברירת-מחדל" שתספק אפילו 20% מהמשחקים. נראה זאת מיד.
בכתיבת הקוד, אגב, נראה שיש שימוש נרחב בפרדיגמה הקלאסית של ״גזור-הדבק-פצפץ׳״: יש הרבה דוגמאות קוד להעתיק מהן – ופחות הסברי-עומק איך הדברים עובדים מאחורי-הקלעים. בפוסט אשתדל דווקא להראות מעט קוד – ויותר להסביר מה קורה מאחורי הקלעים.
נתחיל: Platformer הוא משחק של דמות שזזה בעולם, קופצת, מתכופפת, נופלת, וכו'. כמו Super Mario. בואו נתבונן כיצד מנהלים תנועה של דמות פשוטה שכזו.
בצורה הנאיבית ביותר אנו מגדירים אובייקט המייצג את הדמות ("Player") עם תמונה/אנימציה מתאימה, מציבים אותה על המסך – ומזיזים אותה בעקבות קלט של השחקן, למשל: ימינה/שמאלה.
מה קורה כאשר הדמות פוגשת בקיר? בפיתוח נאיבי (ללא מנוע של משחקי מחשב)  – הדמות תעבור דרך הקיר. אולי אפילו תמחק (ויזואלית") את הקטע שדרכו עברה.
אז מצד שני: מנוע המשחק כן מספק כלים משמעותיים – ואנחנו לא צריכים לרדת לכאלו רזולוציות.
הבסיס לתנועה "טבעית/נעימה לעין" של מנועי-המשחק הוא מנוע פיסיקלי (Physics Engine, נקרא בעבר מנוע דינמי) שכנראה מגיע עם כל מנוע-משחקים שמכבד את עצמו. למשל, המנוע של Godot שהוא הפשוט יותר להבנה (מול Unity), מגיע עם כמה טיפוסי בסיס המסייעים לחבר משחק למנוע הפיסיקלי:
  • StaticBody – מתאר אובייקט ססטי בסביבה שלא נע, אך אפשר להתנגש בו.
  • KinematicBody – גוף שנע, ויכול להתנגש באובייקטים – אך התנועה שלו מנוהלת ע"י הקוד שלנו. המנוע הפיסיקלי מספק פונקציות עזר לניהול תנועה / גילוי וטיפול בהתנגשויות – אך הלוגיקה של התנועה מנוהלת על ידנו. ידע בסיסי של מכניקה ברמת התיכון, מאוד עוזרת בכדי ליצור התנהגות הגיונית ונעימה.
  • RigidBody – גוף המנוהל לגמרי ע"י המנוע הפיסיקלי – ובדיוק רב.
    • כולל אלמנטים כגון:
      • כח-כבידה
      • התנגשות אלסטית ("קפיצה לאחור" כאשר גוף אחד מתנגש בשני, כאשר המהירות והמסה של כל גוף משפיעה על ההתנהגות)
      • מומנטים (חלקים שונים בגוף נעים באופן שונה) – מה שגורם לסיבוב / סחרור של גופים.

    • בקוד אנו לא שולטים בהתנהגות ה RigidBody – ורק יכולים להפעיל עליו כוחות, לפרקי זמן נתונים.
      • גם כאן, הבנה של מכניקה ברמת תיכון – יכולה בהחלט לעזור.
כמובן ש RigidBody נשמע המשוכלל / "הטוב" ביותר – וייצור חוויה "מגניבה", אך יש סיבות טובות לצמצם את השימוש בו:
  • חישוביות CPU/GPU – הזיזו על על המסך כמה עשרות אובייקטים כאלו – והשבתתם מחשב ממוצע.
    • התנגשויות מרובות, למשל, יכולים לגרום "לתקיעה" קצרה של המשחק, בגלל החישוביות המורכבת של אירוע שכזה. יש צורך במומחיות מסוימת בכדי לגרום למשחק לעבור אירועים כאלו ללא הפרעה.
  • התנהגות (לא) צפויה – מנוע פיסיקלי מחשב התנהגויות בדיוק ריאליסטי רב – אך הוא יצור גם תנועות שונות ובלתי-צפויות – שיובילו למצבי-קצה שלא צפיתם. זה עלול להיות מקור לבאגים בתצוגה ואף ממש במשחקיות, שקשה לצפות, וקשה לתקן.
ההמלצה המקובלת היא  להשתמש בטיפוסים הפשוטים ביותר שיספקו חוויה "מספיק טובה", ולשמור את השימוש ב RigidBody בעיקר עבור אלמנטים "מגניבים" שאתם מוכנים להשקיע בהם הרבה.
אפילו את דמות השחקן, מעדיפים לנהל בד"כ כ KinematicBody – בכדי להימנע מטיפול במקרי קצה מורכבים. ישנם מנגנונים המפשטים את השימוש ב RigidBody – למשל: נעילה שלו כך שלא יוכל להסתחרר (וכך יהיה דומה יותר ל KinematicBody – הנע כולו כ Particle).
בקיצור: Tradeoffs מובנים, שאנשי תוכנה טובה מתורגלים בהם.
בכדי לפשט את העבודה של המנוע הפיסיקלי, לכל אובייקט במשחק יהיה CollisionShape שיעטוף אותו וייצג אותו מבחינת התנגשויות. ה CollisionShape יהיה לרוב צורה פשוטה לחישוב, כגון מלבן, או אליפסה (בתלת מימד: תיבה, גליל, כדור, וכו') – מקסימום פוליגון / הרכבה של כמה פוליגונים.
במקום לחשב התנגשות של bitmap של אובייקטים שונים במשחק (קשה!), המנוע יחשב התנגשויות רק על בסיס ה CollisionShape – שהוא קירוב קל לחישוב של צורת האובייקט.

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

ניהול תנועה של שחקן

נראה שאין תחליף להסתכל על מעט קוד. הנה ההתנהגות שהקוד שלנו יאפשר:
דוגמת הקוד הבאה מנהלת תנועה בסיסית מאוד של שחקן (רובוט) שיכול לנוע ולקפץ על פלטפורמות. הרובוט הוא אובייקט מסוג KinematicBody2D, כלומר: אנו שולטים בתנועה. הסיומת 2D מציינת שזהו גוף בעולם דו-מימדי, רק X ו Y – שהוא קצת יותר פשוט מעולם תלת-מימדי. הקוד כתוב ב GDScript – שזו ואריאציה של פייטון:

  1. אנו מייצרים וקטור חדש עם ערכי אפס. כל החישוב של תנועה מבוצע בוקטורים. וקטור מייצג כיוון וגודל. במקרה הזה זה מבנה נתונים שמחזיק שני ערכים: x ו y (מספרים שלמים). בציור: x=4, y=3.
    1. היחס ביניהם מתאר את הכיוון (זווית θ).
    2. ההרכבה שלהם ("פיתגורס") תתאר את הגודל (m – מלשון magnitude).
  2. המשחק רץ ב event_loop בו מקבלים input, מטפלים ב events, מפעילים את המנוע הפיסיקלי – ומעדכנים את התמונה על המסך. כל עדכון תמונה על המסך נקרא "פריים" (frame) כאשר השאיפה היא ל 60 פריימים בשניה.
    1. ()physics_process_ הוא המעורבות של האובייקט בשלב המנוע הפיסיקלי, כאשר הפרמטר delta מבטא את הזמן שעבר מאז הטיפול הקודם של המנוע הפיסיקלי. חשוב לנרמל כל תנועה מתמשכת לקצב הפריימים ע"י הכפלה ב delta, אחרת התנועה תושפע משינוי בקצב הפריימים.
  3. אנו קולטים קלט מהמשתמש, תנועה ימינה או שמאלה – וקובעים תנועה על ציר ה X.
    1. elif הוא קיצור ל else if (בפייטון)
    2. כאשר פונים שמאלה נרצה להפוך את הצלמית של הדמות בכדי לספק מידה מינימלית של ריאליזם.
  4. הוספת כבידה: כפי שאפשר לראות את מערכת הצירים בתרשים של הוקטור, ערכי y חיוביים הם למטה, ולכן הוספה ל velocity.y מוסיפה מהירות כלפי מטה.
    1. זה חוסר דיוק מבחינת הפיסיקה, כי הכבידה היא בעצם כח שגורם לתאוצה, ולא מהירות קבועה. עבור חווית משחק בסיסית זה מספיק טוב – וזה יותר פשוט מאשר הדמייה של תאוצה.
  5. כאן אנו נעזרים במנוע הפיסיקלי לבצע את התנועה: אנו מספקים את מהירות הדמות (וקטור) ואיזה כיוון במסך מייצג למעלה (למה לא שמו default?) – והמנוע ינהל את הזזת הדמות בפריים הנוכחי + ניהול ההתנגשויות. אם השחק מתנגש בקיר – הוא ייעצר.
    1. אם אנו רוצים חווית התנגשות עשירה יותר ("קפיצה לאחור" או השפעה על העצמים האחרים) עלינו להשתמש בפונקציות "נמוכות" יותר ברמת ההפשטה שלהן – ויש כאלו.
  6. אם השחקן לחץ על "Jump" (ממופה למקש הרווח), והדמות נמצאת על הרצפה (הפשטה שניתנת לנו מהמנוע הפיסיקילי, מכיוון שסיפקנו לו מה הוא "למעלה" עבורנו) – נרצה להזניק את הדמות למעלה בקפיצה.
    1. מכיוון שזו אינה תנועה מתמשכת, אלא חד פעמית – אנחנו לא מנרמלים אותה לקצב הפריימים (delta) .
כל משחק צריך להתאים לעצמו את חווית התנועה ולדייק אותה למקרים שמתמודדים איתם, ולכן זה לא משהו שהצליחו לספק "out of the box". ב Unity יש הפשטה מעט שונה של CharacterController, המבוסס RigidBody – ולמרות עבודה טובה שעשו לצמצם סיבוך של RigidBody – הקוד מורכב יותר, ולכן בחרתי דווקא לספק דוגמה מ Godot.
יש עוד הרבה מה לשפר בתנועה של הדמות שלנו. למשל: התנועה שלה פתאומית: עוברת מעמידה למהירות מירבית באופן מיידי. חוויה יותר "נעימה" היא כאשר יש אלמנט של האצה ו/או האטה לפני עצירה. אלו דברים שקשה לשים לב מה Animated Gif למעלה, אבל מרגישים בהחלט תוך כדי שימוש במשחק. הנה שיפור הקוד שיוסיף את החוויה של האצה / חיכוך עד עצירה (האטה):
הפונקציה lerp (קיצור של linear interpolation) עושה ממוצע בין הפרמטר הראשון לפרמטר השני – ביחס בין 0.0-1.0 (הפרמטר השלישי, במקרה שלנו 0.2 או 0.1 בהתאמה), והיא עוזרת להגדיר הדרגתיות. למשל: האצה / האטה או גדילה / סיבוב הדרגתיים. (נתקלתי גם בשימוש מעניין שלה בדיגיטציה של צורות גיאומטריות – אבל זה חומר לפוסט נפרד).
בשורה הראשונה (האצה) אנחנו עושים lerp ("ממוצע") בין המהירות הנוכחית (שגדלה בכל פריים) למהירות המירבית (direction * speed) הרצויה שלנו. שינוי כיוון ידרוש האצה מחדש.
בשורה השנייה (האטה) אנו עושים lerp ("ממוצע") בין המהירות הנוכחית (שקטנה בכל פריים) ל 0.
הנה סרטון שמציג שמדגים בצורה מעט יותר ברורה את החוויה של האצה / האטה בדמות שחקן:

משהו לסיום: תנועה אוטונומית "חכמה" של דמות "אוייב"

אוקי. גרמנו לדמות השחקן שלנו לזוז. השחקן האנושי (והנבון) מכווין אותה.
כיצד אנחנו מניעים אויבים? מספר רב שלהם – ובלי יישות תבונית שמכווינה אותם?
זה נשמע מסובך ומתוחכם, אך מסתבר שזה עשוי להיות מתוחכם ודיי פשוט.
רעיון בסיסי ומקובל הוא לייצר מסביב לכל אויב מעגל יוזמה שאם השחקן חודר אליו (לפעמים תוך כדי תנועת "פטרול" של האויב) – האויב משנה התנהגות וחותר למגע ישיר (ועוין?) עם דמות השחקן.
השאלה המעניינת היא כיצד האויב לא נתקל במכשולים, ו"יוצא טמבל"?
כאן נתקלתי בדפוס מעניין שחשבתי לשתף, שנקרא "Context-Based Steering". אדלג על הקוד ואסתפק בהסבר עקרוני.
התוצאה היא תנועה אוטונומית ו"חכמה" של אויבים שלא מתנגשים במכשולים – הישג יפה לקוד פשוט יחסית:
וואהו! אני ממשיך ומתרשם מהדוגמה הזו, כל פעם מחדש.
הרעיון הוא כזה:
  • משתמשים בוקטור / מערך של התנועות האפשריות של כל אוייב. במקרה שלנו 8 כיוונים (אפשר לדייק ולהגדיר גם יותר).
  • מחזיקים שני וקטורים כאלו:
    • וקטור רצונות (interest) – באיזה כיוון יש שחקן, אליו האויב רוצה להגיע.
    • וקטור חששות (danger) – באיזה כיוון יש מכשול, לשם לא כדאי לנוע.
  • הדרך לעדכן את המערך הוא בעזרת כלי שימושי הנקרא Ray-Casting: שולחים קרן דמיונית מנקודה מסוימת (מרכז האויב) בכיוון מסוים (אחד מ 8 הכיוונים שלנו) – ומקבלים באיזה אוביקט נתקלנו ראשון (אם בכלל). Ray-Casting היא פונקציה שימושית למגוון של סיטואציות.
    • בוקטור הרצונות – אנו ממלאים את המרחקים לשחקן (בכיוונים שאכן נתקלנו בשחקן): מרחק קטן = רצון גדול, ולהיפך.
    • בוקטור החששות – אנו ממלאים את המרחקים למכשול (בכיוונים שאכן יש מכשול): מרחק קטן = חשש גדול, ולהיפך.
זה מביא אותנו למצב כזה:
  • עכשיו אנחנו מחסרים את וקטור הסכנות מוקטור הרצונות (וריאציה אחרת: מבטלים את כל הרצונות שבכיוון שלהם יש איזשהו חשש) – ואז מניעים את האויב בכיוון המועדף עליו ביותר.
    • הרצונות חלשים מדי? האויב מפסיק לרדוף וחוזר למצב סטטי או "שיטוט".
    • השחקן נמצא מאחורי מכשול? אין שום כיוון טוב לנוע אליו? נוע בכיוון אקראי, עד שהמצב ישתנה.
  • עצם ההרצה של אלגוריתם פשוט שכזה – מספיקה במקרים רבים לספק התנהגות "אינטלגנטית" של אויבים, שמספקת אתגר והנאה לשחקנים. אותי זה מרשים!
  • כמובן שבכל משחק צריך "לשחק" עם הפרמטרים, ולעתים להוסיף קצת tweaks עד שמגיעים להתנהגות רצויה וטובה – אבל זה הבסיס.
באנימציה שלמעלה ("מכוניות מרוץ אוטונומיות במסלול"), וקטור הרצונות נקבע לפי המשך המסלול: איזה כיוון לוקח את המכונית "הלאה" להמשך המסלול. את המסלול מגדירים בעזרת כלי / מבנה שנקרא PathFollow – ממנו אפשר לבקש בכל נקודה מה כיוון ההמשך.
מקווה שההסבר מספיק ברור. אפשר למצוא הסבר מלא ומפורט על  Context-based steering בלינק הבא.

סיכום

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

TCR, או: מתי TDD כבר יוכרז כמיושן?

TCR היא טכניקת תכנות חדשה מבית היוצר של Kent Beck.

 

אם זה לא הרשים אתכם עד כה, אני אזכיר שקנט בק הוא הבחור שהיה שותף להמצאה של ה Wiki והבאת CRC Cards (שיטת Design) לקהל הרחב. הוא גם המציא את (Extreme Programming (XP וגרם למהפכה בעולם התוכנה. ביחד עם Erich Gamma (מה GOF) הוא כתב את JUnit ועוד גרסאות בשפות שונות. לבסוף הוא המציא את TDD – שגם השאירה חותם גדול על התעשייה.

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

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

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

 

רקע

קנט עבד בשנים האחרונות ב… פייסבוק. הוא חווה את הגדילה של הארגון מ 700 לכ 5,000 מהנדסים בתוך כמה שנים – מה שגרם לו לעסוק הרבה בתרבות הנדסית (Engineering Culture) בחברה – וכיצד לשמר ולהצמיח אותה.

למי שלא מבין את הקושי, כלל האצבע של התכנותיקה גורס שגדילה של ארגון הנדסי (נניח: גדול מ 25 עובדים), בקצב של מעל 50% בשנה – הוא הרסני:

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

המצב של גדילה כ"כ גדולה (יותר מ 50% בשנה) נקרא Hyper-growth או Blitzscaling, ואין לו פתרון פשוט.

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

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

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

התנגשות (merge או גרוע הרבה יותר: build שבור) – הוא נזק סביבתי חמור. כאשר ה build שבור:
א. מספר לא חסום של מפתחים – חסום לעבוד לפרק זמן.
ב. ייתכן וחלק מהמפתחים שכפלו ל branch שלהם את התקלה ואז:
     ב1 – היא עלולה לגזול להם זמן נוסף באיתור ותיקון התקלה בעצמם.
     ב 2 – היא יכולה, בצורה מסוימת, לחזור ל master מחדש כחלק מתהליך ה merge חזרה.

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

 

תרשים של קנט בק על ההבדלים בין TDD ל TCR

ל TDD – המתודולוגיה הקודמת של קנט, שעוזרת לשמור על קוד בשליטה וניתן-לשינוי בכל רגע, יש כשל עמוק כאשר מדברים על Scale: מתודולוגית ה TDD לא מחייבת אנשים ל commits קטנים. מתכנת יכול לכתוב 20 בדיקות, ואז לעבוד עוד יום או יומיים או שלושה עד שכולם יהיו ירוקים. הוא כבר לא יעמוד בקריטריון הרשמי של Continuous Integration [א] – אבל לכאורה הוא עדיין עושה TDD.

קנט בא לתקן את מתודולגיית ה TDD לעבוד ב Hyperscale וקרא לה Test && Commit:
הרזולוציה המחויבת מעכשיו היא של בדיקה בודדת:

  • בצע שינוי קטן של קוד.
  • כתוב בדיקה שמאמתת את השינוי.
  • הרץ את הבדיקה, ואם היא עברה – עשה merge ל master.

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

השם של השיטה מתוך הסקריפט הבא, שאמור לרוץ בסוף כל cycle קצר שכזה:

test && git commit -am working
 

הרעיון של "Living Integration" במקום "Continuous Integration" (שאולי עכשיו נכון יותר לקרוא לו Daily integration – הוא אטי הרבה יותר מדי מ"הנדרש") – נקרא ע"י קנט והצוות שלו בשם "Limbo", המצב האינסופי של העולם הבא.

יש קונצנזוס רחב בתעשייה, כבר כשני-עשורים לפחות, שעבודה ב batches קטנים ככל האפשר – היא דרך ליעל תהליכי עבודה ולהימנע מהתברברויות יקרות. הלימבו אמור להיות המצב התאורטי הגבוה ביותר של batches הקטנים ביותר והאינטגרציה הרציפה ביותר האפשרית. רעיונות שעלו בצוות היו "בואו נכתוב סקריפט שעושה push ל master כל פעם שקובץ נשמר" או "בואו נאסור על commit לכלול יותר מ 25 שורות שהשתנו" (שני רעיונות קיצוניים למדי, ותאורטיים – לפחות כיום).

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

קנט לא אהב את הרעיון, ולכן לפי דבריו "הוא היה חייב ללכת לנסות אותו!". בעצם נראה שקנט אהב את הרעיון – ומשם המתודולוגיה הפכה ל: (Test && (Commit || Revert – מכאן שמה הוא TCR.

מעכשיו המתודולוגיה עובדת כך:

  1. בצע שינוי קטן של קוד.
  2. כתוב בדיקה שמאמתת את השינוי.
  3. הבדיקות עברו? – בצע commit (ושלח את הקוד ל master)
    הבדיקות נכשלו? בצע revert –hard לקוד – והתחל מהתחלה.
 
כל הכנסה של קוד צריכה לעבור את כל הבדיקות, יש ב TCR איזו הנחה סמויה שהמערכת מכוסה לגמרי בבדיקות-יחידה, שיכולות לרוץ בדקות בודדות – לכל היותר.
 
 
הרעיון של TCR הוא די קיצוני: למחוק את הקוד אם לא הצלחנו בפעם הראשונה?
 
קנט הוא בית-חרושת לרעיונות קיצוניים, והדרך הזו באמת מחייבת לעבוד ב batches קטנים: מספיק שכמה פעמים ביום יהיה עליכם לעשות revert לקוד שכתבתם – בוודאי משם תתחילו לעבוד ב batches יותר ויותר קטנים, בכדי לאבד פחות עבודה. ההיגיון ברור, אני מניח.
סקריפט שימושי להריץ בזמן שאתם עובדים על ה master branch. כולם צריכים לעבוד על ה master branch – כמובן!

שימוש מעשי

אם אתם מפתחי תוכנה מנוסים, אתם עשויים לחשוב בתחילה שהרעיון של TCR, ומחיקת קוד פעם אחר פעם – הוא רעיון אבסורדי.
 
בספר "Extreme Programming Explained" קנט בק הסביר שב XP הוא עורך בעצם ניסוי. הוא לוקח כל מיני פרקטיקות טובות שכרגע נמצאות על רמה 2 או 3 – לרמה 10, לראות מה קורה.
 
"אם Code Review הוא טוב – עשו Code Review כל הזמן!" – כך נוצרה הפרקטיקה של Pair Programming, למשל – כחלק מ XP.
 
XP לא ממש הצליחה כשיטה, אבל הפרקטיקות שלה, שהחזירו אותן לרמה 3 או 4 – עבדו מצוין, והן מוטמעות היום עמוק בתעשייה. 
 
גם TDD – לא ממש התממש לפי החזון. אומרים שרוב מי שעושה TDD – לא באמת מבין את המתודולוגיה, וגם מי שמבין אותה – לא עושה אותה "By the Book".
 
אז מה? TDD גרם למהפכה, וגרם לאנשים לחשוב מחדש על בדיקות (גם אם חלקם הגיעו למסקנה מעט שונה מזו שקנט התכוון).
 
ברור שהרעיונות של TCR הם אבסורדים ולא מעשיים! זהו קנט בק, וטוב שיש מישהו כמוהו – שמעלה אותם.
 
מה שנשאר למצוא הוא כמה צעדים אחורה יש לקחת מנקודת הקיצון – על מנת להפוך את הפרקטיקה ליעילה.
כנראה ואין תשובה אחת, ולאנשים שונים / ארגונים שונים / זמנים שונים – יתאימו התאמות שונות של הפרקטיקה.
בואו נראה כמה יישומים מעשיים שמדוברים עכשיו:
 
 
לימוד / אימון
 

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

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

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

LDR הוא סוג של TCR. בחיי.
 

"30 דקות ל commit"

את הגרסה הזו אני ניסיתי בחודשים האחרונים:

  1. התחל לעבוד על קוד. 
    1. חתור למצב בו יש מצב יציב שעובר commit מהר ככלל האפשר: 5 עד 10 דקות.
    2. עדיף מאוד עם בדיקות – אבל לא כל commit שלי באמת נבדק בטסט חדש.
  2. אם הגעת ל 30 דקות בלי commit – בצע revert לקוד והתחל מחדש.

בגרסה הזו מתמקדים ב commit מהיר כל 5-10 דקות (מקסימום: 30 דקות) – ולא ב merge מהיר ל master, מה שעוזר "לדלג" מעל קושי גדול ביישום ריאלי של TCR. ה merge עדיין מכיל מספר commits, ועובר תהליך qualification ארוך ומורכב (יחסית לדרישות של TCR).

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

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

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

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

It’s 2019. Blocking, asynchronous code reviews are the dominant method for collaboratively developing software. If I draw one certain conclusion from my experiments with TCR and Limbo, it’s that blocking, asynchronous code reviews are not the only effective workflow for collaboration. While I don’t know if TCR and/or Limbo will be the future, I think that something different  is coming — Kent Beck

TCR /w Async Merge

תהליכי ה Integration בחברות לא מאפשרים כיום לעשות merge כל כמה דקות. מי שאינו קנט בק חי בדרך כלל עם Suite של בדיקות מסוגים שונים שלוקח להם לפחות 10 דקות לרוץ, ולעתים גם כחצי שעה או שעה (זה הקצה הקצת פחות טוב…).
ויש גם את ה Code Review שיכול להסתיים רק אחרי כמה שעות. גם היסטוריה ב Git תהיה בלאגן אם כל מפתח יכניס commit כל 10 דקות. המערכות שלנו לא מוכנות כרגע באמת ל commit ל master כל 10 דקות, ע"י מאסות של מפתחים.

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

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

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

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

הנה קישור למאמר של עוד כמה וריאציות שבחור בשם תומאס מציע: BTCR, Relaxed TCR וכאלו.
אני חושב שהמסקנה מהניסיונות הללו היא ש:

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

אני אישית חושב ש "30 דקות ל commit" היא האופציה המעשית ביותר. לי היא עובדת כשינוי גישה מרענן.  

סיכום

  • מה אתם חושבים על TCR?
  • האם ניסיתם? האם היו תוצאות טובות?

תמיד אפשר לחכות כמה שנים עד שגרסה בוגרת תגיע יותר למיינסטרים…

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

לינקים רלוונטיים

[א] ע"פ ההגדרה: כל מפתח עושה merge ל trunk/master כל יום.

4 חטאים של פיתוח תוכנה בן-זמננו [דעה]

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

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

  • בספרי תכנות, למשל, הפרקים המאוחרים (לעתים מאוגדים כ "Advanced Topics") הם לרוב נושאים פחות שימושיים ביום-יום. ביום פקודה – אפשר להשלים את הידע נקודתית. זו גישה מאוד הגיונית.
  • היתרון מלהכיר עוד Frameworks הולך ופחות ככל שאתם מכירים יותר Frameworks. אם אני מכיר כבר שני Web Frameworks בצורה טובה – איזה יתרון באמת יהיה לי מללמוד את השלישי?!
  • אפשר ללמוד אינספור כלים וספריות, אבל אם לא עובדים בהם בצורה משמעותית – זה יידע שלא יעשה בו שימוש ו/או יישכח במהרה.
  • ישנם נושאים קצת יותר רחוקים מכתיבת הקוד עצמו, אך מספיק שונים בכדי לספק לנו "קרקע בתולית ללמידה". הרבה פעמים יש להשקיע בהם השקעה משמעותית מאוד – עד שנראה תמורה אמיתית ביום-יום שלנו. למשל: Machine Learning, מערכות מבוזרות, או Big Data. לא בטוח שזה אפיק משתלם עבור רוב אנשי-התוכנה.
 

—-

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

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

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

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

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

אז מה יש לנו?

TDD – איך כותבים בדיקות מוצלחות, ואיך כותבים קוד שקל לכתוב לו בדיקות מוצלחות.

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

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

אני משתמש במונח ״TDD״, על מנת להדגיש שמדובר בכתיבת קוד – ולא בבדיקות ידניות או ״כלי אוטומציה״ (נוסח Selenium). הטכניקה של כתיבת בדיקות לפני הקוד – היא לא העיקר מבחינתי. העניין הוא לכתוב בדיקות טובות.

מה הן בדיקות טובות? יותר קל לי להציג אלמנטים נפוצים של בדיקות לא טובות:
  • אנשים מגזימים בכמות הבדיקות המערכתיות (System Test, Integration Tests) על חשבון בדיקות ממוקדות. גלידה ולא פירמידה. זה כ״כ נדוש ושחוק, אך עדיין – טעות שממשיכה ונעשית.
  • אנשים לא מבודדים Pure Business Logic משאר הקוד לצורך unit tests – ואז באמת קשה מאוד מאוד לכתוב ולקבל את היתרונות של unit tests.
    • נתקלתי הרבה פעמים במצב הזה, וזו בעיה שיחסית קל לתקן, ברגע ש״נופל האסימון״ – ומשנים גישה.
  • אנשים כותבים יותר מדי קוד בדיקות – מה שמאט את העבודה שלהם, ומקשה על ביצוע שינויים במערכת:
    • גם בדיקות שהן overfit למימוש ארעי (situational), כלומר תנאי שמתקיים – אך אינו חשוב ועקרוני לפעולת המערכת / הביזנס. בהמשך הוא ישתנה, לא תהיה בעיה עסקית – אך הבדיקות יפלו וידרשו עדכון.
    • גם בדיקות שהן יתירות (בודקים את אותו הדבר שוב ושוב באופנים שונים). כל שינוי של מימוש קוד – ידרוש סדרה של שינויים בקוד הבדיקות – מה שיגרום לנו לרצות לעשות פחות שינויים.
      • יעילות מגיעה מניהול סיכונים נכון: האומץ לצמצם את כמות הבדיקות (לא לכתוב בדיקות מסוימות), מתוך הבנה אלו בדיקות חשובות ומשמעותיות יותר.
  • אולי הכי גרוע: בדיקות ועוד בדיקות שנכתבות (ומתוחזקות!) מבלי שהן מגרדות את פני השטח. הן בקלות יכולות לעבור – בזמן שמשהו עקרוני ולא טוב קורה ב flow. בקיצור: בדיקות לא-משמעותיות.
    • זכרו: אם הבדיקות שלכם אף פעם לא נשברות – זו לא סיבה לגאווה. זה אומר שבזבזתם את הזמן בכתיבת בדיקות שלא אומרות כלום.
  • אנשים שהתייאשו מבדיקות ו״למדו״ (אבוי!!) – שבדיקות הן נושא overrated ומיותר.
    • זהו מצב שמאוד קשה להתאושש ממנו.
לסיכום: Programmer Testing הוא כלי כ"כ חשוב, כל כך יישומי, ושמביא תוצאות כ"כ טובות ומהירות (כשעושים אותו נכון), ועדיין – רק אחוז נמוך בצורה מבהילה של אנשי-תוכנה באמת שולט בפרקטיקה הזו.

 

המחשבה שאם אתם מכירים את הספרייה שאיתה עושים בדיקות (JUnit5, Jasmine, RSpec, Sinon), אזי אתם יודעים "לכתוב בדיקות טובות" – היא שגויה מיסודה. חפשו את העלות/תועלת: כמה השקעה יש בכתיבת ותחזוקת הבדיקות – מול כמה זמן הן מקצרות בתהליכי ה debug ותיקון שגיאות.
 

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

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

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

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

Refactoring אקטיביסטי

הנה עוד דבר שעשוי להישמע מעליב: ״אני לא יודע לעשות Refactoring טוב מספיק? יש לך מושג כמה פעמים כבר עשיתי Refactoring? מה הבעיה בלעשות Refactoring?״

אני רוצה להדגיש מימד שקצת שנשכח לגבי Refactoring: האקטיביזם.

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

המון! מן הסתם.

ברגע הזה, שקורה לרובנו על בסיס יומי, עומדות בפנינו שתי ברירות:

  • להמשיך הלאה.
    • יש לי מנהל עם ״סטופר״ שיבוא בתלונות אם לא אדלוור פיצ'רים מהר.
    • יותר גרוע: שינוי בקוד הוא סיכון ליצירת באג. אם יש באגים שנוצרו על ידי – אני יוצא לא-טוב. (איפה ה Unit tests שלכם עכשיו, הא?)
  • לבצע Refactoring ולהחזיר את הקוד לרמה אופטימלית X (כלומר: רמה טובה, אבל לא מושלמת. שלמות היא בזבוז).
    • Refactoring אינו צריך, ועדיף שלא יהיה "פרויקט ענק". הוא יכול להיות בכל commit שלישי.
    • אם שומרים על רמת קוד טובה לאורך הזמן – יהיה הרבה פחות צורך בפרויקטי ענק.
אז מה אתם בוחרים?
 
לרוע המזל רוב אנשי-התוכנה בוחרים ב"דרך הבטוחה". זה עובד? – אז לא לגעת! 
אשמה גדולה היא בקרב המנהלים, שהם קצרי רוח לזמני ביצוע של פיצ'רים והופעות של באגים – אך יש להם מספיק סבלנות ל"פרויקטי תחזוקה", ופיצ׳רים פשוטים / חקירות באגים שמתארכות לאורך ימים.
 
החטא של המפתחים הוא שהם תורמים את חלקם למעגל המזיק הזה – ובעצם פוגעים באינטרסים שלהם.
התמריץ לשמר את הקוד ברמה "אופטימלית X"  הוא לא רק עניין של ערכים "אני בעד קוד יפה", חלילה!
יש פה אינטרסים מעשיים:
  • קוד שמתוחזק ברמה גבוהה – יאפשר להוסיף פ'יצרים נוספים בצורה קלה ומהירה יותר, ועם פחות תקלות.
    לאורך הזמן השאלה צריכה להיות: האם אתם רוצים לעבוד בקוד מתוחזק, או בקוד "עולם שלישי"? באיזו סביבה אתם חושבים שתתפתחו, אישית – בצורה טובה יותר?
  • כאשר בוחשים בקוד – רמת העומק וההבנה האישית שלנו את הקוד, ומה שקורה בו – צומחת בקצב אחר לגמרי.
    • אני לא יכול להדגיש זאת מספיק: מי ששובר את הקוד (או לפחות מסתכן בשבירה) – הוא מי שמבין אותו לעומק. "לשבת על הברזלים" זו אסטרטגיה נוחה לטווח הקצר – אך נחותה לטווח הארוך.
עוד אלמנט חשוב הוא היכולת שלנו לראות כיצד הקוד יכול ללבוש צורות שונות – והיכולת להעביר את הקוד בקלות מצורה לצורה: אולי functional? אולי לולאת foreach? אולי break ואולי exceptions.
  • בעיות שונות בקוד יפתרו באלגנטיות רבה יותר בעזרת צורות שונות של קוד. 
    • כאשר אנשים מקובעים לתבנית אחידה / סגנון אחיד – זה מגביל!
    • לאנשים רבים, גם כאלו עם ניסיון של שנים – חסרה ממש הגמישות הזו: קשה להם לקרוא ולהבין קוד בסגנון שונה, והם חוזרים וכותבים קוד בצורה "שהם רגילה אליה" – גם במקרים בהם היא מסורבלת וקשה לקריאה.
  • Refactoring תכוף – הוא דרך נהדרת ללמוד ולהתנסות בצורות קוד שנות. זה האינטרס האישי שלכם!
  • שווה לציין גם טכניקה בשם "Coding Dojo״ שאמורה לפתח מנעד רחב יותר של סגנונות קוד:
    • מתכנסים כמה אנשים בחדר ופותרים תרגיל קוד קטן כאשר מחליפים ידיים כל פרק זמן נתון (מעבירים את המקלדת מאדם לאדם). עוד נוהג הוא לעשות את אותו התרגיל – מספר פעמים. בכל פעם – תהיה תוצאה קצת אחרת.
    • נ.ב. אני נוטה להאמין שיעילות המפגש שכזה היא ביחס ישיר לאדם המוכשר ביותר בסגנונות קוד שנוכח בו.
בקיצור: האם יש ״סגנון שנוח לכם איתו״ ורק בו אתם כותבים?
האם אתם עסוקים ב"לשמור על הקונבנציות" יותר מאשר לחשוב ולהבין איזו צורה של קוד היא הטובה ביותר לבעיה?
האם אתם "חכמים מספיק לא להתעסק עם קוד שעובד". מעתיקים מדוגמאות קוד אחרות במערכת – מבלי לצלול לעומק מדוע הקוד הזה עובד?

 

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

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

Design to Go

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

  • עבודה ב Small Batches.
  • יצירה של Short and Effective feedback cycles.
  • בחינת אלטרנטיבות – מתוך ההבנה שיש יותר מדרך משמעותית אחת לסדר קוד ו/או לפתור בעיה.
    • ״קו האפס״ הוא פתרון יחיד שעובד – ומשם משפרים. 
    • אחרת: אנחנו עובדים על עצמנו. לא משנה כמה מלבנים ציירנו בדרך.
  • כאשר ״תקיעה״ בתהליך הדזיין, מובילה אותנו לוותר עליו – במקום לעבור ל Exploration.
כבר דיברתי הרבה בנושא בהרצאה שלי ברברסים. אין טעם לחזור.
 
מקור: Integrating and Applying Science" (pg. 136) – http://ian.umces.edu/press/publications/259/

 

 

Modeling

 

Modeling היא לא פרקטיקה נפוצה בקורות החיים של אנשים. 

המונח ״Medling״ כנראה מובן לרוב האנשים, אך הוא לא נתפס כנושא בעל חשיבות עליונה – שכדאי לפתח.
  • הזכרנו כבר שנקודת מפתח ב Design היא בחינת אלטרנטיבות.
  • החלק המשמעותי באלטרנטיבות הללו הוא לא ״אובייקט גדול״ מול ״שניים קטנים״ – אלא מידול שונה של האובייקטים העסקיים. למשל: ״תשלום, הכולל ניסיונות תשלום״, מול ״נסיונות תשלום הכוללים תוצאה״.
  • ״גמישות לדרישות עתידיות״, ו״פשטות״ הם BuzzWords – אך הם גם סופר-משמעותיים במבחן התוצאה. 
    • מודל פשוט וטבעי לביזנס – יכול בהחלט להכפיל את התפוקה של הצוות.
      מיומנות מעטות בעולם התוכנה עשויות לגרום להשפעה (impact) רבה שכזו!
  • היכולת לעשות modeling נכון נובעת מניסיון תמידי להבין את הביזנס והצרכים + הפעלה של חשיבה ביקורתית.
    • קל לצייר בראש מודל – שלא ממש מתאים לביזנס. חשוב לתקשר ולאמת אותו.
    • לא תמיד אנשי הביזנס יתחברו למודל – וחשוב גם לנסות ולאתגר אותם.
  • Modeling לא נעשה רק בשלב דזיין – אלא גם כתהליך refacotring, שינויים קטנים כל הזמן.
  • Modeling מתקשר בד״כ למידול של אובייקטים עסקיים, אך הפרקטיקה נכונה גם למודל טכני (מודל concurrency, מודל eventual consistency, מודל sevurity):
    • שואלים ומאתגרים כל הזמן מה הם הצרכים
    • מנסים למצוא מודל פשוט ואלגנטי ככל האפשר, פשוט ע״י איטרציות של שיפורים במודל.
    • מתקשרים את המודל – כך שיהיה רעיון משותף, ולא ״מחשבה פרטית״.
  • איך לומדים לעשות מודלינג?
    • ע״י צפיה בדוגמאות של מודלים. למשל הספר PEAA (דוגמה יפה: המודל של Money), או הספר המעולה (אך קצת מיושן): Analysis Patterns – של אותו המחבר.
    • ע״י בניית מודלים והפקת לקחים אישיים.

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

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

 

ה Killer instinct

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

כשאתם נתקלים ב״Killer Instinct״ – קשה להתעלם ממנו.

  • זה השילוב של הבנת ביזנס, חשיבה ביקורתית, קריאה נכונה של הארגון (מי מדבר שטויות, מי יודע), קצת חוצפה (ממי להתעלם, למי להתייחס), והאומץ לבצע שינויים / לכתוב קוד שיש לו חסרונות ברורים – לצד יתרונות ברורים, כמובן.
    • תמיד נתקלתי ב Killer Instinct בצמידות לנטייה לגעת בקוד ולשנות אותו. חוסר פחד, ביחד עם סקרנות ורצון לחולל שינויים.
      אני נוטה להאמין שיש פה גם אלמנט של סיבתיות: הניסיונות הקטנים לשפר את הקוד -> יוצרים הבנה עמוקה של הקוד (עם הזמן). הבנה עמוקה של הקוד -> מאפשרת את ה״מאסה הקריטית״ של העומק – הדרושה בכדי לבצע שינויים משמעותיים במערכת בזמן קצר.
  • ״להתעסק״ עם הקוד בלי שיש בדיקות טובות – לא כדאי. הקוד ישבר, וההתעסקות תהפוך לעניין כואב ומתסכל.
  • הבנה עמוקה של הקוד, ללא הבנה של הביזנס – עשויה לפספס את האימפקט:
    אתם עושים שינוי עמוק במערכת, שאף אחד לא האמין שאפשרי – אבל אז גם לאף אחד לא אכפת הוא נעשה, כי הוא פשוט לא מעניין.
  • בכדי ליצור אימפקט, חשוב להבין את הביזנס. הבנה של הביזנס נבנית מתוך Modeling.
  • בכדי שהתוצר יהיה טוב יותר, ומשמעותי גם לאורך זמן – חשוב גם לדעת איך לעשות Effective Design.
 
האם זה מספיק? האם זה המתכון הסודי והמובטח לשחרור ה״Killer Instinct״?
לא. מן הסתם זה גם עניין של אופי: חוצפה/תעוזה, הרבה אכפתיות ורצון עז להשפיע.

 

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

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