2021-04-29

מי מקבל את ההחלטות הטכנולוגיות בארגון? [דיון]

 בפוסט אפשור מול ריסון: קרב האור באופל של ארכיטקטורת התוכנה? התפתח דיון: 

  • מי נכון שיקבל את ההחלטות הטכנולוגיות בארגון?
  • איך זהות מקבל ההחלטות משפיעה על היחס בין ריסון לאפשור? 
  • ב Code Review - מה מעמדו של ה Reviewer? האם הוא יכול לחייב את כותב הפיצ׳ר לשנות דברים או שהוא רק ממליץ? האם נכון לאפשר לו לחסום הכנסה של קוד ל master?

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

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


טראומות


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

"נושא כאוב נוסף שניתן להתייחס אליו זה code review - מה מעמדו של ה reviewer?..." - עוד תגובה לפוסט


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


אשתף בשתי "טראומות" שלי:

שעבדתי ב SAP עבדנו על מוצר וובי (Web) כלשהו, יום אחד הגיע ארכיטקט בכיר מאוד בארגון ודרש שנשכתב את כל המוצר משפת ג'אווה ל PL/QSL - על גבי בסיס הנתונים החדש ש SAP בנתה, HANA. הוא טען שהביצועים ישתפרו בצורה דרמטית, וזה יועיל למוצר - וגם כדרך אגב SAP תוכל להתנסות יותר בפיתוח משמעותי על גבי HANA, כמו שנאמר: Eat your own dog food.

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

ההתנגדות למהלך בקרב מובילי המוצר הייתה מקיר-לקיר, אבל הארכיטקט הבכיר מאוד (ומי שהוא ייצג) הכריעו - והדבר נעשה. אחרי שישה חודשים של שכתוב קוד ג'אווה ל PL/SQL (שפה גמישה הרבה פחות) שרשרת הכישלונות הייתה כל-כך ברורה וכואבת - שההחלטה התהפכה. לקח לנו עוד כחודשיים להפוך את ההחלטה (לסרוק שינויים ב PL/SQL - ולהכניס אותם חזרה לג'אווה), אבדנו 8 חודשי עבודה (של כ 50 מפתחים), והתשנו מאוד את הצוות ואת האמונה שלו בדרך. המוצר נכשל - אבל גם כנראה היה נכשל בלי קשר להחלטה הזו (לא היה לו מספיק ערך עסקי).


המקרה השני התרחש דווקא בחברת סאטראט-אפ בה רצינו, לראשונה, לבנות אפליקציית Desktop למשתמשים חצי-טכניים. ה VP R&D של הארגון דחף באגרסיביות כלי בשם Eclipse-RCP, פלטפורמת ה UI בה משתמשת Eclipse (ומיועדת לכתיבת IDEs או כלים דומים) - על סמך היכרות על הפלטפורמה מפרויקט שניהל בארגון קודם.

בחנו את הטכנולוגיות מול אלטרנטיבות יותר מקובלות (בזמנו): ממשק וובי, Java Swing, ו FORMS.NET. 

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

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

המקרה השני יותר מוזר / חזק בעיני: בעוד במקרה הראשון נראה שההחלטה שירתה צורך עסקי אמיתי של החברה (HANA היא מוצר דגל של SAP, שהכניס לחברה מאות מיליונים) - גם במחיר "הקרבה" של קבוצת פיתוח / מוצר זניח אחר, במקרה השני לא היה אינטרס אחר או "גדול יותר" לחברה. ההחלטה להשתמש ב Eclipse RCP הייתה פשוט לא טובה, ולא שירתה שום צורך אמיתי נראה לעין. היא התנהלה בצורה רעה - שגרמה למעורבים (ביניהם אני) לאבד אמון בהנהלה. נכון שהנזק (האבסולוטי) היה קטן יותר: כמה אנשים שאיבדו מידה של אמון במנהל, ואולי עבודה של 2-3 מפתחים לאורך כחודשיים - התבררה כבזבוז.


מה אפשר ללמוד מהמקרים הללו?

(אני מתייחס רק לשני המקרים שתיארתי - שאני מכיר אותם היטב)

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

