לחשוב 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״?
לא. מן הסתם זה גם עניין של אופי: חוצפה/תעוזה, הרבה אכפתיות ורצון עז להשפיע.

 

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

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

קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ה': DSLs

אחד החלקים המתקדמים בשפה (באופן טיפוסי בשפות-תכנות?) הן היכולות לייצר DSL – כלומר: Domain Specific Language.
DSL היא "תת-שפה" המשמש בחלק מהמערכת לתיאור יעיל יותר של Domain מסוים. למשל: תחביר לייצור JSON, לגישה לבסיס הנתונים, לניהול חוקים עסקיים, תיאור UI, וכו'.

כאשר שפת-התכנות היא "נוקשה", היכולת להגדיר DSL היא דיי מוגבלת – וה DSL יוצא רב במלים, ועמוס לעין.
הנה דוגמה ל DSL של ספריית Camel כל גבי שפת ג'אווה – להגדרת routing של הודעות. אפשר להסתכל על DSL כ" API נוח יותר, שמושרשת בו הסמנטיקה של הדומיין":

FluentInterface – המעט שג'אווה יכולה להציע

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

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

ראינו את ה DSL המוגבל של ג'אווה,  ו DSL … גרוע (ודמיוני) של Enterprise Java. כיצד נראה DSL יותר מרשים?

יוצרי קוטלין ראו מה שפת Groovy מאפשרת בתחום ה DSL… ראו והתרשמו. הנה דוגמה:

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

מעוז סמלי של יכולות ה DSL של גרובי הוא כלי הבילד Gradle – מפתחי ג'אווה רבים (ברובם אנדרואיד?) משתמשים ב DSL של Gradle בכדי להגדיר build scripts פשוטים, ויעילים – תוך כדי שהם נהנים מבדיקת-התחביר של הקומפיילר.

אם ה API של Gradle היה מבוסס על שפת ג'אווה – לא היו מצליחים להגיע תחביר כ"כ פשוט ומינימליסטי.
לפני כשנה Grade החלה לתמוך בקוטלין כשפת ממשק First Citizen ל Gradle.
השימוש בקוטלין כשפת הממשק ל Gradle עדיין לא נפוץ, אך קהילת הקוטלין בקרוב תעקוף את קהילת הגרובי בגודלה – ודברים עשויים להשתנות