אני אוסיף כמה תובנות נוספות, מכלל הניסיון שלי:
  • אנשים שונים מבינים את המציאות באופנים שונים, לעתים במידה שכמעט ואינה ניתנת לגישור.
    • לפעמים, התכתשויות על החלטות טכנולוגיות - נעשות באמונה שלמה, של שני הצדדים, שזה הדבר נכון לארגון. זה המקרה הטוב יותר.
    • יש מקרים, לא נדירים, בהם השיקול שמנחה אנשים הוא לא באמת טובת הארגון, אלא טובתם שלהם: חשק, עניין, תחושת ביטחון ("זו הטכנולוגיה שאני מכיר", "נוח לי יותר לעבוד עם הקבוצה הזו", או "זה יקנה לי שקט מול מישהו קולני בארגון"). הרבה פעמים אלו צרכים אישיים ולא "רשעות טהורה" - אבל הגישה הזו לרוב תוביל לתוצאה (וגם אווירה) - טובה פחות. זה המקרה הפחות טוב.
      • אני אתוודה, שאני לא פעם "דחפתי" החלטות טכנולוגיות על בסיס היכרות אישית שלי. רציתי שנעבוד עם כלים שאני בקיא בהם, ויהיה לי יותר מה לתרום, ויותר יכולת השפעה. רק במעט הפעמים - זה היה השיקול הנכון לארגון (כלומר: זה שאני אכיר / ארגיש בנוח עם הטכנולוגיה), ואני מנסה מאוד להימנע ולחזור על הטעות הזו. 
  • החלטות טכנולוגיות, על אף המאמץ להפוך אותן "למדע מדויק" - לעולם לא יהיו לגמרי כאלו. תמיד יש בהן אלמנטים של טעם אישי וניסיונות עבר סובייקטיביים, הן יוכרעו במידה על סמך יחסי-הכוחות בארגון, ובלתי-נמנע שחלק מהאנשים לא יהיו מרוצים.
    • אני לא מאמין בניסיון לרצות את כולם - ואני מאמין שהן צריכות להתקבל ולהעשות, גם כשיש חוסר שביעות-רצון. כמובן שככל שחוסר שביעות הרצון גדול יותר, ומאנשים רלוונטיים יותר - נכונות ההחלטה נחלשת.
    • חשוב, לתת משקל משמעותי למי שעומד "לחיות" עם ההחלטה, ולשלם בפועל על טעויות בהחלטה. למשל: אם בניתוח הטכני אופציה א' מקבלת ציון 90 ואופציה ב' ציון 70, אבל הצוות שאמור לממש את הקוד ולחיות איתו מאמין יותר באופציה ב' - סיכוי טוב שנכון יותר לאמץ את אופציה ב', משני טיעונים מאוד ענייניים:
      • א. סיכוי טוב שמישהו בשטח מבין משהו שאתם לא, אפילו אם הוא לא יודע להסביר את זה בצורה טובה. (אם היה מתקשר מעולה, אולי היו מקדמים אותו והוא כבר לא היה בשטח?).
      • ב. מוטיבציה היא הדלק של תעשיית התוכנה. מי שכואב את הבחירה באופציה א', יוכל בלי משים, לממש אותה בדירוג 60, בעוד את אופציה ב' שהוא מאוד מאמין בה, להביא למימוש לדירוג 80.
        זו נשמעת כמו סחטנות או כוחנות, אבל זו המציאות האנושית ככל שאני מצליח להבין אותה: כך הדברים מתגלגלים בשטח ולא חכם "לטבוע בצדקנות". אנשים טובים יעשו עבודה פחות טובה שהם לא מרגישים חלק ממנה, ועבודה יותר טובה כאשר הם כן מרגישים חלק ממנה. לכן זה צריך להיות פאקטור גדול בשיקול.
  • המציאות חזקה יותר מכל טיעון - ולכן לעתים עדיף להתכתש בשטח מאשר באולם הדיונים. כאשר זה אפשרי, יש טעם בפשוט לנסות בשטח, ולשמור אופציות לשינוי כיוון. 
    • לא פעם נופתע, שבעצם אופציה שפחות האמנו בה עובדת "טוב מספיק".
    • לא פעם, המציאות בשטח תשנה את דעת המתנגדים - ברוח טובה, ונוכל לפנות לדרך טובה יותר, כקבוצה מגובשת.
    • הכרעה בשטח היא בשום אופן לא הסתמכות על הגורל או "הרמת ידיים". כדי להצליח בה טוב חשוב:
      • לחתור בשטח למגע מוקדם וחד-משמעי, ככל האפשר, עם נקודות המחלקות.
      • תיאום ציפיות לשינוי כיוון, במידה ומדדי הצלחה מסוימים לא יוכרעו.
      • אפשור האופציות הטכנולוגיות (ארכיטקטורה), והפרויקטליות (לוחות זמנים) לשינוי הכיוון. אם יצאנו לניסיון בלי יכולת אמיתית לבצע שינוי כיוון אמיתי מאוחר יותר - יצאנו פשוט טיפשים. סליחה.
  • ניתן אולי להסיק מכאן שעדיף "להפוך את הפירמידה" ולתת למפתחים את מירב שיקול הדעת וההחלטות בנושאים טכנולוגיים - וכך גם להשיג את מירב המוטיבציה וההיכרות עם השטח. זו שאיפה יפה, אבל אני חושב שהיא לא מתרחשת מהסיבות הבאות:
    • לעתים מפתחים בשטח מבינים טוב יותר - אבל גם לפעמים לא.
    • מפתחים נוטים לגלות אהבה רבה לטכנולוגיות חדשות ומדוברות - באופן שמטה יותר מדי החלטות לכיוונים פחות טובים.
    • גם מפתחים לא חפים מאלמנט של קידום האינטרס האישי: "שימוש ב <באזז> ישפר את מעמדי מול חברי / קו"ח / בטחון עצמי".
    • למפתחים לרוב יש הבנה פחות טובה של הביזנס, אלמנט קריטי בנכונות של ההחלטה.
    • מפתחים לא פעם מפגינים חשיבה קצרת טווח / נאיבית - לגבי החלטות.
    • בקיצור: הבכירים לרוב קונים את מעמדם (וקולם העדיף) בזכות האמון בהם, שהם מסוגלים לקחת החלטות טובות יותר. כמובן שיש כשלים במינויים, ולא פעם הבכיר הוא אדם שמקבל החלטות טכנולוגיות רעות באופן סדרתי.
  • ניסיון וידע הם דבר להתבסס עליו בקבלת החלטות טכנולוגיות - אבל כיצד?
    • אני רואה משקל משמעותי למישהו שכבר היה בסיטואציה דומה. זהו ניסיון קונקרטי.
    • "שנות-ניסיון" הוא לטעמי מדד מטעה: זה חבל, אבל יש אנשים עם 10 ו 20 שנות ניסיון שהידע שלהם פחות יסודי ו/או הם מקבלים החלטות טכנולוגיות פחות טוב מאנשים אחרים עם 5 או אפילו פחות שנות ניסיון. הם עברו הרבה מערכות וחברות - אבל ספגו מזה רק מעט. מאוד הייתי רוצה לאפיין מהי קבלת החלטות טובה וכיצד רוכשים את המיומנות הזו. ניסיתי בכמה הזדמנויות - אבל זה נושא גדול.
    • ברור שהמעמדות (מונח מציק באוזן) בארגון משנים גם הם. לארכיטקט או ראש צוות יהיה משקל גדול יותר - על אף שייתכן שהם לא "יספקו את הסחורה".
      • ראשי צוותים, למשל, הם ה Backbone של הארגון. זה תפקיד קשה, שוחק, ולעתים - כפוי טובה. הארגון "חייב" משהו לאנשים הללו בתמורה, ולעתים זה מתבטא בסלחנות להחלטות פחות טובות שלהם. דינאמיקת ה"קח ותן" הזו נוגעת בכל מי שנותן ערך נוסף לארגון והארגון "חייב" לו. זו דינמיקה חיונית, כנראה - שיש לה צדדים פחות יפים, אך היא חשובה.


אפשור מול ריסון בהיבט של קבלת החלטות


מכאן בעצם התחלנו. אני לא בטוח שיש לי משהו משמעותי / מפתיע לומר כאן, אבל אומר את מה שיש לי - ליתר ביטחון.

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

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

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


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

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


חקר-מקרה: נקסט אינשורנס 

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

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


החלטות טכנולוגיות

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

* אנשי-המפתח במקרה הזה הם בעלי מעמד/סמכות אחרים (ראשי צוותים אחרים) ו/או בעלי מומחיות (מי שכתב רכיב רלוונטי, אולי איש Operations או Data Engineer שיש לו פרספקטיבה נוספת).

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

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

צוותים אחראים על מיקרו-שירותים - ולכן ניתן להם חופש בעיצוב המיקרו-שירות. לחופש הזה יש גבולות, ולרוב אני (או הארכיטקטים שאיתי) הם אלו שמגבילים אותו:
  • חשוב לנו ניהול Stack מינימלי של טכנולוגיות - בכדי לפשט את המערכת ולשמר את היכולת של מפתח מצוות X להבין ולשנות קוד של צוות Y. אנחנו נקשה/נאתגר בכל פעם שיהיה רצון להרחיב ולשנות את ה Stack הטכונולוגי, ונגדיר סדרת מבחנים - שיש לעבור בכדי להוסיף טכנולוגיה ל Stacks (יש לנו כמה Stacks שכאלו: Backend, Frontend, ו Data).
  • כנ"ל לגבי מבנים וסגנונות עבודה. יש לנו קווים מנחים / Patterns למבנה של מיקרו-שירות (שכבות, אינטרקציה, דרך לכתוב בדיקות, איך מתבצעת תקשורת בין שירותים, דפוסים לעבודה עם בסיס הנתונים) - ואני והצוות שלי נאתגר את מי שירצה לחרוג או לשנות אותם. צריך להראות ערך / צורך ברור ומעל ספק סביר - בכדי לעשות שינוי. העיקרון המנחה הוא שמערכת אחידה יותר - תהיה מובנה וצפויה יותר לשאר הארגון.
  • יש לנו תהליך של "ניסוי" בו כל אחד, בתיאום ראש הצוות, יכול לנסות טכנולוגיה חדשה / סגנון חדש באזור מוגדר. לאחר כמה חודשים של ניסיון אמור להיות שלב Go/no Go. אם קבוצת ה Reveiw השתכנה שזה שיפור ברור - מאמצים כחלק מהסטנדרט (או לעתים: סטנדרט רק למקרים ספיציפיים ומגודרים היטב.). אם צוות ה Reviewer לא השתכנע שזה מספיק טוב - הניסוי יימחק והקוד יומר לסנטדרט.
    • תהליך יפה, שלא כ"כ מתנהל בפועל. יש שני מקרים ברורים בהם נכנסו כך טכנולוגיות חדשות לרוחב החברה - וגם מספר דחיות, אבל עדיין ממתינים כ 10 ניסיויים באוויר, בלי שאנחנו עושים להם תהליך רביו. כשל אישי שלי.
  • לא מזמן הקמנו קבוצת עבודה להגדרת Guidelines לעבודה: אני מוביל את הקבוצה בעזרת נציג מכל מחלקת פיתוח. אנו מציעים ביחד טיוטה לסטנדרטים (שהוסכם שכארגון אנו רוצים בהם): כיצד להגדיר API, כיצד לעשות Code Review, כיצד להשתמש ב Exceptions. הטיוטה עוברת Community Review - סשנים בהם כל מפתח יכול להצטרף ולהציע שינויים, אבל הוא צריך לבוא עם "שיעורי בית" מוכנים: טיעונים ודוגמאות משכנעים. על השינויים מסכימים ברמת ה Community. לפעמים זה קל, לפעמים בהצבעה - ואחרת אני מכריע (מקרים נדירים שזה נדרש. נדרש כדי לא למרוח החלטות). מאותו שלב אנחנו מפרסמים את ה Guideline ועובדים לפיו.
    • עד כמה ה Guideline תואם למציאות בפועל? לפעמים יותר, ולפעמים פחות. כשיש ויכוח בין שני צדדים - הוא כן מסמך מכריע בהחלטה. כלומר: צוות שיחליט לעבוד אחרת יכול לעשות זאת "מתחת לראדאר" עד אשר יהיה ויכוח פומבי בנושא. גם אז - ננסה להבין אם הבעיה היא חוסר-הגמישות של הצוות, או בעיה ב Guideline שנקבע.