מעוז חשוב אחר של יכולות ה DSL של קוטלין היא ספריה לאנדרואיד בשם Anko – להגדרה של layouts (שלא ע"י XML).

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

הנה רשימת "יכולות ה DSL" העיקריות של קוטלין:

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

ברוכים הבאים לעולם היפה והמתעתע של הגדרת DSL 😄

Infix Functions

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

אפשר להפעיל את המתודה foo על האובייקט A בתחביר רגיל (1) – או מקוצר.
התחביר המקוצר אפשרי רק כאשר הפונקציה מוגדרת כ infix, ו modifier של infix ניתן להוסיף לפונקציה רק כאשר היא מקבלת פרמטר בודד.

מתי זה שימושי?
זה יותר שימושי על טיפוסי בסיס: String, Boolean, וכו'. הנה דוגמה מ KotlinTest:

אבל… טיפוסי הבסיס הם לא שלנו – הם שייכים לשפה או לספריות הסטנדרטיות. מה עושים?

כאן באה לידי ביטוי יכולת חשובה של השפה בשם Extension Functions.

Extension Functions

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

קוטלין הושפעה מיכולת של שפת #C בשם Extensions Methods – ויצרה יכולת דומה הנקראת Extension Functions. אפשר פשוט להרחיב מחלקה, מבחוץ – מבלי "לפתוח" אותה:

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

בואו נשלב פונקציות הרחבה עם infix extension בכדי לבנות את הבסיס לספריית הבדיקות העתידית של קהילת הקוטלין:

  1. הנה הפעלה של extension function ל String – בתחביר הרגיל.
  2. הנה הפעלה בתחביר ה infix. רצף הקריאה הוא קולח יותר.
  3. נשלב את הקריאה לפונקציית למבדה.
  4. ניתן לשרשר קריאות infix אחת על השנייה.
    1. הרחבתי את האב הקדמון Any להכיל מתודה thenAdd – שתהיה זמינה לכל אובייקט בשפה.
    2. לא כל מה שאפשר להגדיר כ "DSL" הוא באמת קריא יותר: זו דוגמה לרצף קריאה מבלבל, שכנראה רק מי שהגדיר את ה DSL – יבין…

כדי לא לבלבל, כדאי להקפיד על הכללים הבאים בעת הגדרת infix functions:

  • על שם הפונקציה להיבחר בקפידה – עבור רצף הקריאות (readability flow). לא לבלבל ולא להפתיע.
  • אם אתם מתכננים לשרשר קריאות Infix נסו שהטיפוס יישמר, ולא לעבור בין טיפוס א' (Boolean) לטיפוס ב (מחרוזת) – כמו בדוגמה (הרעה) למעלה.
  • אל תזרקו exceptions מתוך infix functions.
    • infix function נראית כמו מילה שמורה בשפה – והמשתמש לא יצפה לאפשרות של exception (כפי שהוא לא מצפה ממילה שמורה בשפת קוטלין)

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

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

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

  1. יצרנו מחלקה עם המתודה ()foo – ואז הרחבנו אותה. "מי תיקח"?
  2. בקוטלין יש כלל מאוד ברור: member תמיד קודם ל extension function.
    1. אתם אולי יכולים לראות של ()C.foo – יש warning ב IDE שאומר: הפונקציה הזו מוחבאת ע"י ה member – ולעולם לא תוכל להיקרא.
  3. עכשיו יש לנו 2 מחלקות היורשות זו מזו: B יורשת מ A. לכל אחת – הרחבה שונה לפונקציה foo.
  4. הכלל בקוטלין הוא שה resolving ל extension function הוא סטטי: ממשק האובייקט יקבע איזה פונקציית הרחבה להפעיל בפועל – ולא טיפוס האובייקט בפועל.
    1. הנה דוגמה: הפונקציה ()printFoo מצפה ל A (ממשק), אך מקבלת מופע של B (טיפוס האובייקט בפועל). מכיוון שה resolution הוא סטטי – בודקים אם ל A יש פונקציית הרחבה, ומכיוון שיש – מפעילים אותה.
  5. כאשר אין הרחבה לטיפוס / ממשק נתון – מחפשים בעץ ההיררכיה את המחלקה הראשונה שיש לה הרחבה מתאימה.

ניתן להגדיר extension functions גם על nullable types – מה שמאפשר להפעיל את הפונקציה גם כאשר הערך של האובייקט הוא null:

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

Extensions, extensions, extensions

Extension Properties

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

כלומר: לא ניתן להוסיף initializer ולאתחל את הערך של extension property.
כן ניתן להגדיר את התכונה כ var, ולהוסיף לה setter.

Extension Functions הן כלי חשוב, אבל הן גם מועדות לשימוש-יתר.

מה יותר נחמד להוסיף עוד ועוד הרחבות לאובייקטים קיימים, לייצר "DSLs", ולהשתמש ב"יכולות המתקדמות" של השפה? אבל:

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

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

fun String.toJson()

מול

fun UtilsClass.toJson(string)
  • שאלה טובה היא: האם סביר שבעתיד יוסיפו את פונקציית ההרחבה שלי למימוש הסטנדרטי? האם ההרחבה שלי באמת כללית ומשרתת יפה מופעים שונים מהמחלקה?
    • אם התשובה היא "לא" – כנראה שיש פה מימוש ספציפי שלכם, ולא כדאי להשתמש בפונקציית הרחבה.
    • דוגמה חיובית היא למשל ההרחבה kotlin-jackson. לא דימיוני ש Jackson ירחיבו את ספריית הבסיס לתמוך כמו שצריך באובייקטים של קוטלין, וההרחבה הזו היא שמימושית לכולם.
  • כאשר אנו רוצים להרחיב אובייקטים בג'אווה / ספריות של ג'אווה לתמוך יותר טוב בשפת קוטלין (למשל: nullable types, הגדרת אופרטורים) – יותר הגיוני להשתמש בפונקציות הרחבה.
  • כאשר מחלקה היא גדולה ומורכבת – נסו ליצור אבסטרקציה חדשה, ולא להעמיס עוד הרחבות על המחלקה.
  • אם המחלקה היא שלכם – נסו להוסיף לה members, ולא הרחבות.

בקיצור: פה אתם משמשים ככלי העיקרי לבחירות מושכלות ובעלות רגישות.

Lambda Extension Functions (או: Lambda with receivers)

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

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

את היכולת הזו משלימים עם היכולת להגדיר receivers לפונקציות למבדה – מה שהופך אותן בפועל ל extension (lambda) functions:

  1. נפתח את הדוגמה בהגדרת מחלקה בשם RoutingMap. בשלב הזה – זהו בעצם alias. דמיינו שיש יותר.
    1. נ.ב: אם באמת הכוונה שלנו היא רק להציע alias – הצורה האידיומטית (idiomatic) לעשות זאת בקולטין היא בעזרת המילה השמורה typealias:
    2. מה שנחמד ב typealias הוא שאפשר להשתמש בו מול מחלקות שהן final – שלא ניתן לרשת אותן, כמו String או Int.
  2. אני יכול להגדיר DSL, בעזרת הגדרת פונקציה שמקבלת כפרמטר פונקציית למבדה.
  3. הנה ההפעלה: הפונקציה route:
    1. יוצרת RoutingMap.
    2. מפעילה את הלמבדה ("צריכת התוכן" ב DSL)
    3. רושמת את ה routes במקומות השונים, או במקרה שלנו – מדפיסה את ה map.
  4. בעזרת פונקציית למבדה עם receiver – הקוד יכול להראות נקי יותר.
  5. הנה ההפעלה של הפונקציה (route(2:
    1. יוצרת RoutingMap.
    2. שימו לב להבדל: בזכות ה receiver – בעצם הלמבדה מרחיבה (משמשת כ extension function) את מחלקת ה RouteMap – ולכן אנו פשוט מפעילים את הפונקציה של המחלקה.
      כמובן שההרחבה הזו טובה רק ל scope של פונקציית ה route2 – ולא מעבר לה.
    3. רושמת routes / מדפיסה.
  6. התחביר של פונקציית הלמבדה עשוי להראות מעט מוזר –  בואו נפרש אותו:
    1. התחביר הוא בעצם : .() ->
    2. לדוגמה: String.(Int, Int) -> Int
      אנו מרחיבים את המחלקה String, בעזרת פונקציית למבדה שמקבל שני פרמטרים מסוג Int ומחזירה Int.
    3. בד"כ / בפונקציות למבדה "רגילות" – פשוט אין Receiver.
    4. בדוגמה שלנו – פשוט אין לפונקציה פרמטרים.

רוצים להקדם קצת יותר ב DSL? – הנה שתי תוספות שניתן לעשות:

  1. אפשר להחליף את השם set לשם נוח יותר: למשל: addRoute – ע"י הוספת פונקציה ל RoutingMap שרק עושה delegation ל set.
  2. אפשר אפילו להרחיב את הביטוי כולו כדי שהיה "בשליטתנו". בדוגמה הבאה (הזריזה) החלפתי את ה Map ל ArrayList של Pairs משלי כדי להגדיר את respondWith עליו.
    1. יכולתי להרחיב את Any – אבל זו פרקטיקה רעה למדי. דמיינו אלו תקלות יכולות לקרוא שמישהו מוצא ב autocomplete פונקציה בשם שנראה לו הגיוני.
    2. מצד שני, אני לא בטוח שהפתרון של MyPairs הוא מוצלח גם כן: ויתרתי על יכולות ה hash של ה HashMap. כאמור: זו לא המלצת מימוש – רק משחק ביכולות של שפת קוטלין.

הרחבה אחרונה: אני רוצה לבנות מבנה מקונן, בו יש type safety ל response.

בדוגמה הבאה שיניתי כמה שמות – לצורך קריאות הקוד.
MyPair הוא בעצם RouteEntry. עברנו את השלב להסביר שהוא מחזיק בעצם זוג משתנים.
RoutingRegistry הוא לא באמת Map – אז לא נכון להצהיר עליו ככזה.
את HttpStatus – המחלקה המקוננת המתארת את ה response, יצרתי כ data class. ב RouteEntry דרסתי את toString בעצמי.

בסופו של דבר, הגענו לקוד יותר "DSL-י". כזה שאולי היינו שמחים לעבוד איתו להגדרת routes ב Web Framework:

Operator Overloading

Operator Overloading היא היכולת להגדיר התנהגויות של Operators (+, -, ==, וכו') למחלקות שלנו. למשל:

  1. בחרתי במחלקה דיי פשוטה: data class.
  2. האופרטור + הוא בעצם המימוש של הפונקציה plus, והאופרטור * – של הפונקציה times.
  3. ההפעלה של a+b היא דיי ברורה, אבל מי החליט מהי מחרוזת כפול מחרוזת?
    1. פה מתגלה הסיכון הגדול של שימוש ב Operator overloading: בניית סמנטיקה לא-צפויה.
    2. בכלל בהגדרה של DSL, לסמנטיקה ולמינוח המדויק – יש חשיבות רבה. בדריסת אופרטורים – על אחת כמה וכמה: חשוב מאוד לבחור סימן שיתאר התנהגות צפויה, גם ליישות =! self, גם למי שלא קורא את מימוש האופרטור.

בניגוד לסקאלה, לא ניתן להגדיר בקוטלין כל סימן אפשרי כאופרטור.
אולי אפשר להבין ש אבא + אבא = סבא, אבל מה זה לעזאזל אבא £ אבא, או אבא ^_^ אבא? (כל אלה אפשריים בסקאלה).

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

Destructing Operator

אופרטור מיוחד אחד של קוטלין הוא ה destructing operator ("פירוק המבנה"?).

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

נתחיל במילה הקצת מוזרה to המשתמשים בה להגדרה inline של Maps:

הא! זה "כולה" infix function, שעושה הרחבה (extension) לכל טיפוס (?Any) שייקרא A, עם פרמטר יחיד מכל סוג (?Any) שיקרא B – ומחזירה Pair של שניהם.
השורה:

val x = null to null

היא אם כן, ביטוי לגיטימי השומר במשתנה x אובייקט Pair עם שני nulls.
פשוט!

מה משמעות התחביר (char, index) אומר?
זו פעולת destructing declaration המציבה ערכים בשורה של משתנים (v1, v2, …​, vn) באופן הבא:

v1 = .component1()
v2 = .component2()
vn = .componentn()

לדוגמה:

מאיפה מגיעות הפונקציות ()component1 ו ()component2?

זהו פשוט operator overloading שנעשה במחלקה Pair, מה שנקרא deconstructing operator.

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

Invoke

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

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

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

סיכום

צללנו ליכולות ה DSL של קוטלין. מקווה שיצאתם מזה בשלום 🙂

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

>> בחור אחד שאל אם יש בקוטלין משהו דומה ל (rand(0..n של רובי, ביטוי המחזיר מספר רנדומלי מהטווח.
>> פתרון יפה שהציעו לו התבסס על extension function למחלקת ה CustomRange של קוטלין:

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

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

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

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