ספציפית Code Review הוא חוזה חשוב במארג היחסים והדינמיקה של החלטות טכניות בארגון. אסכם שוב את הרקע: אנו עובדים במיקרו-שירותים בהם לכל שירות אחראי צוות מוגדר. אנחנו מאפשרים ומעודדים אנשים לשנות קוד בשירותים אחרים - אבל רק בהסכמה של הצוות שאחראי על השירות. אלו עיקרי העקרונות שלפיהם אנחנו עובדים:
  • חייב להיות לפחות Reviewer אחד שמאשר את הקוד. אם מדובר בקוד של צוות אחר - חייב להיות אישור של מישהו מהצוות.
  • Reviewer שלא מרגיש שיש לו משהו משמעותי לתרום ב Review יסיר את עצמו, ויוסיף Reviewer אחר במידת הצורך.
  • ברוב המקרים, ה Reviewer רק מספק הצעות לכותב הקוד. הצעות יכולות להתקבל או להדחות. אנו מעודדים אנשים להיות רגישים אחד לשני, וגם לפרגן על קוד טוב - בכדי לבנות את האמון.
    • Code Review בהגדרה הוא לא כלי להכתיב סגנון מועדף לאנשים אחרים. ה Reviwer נותן עצות, ורצוי שיצליח לתת אותן ממקום חברי ותומך (ולא ממקום נוזף ומתנגד).
  • במידה וה Reviwer מזהה בעיה משמעותית: באג, תכנון (Design) בעייתי, או חריגה משמעותית מה Guidelines הוא מסמן את ה PR כ "Changes Required". מהשלב הזה ה PR לא יוכל להתמזג (merge) ללא הסכמה של אותו Reviewer. כלומר: אנו נותנים ל Reviewer דגל אדום להרים, שיש מאחורי סמכות - אבל הציפיה היא להשתמש בו בזהירות ובאחריות. 
    • מי שישתמש בזכות הזו במידה רבה מדי - יזכה להכוונה מחדש מהמנהל שלו.
  • יש לנו גם שורה של עצות מה לבדוק בקוד Review, ועל מה להעיר. זה בעיקר Checklist לבודק.


סיכום


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

מה הדבר המשמעותי ביותר שאפשר לעשות בכדי לקחת החלטות טכנולוגיות טובות יותר?

כלל חשוב, לדעתי, הוא לקדש את הנוסחה: Org > Group > I. 

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

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

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



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




2021-03-20

אפשור מול ריסון: קרב האור באופל של ארכיטקטורת התוכנה?

פעמים רבות בחיי השתתפתי בשיחה שהחלה בערך כך:

"אז למה שלא נשתמש ב GraphQL / בסיס נתונים משותף / נעבור מ p2p ל broadcast / נסיר שכבת הפשטה מהתוכנה? - זה יעזור לנו לכתוב תוכנה מהר יותר!".

"אבל מה עם המחירים?"

"איזה מחירים? מה יותר טוב מלכתוב קוד מהר יותר? למה לא?"


אני מניח שברוב הפעמים הייתי הצד המרסן - ורק במיעוט הפעמים בצד המאפשר.

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


הוויכוח הזה לא התחיל היום. הוא לא ויכוח בין אדם אחד לאחר, ולא ויכוח ייחודי למיקום יחיד.

זה ויכוח שמתרחש בעולם התוכנה יום וליל, מסביב לגלובס. 

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

במבט-על הוא נראה כמו ה Game of Life בו "שטחים" נכבשים ומשתחררים:


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


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

לופ שלא נגמר.


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


הנה כמה התכתשויות כאלו, שהתרחשו בקנה-מידה עולמי:

--- ביבשת "מבנה התוכנה" ---

Design Patterns נגד ... מה שהיה קודם

YAGNI מול Design Patterns

S.O.L.I.D מול YAGNI

--- ביבשת ה"תקשורת" ---

SOA מול RPC

REST מול SOA

newer RPC (Thrift/gRPC) מול REST

GraphQL מול newer RPC


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





אז מה קורה שם?


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

כשעלויות המחייה עולות וכואבות - דורשים הסרת חסמים / פיקוחי.

כשמתגלה ניצול / חוסר צדק של חזקים מול חלשים - דורשים בקרה ורגולציה.

הדיון הזה מתקיים בעולם כבר מאות שנים - ועדיין לא הגיע להכרעה.

האם זו בעיה כ"כ סבוכה? NP-Complete? אי אפשר כבר להציב מחשב שיפתור אותה?!

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

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


ה Tradeoff בעולם התוכנה, הוא דיי דומה:

  • בצד אחד, אפשור (Enablement) - החלטות שעוזרות לנו לכתוב קוד בצורה קלה/מהירה יותר. הסרת חסמים.
  • בצד שני, ריסון (Restraint) - בניית מנגנונים שיגנו עלינו בפני טעויות, ישמרו את הקוד קל לשינוי.
למשל: אם משתנה הוא גלובאלי (נגיש מכל מקום בתוכנה [א]) - קל לגשת אליו ולהשתמש בו. למה להתאמץ?

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

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

אז מצד אחד אפשרנו - זזנו מהר, מהרגע הראשון.

מצד שני - בטווח הבינוני או הארוך - אנחנו משלמים מחיר שמאט אותנו.

מה עדיף?


הפתרון

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

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

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

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

שפת ג'אווה (1995), למשל, הלכה צעד הלאה וקבעה נורמה של שַׁלְפנים (Getter/Setters) על האובייקטים: הקצנה של הלקח של "אסור לאפשר משתנים גלובאליים" - על scope מצומצם בהרבה, הרי הוא האובייקט. אינספור מפתחים בזבזו במצטבר שנות חיים בכתיבה של getter/setter גם למשתנים שלעולם הגישה אליהם לא תשתנה, ולעולם לא יעשה בהם שימוש לרעה.

אחרי כמות אדירה של סבל (או לפחות: בזבוז) שהצטברו - המגמה התהפכה. שפות רבות מאפשרות ומעודדות גם חשיפה של משתנים שלא דרך שׁלְפנים, ודרכים אלנגנטיות להוסיף שלפנים רק בעת הצורך. Records שהוצגו בג'אווה 14 - באיחור רב, הם דרך אחת לחסוך כתיבה של שלפנים עבור מבני-נתונים (במובן ה OO שלהם). 

הנה לנו עוד מחזור.

לכאורה נראה שחשוב ללמד אנשים לנתח את ה tradeoff בין אפשור וריסון - כדי שיחליטו טוב יותר.

אבל:

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

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


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

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

בכל זאת, הסיכון הוא לא לגמרי סימטרי:

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

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

נדמה לי שבחברות תוכנה, ובתרבות הישראלית בפרט - המצב הזה הופך ליותר ויותר נדיר. 


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


תסביר שוב: מה הנזק מאפשור-יתר / ריסון-יתר? אז מה עושים?


אפשור הוא הרצון להתקדם מהר ע"י הורדת חסמים. דוגמה קלאסית (ואמיתית): חשיפת נתונים מבסיס-הנתונים ישירות ב HTTP. למשל: אפליקציית הווב פונה ישירות לבסיס הנתונים ושולפת מידע (מסוים), וכך לא צריך לעבור דרך API GW ודרך ה Backend - חסכנו עבודת פיתוח ושיפרנו את הביצועים.

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

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

האם זה רעיון טוב או לא? זה תלוי בהקשר. 

יהיו פעמים שכן - שזו תהיה הדרך הפשוטה והטובה לעשות את הדברים.

במערכת אפליקטיבית בה:

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

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

דוגמאות:
  • השימוש ב private על members בפרדיגמת ה OO - הוא ריסון ברמת המיקרו. למעבד אין בעיה לגשת לכתובת הזו בזיכרון - אבל אנחנו מורים לו לא לעשות זאת (ומשלמים תקורה בביצועי התוכנה / זמן כתיבת הקוד).
  • כלים לניהול תלויות בין מודולים / מיקרו-שירותים או נהלים להגבלת ספריות ה open source שנכנסות למערכת: אין למתכנת בעיה להוריד כל ספריה ולהתשמש בה - אבל הארגון רוצה לבדוק את הספרייה מבחינת אמינות / תמיכה / כפילות / רשיונות / היבטי אבטחה - לפני שזה נעשה.
  • קונבנציות של קוד - הן ריסון. הקומפיילר יקבל סגנונות שונים ומשונים - אבל הארגון מחליט שבנקודות מסוימות הוא מקבל סגנון רק מסוים.
  • לפעמים גם בחירה של שפת-תכנות היא ריסון. שפת Go מחייבת מבנה אחיד ופשוט הרבה יותר משפה משופעת באפשרויות כמו רובי. מעבר מרובי ל Go - הוא ריסון ניכר.

דוגמה קלאסית לריסון-יתר הוא Closed Layered Architecture מרובת שכבות. בכדי להתגונן בפני בעיות שונות בכתיבת מערכת, הומצא מודל השכבות בו כל שכבה יכולה לקרוא רק לשכבות מתחתיה. בגרסה ה"סגורה" של המודל, כל שכבה יכולה לקרוא רק לקוד באותה השכבה או בקוד בשכבה אחת מתחתיה:


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

ייעצתי פעם לארגון שעבד עם מודל של 6 שכבות סגורות. המערכת לא הייתה עשירה כ"כ ב business logic ורובה ביצעה אינטגרציה בין מערכות.
מידע רב עבר בין שכבה 2 לשכבה 6 - ורוב הקוד בשכבות 3, 4, ו 5 פשוט תרגום שוב ושוב קריאות, הלוך ושוב, למבנים שקולים - אך שונים. חשיפת נתונים בין שכבה 2 לשכבה 6 דרשה כמות נכבדת של קוד? על מה?!

אני בטוח שמישהו הגיע עם כוונה טובה. הוא קרא מאמר או שמע הרצאה על מערכת שעבדה ללא סדר / שכבות - והפכה לסיוט לתחזוקה. אני מדמיין שהוא החליט לא ליפול לפח ולעשות את "הטוב ביותר". לא רק 3 שכבות - אלא 6! לא רק שכבות - אלא שכבות סגורות. Crème de la Crème!

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

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



סיכום


אז מה אפשר לקחת מכל הסיפור הזה?

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

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

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

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

אני משוכנע שאין פתרון אחד כולל / נכון-תמיד ל treadeoff שבין אפשור לבין ריסון.

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

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

דיי. מאסתי בתיאורים כאלו.
בואו נהיה מקצועיים.


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


---

[א] בשפות שלא תומכת משתנים גלובאליים ניתן לחשוב על משתנה שעל אובייקט שנגיש לכולם.


2021-03-10

תכנון ופיתוח תוכנה: מידול קונספטואלי

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

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

  • משתמשים במונח "מידול" כתהליך ההצגה של העיצוב (Design). כמטאפורה: אדריכל שבונה מודל מוקטן של הבית בכדי להציג אותו ללקוחות.
    • כאן נכנסות "שפות מידול" (UML, ERM, ADL), ומתודולוגיות מסוימות (C4, ATAM, 4+1 Views). הטכניקות הללו מעט ארכאיות לתקופתנו ואני לא מתכוון לעסוק בהן.
  • משתמשים במונח "מידול" כדי לתאר רעיון של יצירת מודל מפורט/מדויק שיחליף את הקוד / נחולל קוד ממנו. כאן המודל הוא פורמאלי, מדויק, ודורש עבודה ותחזוקה רבה. ניתן לאפיין כאן שתי גישות עיקריות:
    • מידול מבנה או נתונים - בגישות כמו Model Driver Architecture (קרי MDA), או מידול של בסיסי נתונים.
    • Semantic Modeling עבור מערכות לעיבוד שפה טבעית או Ontology modeling (אונטולוגיה - הגדרת קשרים בין מונחים) ליישומים יותר כללים של בינה מלאכותית.
    • אני לא מתכוון לעסוק בנושאים הללו בפוסט.
  • משתמשים במודל "מידול" כתהליך ביניים באיסוף דרישות, תיאור העולם / צרכים / דרישות בצורה מאורגנת ומדויקת יותר - לפני שמייצרים את העיצוב הטכני וכותבים קוד. הדרך המקובלת ביותר לתאר מודל מסוג זה היא מודל קונספטואלי (Conceptual Modeling) ועליו אני הולך לדבר בפוסט הזה.
    • על אף שראיון המודל הקונספטואלי נמצא בשימושים כבר עשורים, רבים מקשרים אותו למתודולוגית ה Domain-Driven-Design (בקיצור: DDD) - שהביאה אותו לחזית הבמה / הפכה אותו לקצת יותר מדובר ופופלארי בתחילת שנות ה 2000.



פאק! מעולם לא הגדרתי מודל קונספטואלי. האם זה אומר שאני עושה Design גרוע?!


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

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

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

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

ראו גם: Mental modelRepresentation (psychology), and Cognitive model

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

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

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


איך מגדירים מודל?

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

  • ישויות (Entities)
  • קשרים (Relations)
  • תכונות (attributes) של הישויות. אולי גם מתודות.
  • הכללה / ריבוי-צורות.
  • הבחנה בין מחלקה/קבוצה למופע (instance).
  • כלי עזר: טקסט.
  • כלי עזר: קוד.

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

הדרכים הנפוצות להגדיר מודל, שאני נתקלתי בהן, הן pseudo-UML (לא צריך לדקדק), קוד, או פשוט מסמך וורד/מצגת.

מודל אינו נכון / לא נכון, אלא יעיל / לא יעיל.

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


דוגמה למודל קונספטואלי


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

הנה, למשל, כמה אפשרויות למודלים שונים לכסף (Money), איזה הוא המוצלח לדעתכם?

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

הערה: לא פעם אנשים יתקשו לתאר את המודלים ש"יש להם בראש", אל אף שהוא עקבי ולעתים אף מפורט. אם אלו אנשי-תוכנה, לפעמים עוזר לבקש פשוט מאוד לכתוב קוד / סכמה של בסיס נתונים כדי לנסות ולשחרר את "מחסום התקשורת".

ובכן, מתוך ארבעת המודלים של Money, איזה מודל הוא המתאים / המוצלח ביותר?

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

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

המודל גם מתאר tradeoffs טכנולוגיים. במקרה שלנו: מחשב ספרתי, בו כל מספר הוא כפולה של 2, אינו יכול לייצג בדיוק מספר כמו 1.1 - סכום לגיטימי לחלוטין לכסף. יש דרכים שונות להתמודד עם האתגר, ולא "לאבד" סנטים בעקבות פעולות חשבוניות / פעולות עיגול. למשל: שימוש ב Big Decimal או הייצוג הסנטים כמספר שלם (Long).

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


תהליך יצירת המודל הקונספטואלי, מתוך מסמך אקדמי משעמם.




אז איך אפשר "לדפוק" מודל קונספטואלי?


עד כה, יכול להיות שאתם אומרים לעצמכם: "ברור!" \ "זה פשוט" \ "זה בדיוק מה שאני כבר עושה". כל זה - טוב ויפה. 

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


"האחרון להבין ולזהות" 

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

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

"מודל טוב מדי"

זה אולי לא נשמע כ"כ רע - אבל זו טעות נפוצה וכואבת. 

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

הכל נעשה מכוונה טובה, אבל חשוב לשים לב שתהליך מידול לא הופך בטעות ל Over-Engineering. הוא בקלות יכול להגיע לשם.


"מודל נאיבי"

לא פעם אפשר לסיים את עבודת המידול בשיחה קצרה בת 30 דקות: הצלחה גדולה (!), ויוצאים לדרך.

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

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

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

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


מודל אלגנטי יותר מהביזנס

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

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


תקשורת לקויה / חסרה

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

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


מודל חד-כיווני

"אלו הן הדרישות של הביזנס" - הוא משפט שגור, שמהווה smell לבעיה הזו.

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

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

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


"מודל אחד לשלוט בכולם"

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

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


כמובן ששני מודלים שונים עשויים לגרום לבלבול וטעויות. חשוב להגדיר bounded context - גבולות ברורים וחד משמעיים ואלו חלקים במערכת ובביזנס משתמשים במודל א' ואלו במודל ב'. כמו כן חשוב תרגום טוב מספיק בין המודלים - בנקודות החיבור של המערכת ב contexts השונים. יש המציעים בהשנות מקרים כאלו ליצור context map - מיפוי של כל ה contexts, הגבולות, והתרגומים בין המודלים ה"חופפים". "Good fences make good neighbours" - הם אומרים. 


מודל תוכנה השונה מדי מהמודל הקונספטואלי


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

מודל הקוד הוא המחלקות בפועל של התוכנה, החתימה של הפונקציות, והסכמה של בסיס הנתונים או ה documents הנשמרים ב Mongo או ב S3.

אפשר ליצור אותם שונים זה מזה, אפילו במידה ניכרת.

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

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

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



סיכום


אם הייתם שואלים אותי איך לעשות Design טוב יותר, או איך להיות אנשי תוכנה טובים יותר - אני בטוח שתחום המידול צריך להיות תחנה חשובה בדרך.

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

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

לא נכנסתי לטכניקות קונקרטיות / מפורטות יותר של מידול - כי אין הרבה כאלו. כלומר יש את Event Storming ויש כאלו שיחשיבו את CRC Cards כטכניקה של מידול (דיי דומה, למען האמת) - אבל סה"כ אין הרבה טכניקה מדוייקת שאני יודע לקשר לנושא.

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

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


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



2021-01-28

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

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

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

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


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




2021-01-02

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

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

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

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

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

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


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

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

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



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

ברור שלא.

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

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


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

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

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

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

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

אי אפשר לבדוק מיקרו-שירות ברצינות בלי שהוא יקרא לשירותים שהוא תלוי בהם. לכן חייבים לכתוב Mocks שידמו את המערכת / מיקרו-שירותים האחרים שהשירות שלנו תלוי בהם - בזמן שבודקים את השירות.
ה Scope המצומצם של בדיקת מיקרו-שירות בודד - רק תשפר לנו את המדדים החשובים:
יכולת התמקדות הבדיקה במקרי קצה, זמני איתור תקלה, וזמני הריצה של הבדיקה.

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


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

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

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

  • לוגיקה שהשירות מבצע - להלן "Pure Business Logic" (הכתובים כ Pure functions, כמובן)
  • לוגיקה של תקשורת עם שירותים אחרים - להלן "Integration Logic".
ההפרדה הזו קלה בעת כתיבת קוד חדש - וכמעט בלתי אפשרית על גבי קוד קיים שכתוב כך.

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

ככל אצבע, אני מחשיב שימוש ב Mocks כמופרז אם יותר מ 10% מבדיקות היחידה שלנו משתמשות ב Mocks.

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

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


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

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

הנה דוגמה טיפוסית ל Heavily mocked test, ראיתי אינספור כאלו בחיי - ואראה כנראה (אולי הפוסט יעזור?) עוד אינספור בעתיד:



מה הבעיה בבדיקה הזו?
  • היא רצה ומצליחה!
  • אם מחקתי כמה שורות קוד בפונקציה הנבדקת ()doSomething - היא נכשלת. כלומר: היא בודקת משהו.
  • השתמשתי ב mocks frameworks בצורה יעילה - וחסכתי המון קוד לו הייתי כותב את ה Mocks בעצמי.
מה עוד אפשר לבקש?!

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


בעיה: לא ברור מה בדיוק נבדק, מה הצלחת הרצה של הבדיקה - באמת אומרת.

כשאני קורא את קוד הבדיקה, גם בלי obfuscation ושמות משמעותיים - אני מבין שבוצעה פעולה, אבל אני לא יכול לדעת מה חלקה של הפונקציה ()doSomething בעבודה - ומה חלקם של ה Mocks שלה. 

הדרך היחידה שלי להבין מה החלוקה, ומה באמת ()doSomething עושה לאחר שמסירים ממנה את ה Mocks - היא להיכנס לקוד ולקרוא אותו. לפי מספר ה mocks אפשר לנחש כמה זה יהיה קל. הרבה פעמים קריאה שטחית - מפספסת חלק מהעניין.

גם כאשר אני כותב בדיקה בתצורה הזו והיא הגיונית, לאורך זמן ושינויים (refactorings במערכת) - יש סיכוי שהיא תאבד את המשמעות שלה. 
שוב ושוב ושוב נתקלתי בבדיקות מהסוג הזה שהיו קליפת שום ריקה - שלא בדקו שום דבר. זה נראה מצחיק ומגוחך שכל שאני יוצר Mock עם ערך x ואז מריץ בדיקה ששולפת את x ומראה ש x == x, אבל זה קורה גם לאנשים חכמים שמבינים קוד.

כאשר עושים refactoring במערכת - אי אפשר להבין אלו בדיקות Mock Heavy עומדות לאבד את ערכן.
כאשר הבדיקות הללו נשברות ומתקנים אותן כחלק משינוי - קשה מאוד לוודא שאנחנו משמרים את הערך שלהם. הכלל בגלל שמה שנבדק הוא משתמע ואינו גלוי. 

לכן, זו היא בעיה בתהליך / בתבנית - ולא בקוד הספציפי. 


בעיה: הבדיקה בודקת איך דברים קרו (מבנה), לא מה קרה (התנהגות).


בעצם הבדיקה בודקת שכאשר מפעילים את ()doSomething נקראות פונקציות כאלו וכאלו במערכת, עם פרמטרים מסוימים ו/או ערכים מסוימים ו/או לא נקראות פונקציות אחרות.

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

"האא! הבדיקות לא גילו את זה כי זה היה באג ב SQL" - הוא סוג התירוץ שאנו מספרים לעצמנו במקרים האלו. "אולי כדאי להוסיף גם בדיקה גם על מבנה השאילתא" (בבקשה: לא!)

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

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

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

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



בעיות נוספות

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



לגיקים שבינינו: הכוונה ל Port = "נמל". לא IP Address port :-)




סיכום


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

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

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

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

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


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