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

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

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

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

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

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

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

מתי כדאי להימנע מ 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 בעיקר בבדיקות יחידה ו/או כמעט לא בבדיקות מערכת.
חבל לראות את כל הקוד שנכתב בלי הפרדה של לוגיקה – מה שיהפוך את בדיקות היחידה לקשות יותר וליעילות פחות.
במקרים כאלו, אני אפילו מבין לליבם של "המתנגדים לבדיקות-יחידה": כשכותבים אותן כך – אולי באמת עדיף בלי. לפחות אין את האשליה שהמערכת בדוקה ובטוח / קל לבצע בה שינויים.
שיהיה בהצלחה!

להכיר את גריידל (Gradle)

Gradle (מקור השם: עריסה, Cradle – כאשר האות הראשונה שונתה ל G עבור Groovy) הוא כלי Build נפוץ בעולם הג'אווה, וכלי ברירת-המחדל לפיתוח באנדרואיד.

Gradle הוא כלי Build מודרני, מלא פיצ'רים ואופטימיזציות – שנמצא דור או שניים לפנים מהחלופה העיקרית שלו: Maven. גריידל יודע לבצע Caching ו Incremental Build ברמות הפשטה שונות – ומסוגל לשפר מאוד את זמני ה Build של פרויקטים גדולים.
 
מצד שני, Gradle מורכב יותר לשימוש, וכנראה לפרויקטים פשוטים – מייבן הוא כנראה עדיין כלי עדיף, בשל הפשטות שלו.
 
בניגוד למייבן המשתמש ב XML להגדיר את ה buildscript – בגריידל משתמשים ב DSL מבוסס שפת-תכנות. גרדייל תומכת ב Groovy-DSL ו Kotlin-DSL, כאשר בשנתיים האחרונות היא עושה מעבר לכיוון קוטלין, מכיוון שזו הולכת והופכת לנפוצה יותר ויותר (בניגוד ל Groovy – שקצת "נתקעה" במקום).
 
לו היו נותנים שם היום לפרויקט, נראה ש Gradle היה נקרא Kradle.


לגריידל אין מערכת Repositories משלה, והיא משתמשת בזו של מייבן, של ivy, או פשוט בתיקיה של מערכת הקבצים המקומית. זה יתרון גדול, מכיוון שכך אין תחרות בין "Maven Repositories" ל "Gradle Repositories".
 
 

מה גריידל יכולה להציע מול מייבן?

 
הבדלי ביצועים בין מייבן, גריידל, ובאזל. מקור: האתר של גריידל.

 

הדבר הראשון, וכנראה החשוב ביותר הוא ביצועים:

  • בהרצה ראשונה, גריידל ומייבן יהיו דיי דומים. אולי גריידל תרוץ טיפה יותר מהר.
  • בהרצה השנייה והלאה, ייכנסו לשימוש מנגנונים ייחודיים ל Gradle:
    • Incremental Build – גריידל עוקבת אחר מערכת הקבצים ומקמפלת רק קבצים שהשתנו, או קבצים שתלויים בהם.
    • Config Cache ו Build Cache – שימוש חוזר בתוצרים (ברמות שונות) מהפעולות בילד קודמות.
      • תאורטית גם מייבן עושה זאת – אך בצורה משמעותית פחות יעילה.
    • Gradle Daemon – גרדייל מחזיקה תהליך (process) של מערכת ההפעלה שממשיך לחיות ברקע, להחזיק caches מיידיים בזכרון ולהנות מאופטימיזציות של Java JIT compilation.
גריידל מספקת יותר גמישות:

  • למייבן יש מחזור-חיים קבוע וקשיח, validate > compile > test > package > install, שלא ניתן לסטות ממנו, אלא ב"תרגילים". בגרדייל יש עץ של Tasks (כמו Compile או Test) שניתן לקנפג וקל הרבה יותר להתאים את שלבי הריצה לצרכים שלכם.
    • למשל: בכדי לדלג על שלב הבדיקות או הקומפילציה ב Maven יש לעשות "תרגילים" שלא תמיד מצליחים. בגרדייל דילוג על שלבים זו אופציה מובנית.
    • קל להגדיר Custom Tasks בגריידל – וזו גמישות רבת-עוצמה. המקבילה במייבן היא למצוא Plugin שמתאים בדיוק לצרכים, או לכתוב Plugin בעצמכם – שזה הרבה יותר מסובך
  • גריידל מספקת ניהול תלויות עשיר וחכם הרבה יותר ממייבן – מה שמשמעותי בפרויקטים גדולים ו/או מורכבים.
    • למשל: במייבן תלות במודול יגרור קבלת כל התלויות של אותה ספריה (propograted dependencies, שלעתים נקרא בטעות transitive dependencies). הדבר לא מאפשר לעקוב ולנהל במדויק תלויות של מודולים.
      בגריידל אפשר לבחור או בגישה זהה למייבן, או בגישה מדויקת ונשלטת יותר.
  • גריידל מממשת את העיקרון של Conventions over Configuration, ואין צורך באמת להשתמש בכל הכלים המתקדמים יותר שלה. קושי נפוץ של newcomers הוא להבחין אלו מנגנונים הם פשוטים ו straightforward, ואלו מנגנונים הם רבי-עוצמה, אך דורשים גם הבנה מעמיקה יותר.
גריידל היא מודרנית יותר:
  • גריידל זוכה לתמיכה טובה יותר, ועדכונים תכופים יותר. למשל: כבר שנה וחצי כשיש בעיה בהרצה של JUnit 5 ב Failsafe של מייבן בצורה מקבילית – אך בגריידל הכל עובד כשורה מהגרסה הראשונה של Junit 5.
  • לגריידל יש כלים תומכים משמעותיים וטובים יותר:
    • Build Scan עוזר לנתח ולהבין בדיוק מה קרה ב Build. האם הגדרה מסוימת שרצינו אכן פעלה? לא צריך לנחש מתוך הלוגים – אפשר לבדוק בצורה ישירה.
      • גריידל מספקת Build Scan רזה יותר גם למייבן – בעיקר בכדי לתמוך במעבר ולוודא שה Build בגריידל מכסה את כל מה שקרה במייבן.
    • Continuous Build – גרדייל יכול להאזין למערכת הקבצים ולקמפל את הקוד תוך כדי שאתם עובדים.
    • Gradle Profiler – שעוזר למצוא bottlenecks בבילד ולשפר אותם. ניתן לגשת לתוצאות שלו בתוך ה Build Scan.
    • Gradle Build Debugger – בגלל שה build Script הוא שפת תכנות – אפשר ממש לדבג את תהליך הבילד כדי לנתח תקלות קשות-להבנה.
    • Gradle Enterprise – הם סט של יכולות נוספות שחברת Gradle מספקת בתשלום – כמו Central build Cache (שימוש משותף בתוצרים: מפתח אחד קימפל מודול, כל השאר יכולים להשתמש בתוצר), או כלי Diagnostic ו Analytics משופרים לבילד ולהרצת הבדיקות.
  • נטפליקס, היא תורמת גדולה של Gradle Plugins. רבים מהם היו הבסיס לפיצ'רים שהיום הם סטנדרטיים בגריידל.
  • Gradle Wrapper (שמירת תאימות מדויקת של גרסת גריידל בין מפתחים) הוא כלי פופולארי בגריידל, אך שווה לציין שהוא קיים גם במייבן (אם כי שם הוא פחות נפוץ בשימוש).
התייחסות מהירה לבאזל
ב 2015 שחררה גוגל, את כלי ה Build שלה כפרויקט קוד פתוח, ולאחרונה הוא הפך ל GA.
  • Bazel ו Gradle דומות זו לזו, יותר משהן דומות ל Maven או Ant.
  • גריידל מספקת גמישות רבה יותר, באזל היא יותר מובנה.
  • באזל בנויה יותר מסביב לניהול מרכזי (למשל: ניהול גרסאות של תלויות) בעוד גריידל לא לוקחת הנחות כאלו, ותומכת טוב יותר במציאות מבוזרת של שליטה.
  • לגרדייל יש קהילה גדולה יותר, ובגרות גדולה יותר – במיוחד בעולם ה JVM. סיכוי טוב שקהילות כמו Go ו ++C יזכו לתמיכה טובה יותר בבאזל.
  • היכולת להשתמש ב Cache מרכזי ומשותף היא יכולת ליבה של Bazel (שעליה מתבססים במידה הביצועים הטובים של הספריה) בעוד ב Gradle זו יכולת פרמיום בתשלום (Gradle Enterprise).
  • בסה"כ באופן דיי עקבי (מסקירת מאמרים ומבחנים) Gradle מהירה יותר מ Bazel – גם כאשר משתמשים ב Build Cache מרכזי.
    • מבחני ביצועים לא מעטים משווים את שתי התצורות בגרסה החינמית שלהן – ואז ל Bazel יש יתרון משמעותי על יכולת שב Gradle היא בתשלום (Central Build Cache).
  • הנה מאמר מ 2015 של יוצרי גריידל שמנתחים את באזל.
אין ספק שבאזל מכניסה תחרות וגורמת לגריידל לעבוד קשה יותר. בעוד ההשוואה בין גריידל למייבן היא מאוד מחמיאה, ההשוואה של גריידל מול באזל נראית תחרותית הרבה יותר.
הפוסט שלי נכתב על גריידל, שנראה שתמשיך להיות בטווח הנראה לעין הסביבה המתקדמת והמקובלת בעולם ה JVM. בעולמות הללו לגריידל יש עדיין יתרון משמעותי – וקהילות לא מתחלפות כ"כ מהר. חשוב לציין שבאזל היא רענון חשוב בעולם כלי ה Build, ובוודאי היא מעניינת יותר בעולמות ה Go וה ++C כבר עכשיו.

תכל'ס

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

Maven

Gradle

Pom.xml,
Where:

  Properties section
  Modules section     

  DependencyManagement section
  Most other sections

build.gradle.kts
Mapping to:
  gradle.properties file
  settings.gradle.kts file
  dependencies.kts file (a convention)

  java-conventions.gradle.kts (file per language being compiled)

pom.xml file (what happens when we build)

build script (that comprised of multiple files)

Plugin

Task

–  (there is one fixed build flow)

Plugin (describes build flow)

Goal

Lifecycle Task

Module

(sub) project

Dependency scope

Dependency configuration

Profile

Custom Task or Custom Properties.gradle file (depends what the profile does).

“Install” (to maven repo)

“publication” (to whatever repo)

"package”

“assemble”

בואו נראה מבנה של קובץ בילד מינימלי (שם הקובץ הוא: build.gradle.kts. הסיומת kts מציינת שזה kotlin script, ולא Groovy – שמגיע ללא סיומת):
  1. אנו מייבאים את הפלאגין של Java.
    1. בגריידל, Plugin מביא איתו שורה של Tasks ו Lifecycle Tasks (ועוד הגדרות רבות) – ובעצם קובע את השלבים השונים ב build והסדר ביניהם. במייבן ניסו לבנות תהליך אחד גנרי לכל שפות-התכנות, למרות שלשפות שונות (JavaScript, ++C) – יש צרכים שונים. בגריידל לרוב יהיה לנו בפרויקט Plugin אחד שיגדיר את ליבת התהליך ויספק גם את ה Tasks הנדרשים (Compile, Test, Jar, וכו' – המתאימים לשפה / תהליך)
      בנוסף אולי נרצה להשתמש ב Plugins נוספים כמו: למשל War, PMD, או Reports – ולהרחיב את התהליך.
  2. ה default scope שלנו ברמה הגבוהה ביותר היא אובייקט הפרויקט, וכך אנו קובעים properties מסוימים על הפרויקט.
  3. כאן אנו קובעים באלו Repositories לחפש את ה dependencies שלנו. כמו במייבן, אפשר ומומלץ לקבוע כמה repositories בכדי שיהיה גיבוי (לא עשינו את זה בדוגמה) – והחיפוש יעשה ע"פ הסדר בו נרשמו.
    1. במקום לספק את ה URL ל jCenter, אנו יכולים להשתמש בפונקציה שמסופקת ע"י גריידל ומחזירה אובייקט Repository עם ה URL הנכון (קיימים כאלו ל repositories נפוצים). פחות מקום לטעויות.
  4. כאן אנו נכנסים ל scope של ה Java Plugin (שהגדרנו בתחילת הקובץ) – ומוסיפים לו הגדרות.
    1. כברירת מחדל, גריידל תשתמש ב build בגרסת הג'אווה שמריצה אותה. זה יכול ליצור חוסר עקביות אם במחשבים שונים מריצים את הבילד בגרסאות ג'אווה שונות.
    2. בכדי "ליישר" את תהליך הבילד בצורה מדויקת יותר, אנו יכולים להגדיר לגריידל באזו גרסת ג'אווה להשתמש בתהליך הבילד. ה toolchain עוזר בקלות רבה לקנפג ולטפל בהגדרה של הגרסה שבחרנו. אם לא מצליחים למצוא מקומית את הגרסה המתאימה – ה toolchain יוריד לצורך ה build עותק מקומי של גרסת הגא'ווה שנבחרה.
    3. גם אם אתם מתכנתים בג'אווה 8 (ממגבלות מוכרות), הרצה של תהליך הבילד בגרסה מתקדמת יותר – יכולה להאיץ אותו מעט.
  5. כאן אנו נכנסים ל scope של ה tasks בפרויקט. אמנם אנו רוצים להתייחס ל Tasks שהגיעו מה Java Plugin, אך מרגע שהוספנו את ה Java Plugin והוא רשם את ה Tasks שלו – הם כבר לא משויכים אליו, אלא פשוט רשמים בפרויקט.
    1. ספציפית יש כאן כתיבה מקוצרת בשורה אחת להכנס גם ל scope של ה test plugin.
    2. ה Test Plugin תומך גם ב Junit וגם ב TestNG – ועלינו לציין לו במי להשתמש. הכל עטוף בפונקציות פשוטות ובטוחות לשימוש.
    3. בניגוד ל Maven בו יש שני Plugins: גם Surefire (לבדיקות יחידה) ו Failsafe (לבדיקות אינטגרציה) – בגריידל ה Java Test Tasks מספק את שני הצרכים, וניתן פשוט לקבוע בהגדרות אם לעצור בכשלון ראשון – או לא. אם אחנו רוצים להגדיר שלב של "בדיקות יחידה איטיות" – קל לעשות זאת ע"י שימוש חוזר ב Test Task.
  6. אנו צריכים להגדיר את התלויות לגרסה הספציפית של Junit שבה אנו רוצים להשתמש. לא פינקו אותנו ב wrapper לקונפיגורציה הזו. כמו במייבן – הגדרה של dependency בקובץ הראשי תשפיע על כל תתי-הפרויקטים (במייבן: מודולים) שלו.
    1. ההגדרה הזו מקבילה במייבן לתחביר הבא:

      אני מניח שלא היה לכם קשה לעשות את המיפוי הבסיסי.

    2. מהן הפונקציות הללו,()testImplenentation ו ()testRuntimeOnly?
      נסביר עליהן בהמשך.

The Java Plugin

כפי שציינתי, מרכיב חשוב בגריידל הוא ה Plugin הליבה בו אנחנו משתמשים. הוא מגדיר הרבה דברים חשובים:
  • Tasks – בהם נוכל להשתמש.
  • LifeCycle Tasks (במייבן: "Goals") – אליהם Plugins שונים יוכלו להתחבר, ולחבר Tasks שונים.
  • מבנה התיקיות של קוד המקור (במקרה זה ה Java Plugin שומר על הקונבנציה המורכת לנו ממייבן)
  • Dependency Management (כללים)  – כללים כיצד תלויות מנוהלות. מכיוון שלסביבות פיתוח שונות (Go, ++C, JVM) יש צרכים שונים – נתנו בגריידל ל Plugin להגדיר את הכללים המדויקים לניהול התלויות.
    גריידל מספקת תשתית/כלים עליהם כל Plugin יכול לבנות את המודל שהוא זקוק לו.
כשאנחנו לומדים לעבוד עם Gradle, חלק מהכללים נובעים מה Plugin הליבה בו אנחנו משתמשים – ולכן חשוב ללמוד ולהבין אותו היטב. במקרה שלנו – זה ה Java Plugin, וחשוב ללמוד אותו.

ל Java Plugin יש כמה הרחבות סטנדרטיות:

  • Java Library Plugin – עבור build script של ספריה המופצת לקהל רחב כספריה.
  • Java Application Plugin – עבור build script של אפליקציה. מוסיף Tasks כגון start או install. הוא מתאים הן לכתיבת שרת או אפליקציה שולחנית.
  • Java Platform Plugin – לכתיבה של סט ספריות קשורות (למשל: Junit 5 מורכב מכמה תתי-פרויקטים, הקשורים זה לזה)
  • גרסאות ספציפיות ל Groovy ו Scala – המרחיבות
ייתכן והיה נכון יותר לקרוא לו "JVM Plugin".
אתם יכולים לעבוד עם ה Java Plugin ישירות, או כל אחת מההרחבות שלו שיכולות להקל על המקרה הספציפי. בכל מקרה, רוב ההתנהגויות בכל הוריאציות – נובעות מה Java Plugin עצמו.
התיעוד הוא כלי חשוב, ובוודאי תמצאו את עצמכם לא פעם ניגשים לתיעוד של ה Java Plugin ישירות. במסגרת הפוסט, אני אתמקד בהסבר של שני רעיונות שמעט שונים ממייבן: Lifecycle Tree, וניהול תלויות.

מחזור החיים של בילד ב Java Plugin בגריידל

נתחיל עם מקור ההשראה: מייבן.
 
בניגוד ל make, ant, וכלים אחרים שהיו מקובלים קודם לכן והיו חסרי-מבנה סטנדרטי – מייבן הגדירה מבנה סטנדרטי – שעזר להשריש best practices בתהליכי build, ולהפוך אותם לסטנדרטיים ומאורגנים יותר.
 
מייבן הגדירה שלושה Lifecycles (נקרא להם build, clean, site), שבכל Lifecycle יש שורה של צעדים קבועים (להלן: "Goals"). כל Plugin (המספקים יכולות) מגדיר באלו צעדים הוא יכול להשתתף (למשל: Fail-Safe יכול להתחבר ל verify או integration-test) ואז המפתח ראשי לחבר אותו לאחד או יותר מהצעדים הנתמכים.

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

 

המבנה בגריידל הוא דומה, אך גמיש יותר. במקום שרשרת טורית של צעדים – ישנו עץ. העץ מורכב ממשימות (Tasks) כאשר כל משימה תגרום להרצה של המשימות האחרות בהן היא תלויה.
למשל, כאשר אנו מריצים את המשימה classes (המקבילה ל compile במייבן), המשימה תגרום להרצה של המשימות compileJava ו processsResources – ורק אז תרוץ בעצמה.
כאשר מריצים פרויקט המורכב מכמה תתי-פרויקטים בגרדייל, גריידל יכול להריץ במקביל משימות שונות בפרויקטים שונים על מנת להתקדם מהר יותר. למשל: פרויקט X תלוי בפרויקטים a ו b. גריידל יריץ את שניהם במקביל. אם פרויקט a הוא קטן יותר, גריידל עשוי להתקדם בפרויקט a לשלב הבדיקות על אף שפרויקט b עדיין מתקמפל.
כפי שאתם יכולים לראות בתרשים, משימות בגריידל מסווגות למשימות רגילות (עושות משהו ; בכתום) ומשימות Lifecycle (באדום), הנועדו לשמש כעוגן / מבנה לתהליך ה build.
למשל, המשימות assemble, clean, build ו check (באדום מודגש) הן משימות Lifecycle של ה base plugin בגריידל. כלומר: הן צפויות להיות נוכחות בכל מחזור חיים כלשהו. כמפתח Task אני יכול להסתמך על כך שהן יהיו נוכחות – ולבקש שהן ירשמו ל Task שלי כתלות.
לדוגמה, ה CheckStyle Plugin (כלי לבדיקות קוד סטטיות בג'אווה) רושם למשימה Check תלות בכל ה Tasks של ה Plugin – כך שהרצה של המשימה Check תפעיל אותו. אם לפני הרישום המשימות Check ו Test עשו אותו הדבר, לאחר הרישום – יש ביניהן הבדל.
משימות ה Lifecycle מייצרות סטנדרטיזציה ומבנה כדי שה Builds יהיו פשוטים ומובנים יותר.
כאשר מדובר ב Multi-project build (המקבילה של modules במייבן), כל הרצה של משימה על הפרויקט הראשי – תריץ את אותה המשימה על כל ה sub-projects (כמו במייבן).

Dependency Configurations

זוכרים את התלויות שהגדרנו ל JUnit5 ב build script שהצגנו למעלה?
השתמשנו בפונקציות ()testImplenentation ו ()testRuntimeOnly – על מנת להגדיר תלויות.
אנו נוטים לחשוב על גרף התלויות בפרויקט כמבנה קבוע, אך בעצם גרף התלויות הוא תלוי-הקשר:
אנו רוצים למשל שקבצים וספריות מסוימות (למשל: ספריות Mocking) יהיו זמינות רק כאשר מריצים בדיקות, בעוד אנו לא מעוניינים שהקוד בפרודקשיין יהיה תלוי בהן, או יארוז אותן.
היכולת להפריד בין קוד "לבדיקות" לקוד "פרודקטיבי" מתבטאת בגריידל בשני גרפי תלויות:
גרף התלויות של הבדיקות ("testImplementation") יורש את כל התלויות שהוגדרו בגרף של הקוד הפרודקטיבי ("implementation") – ויכול להוסיף עליהן תלויות נוספות.
הפונקציות ()testImplenentation ו ()testRuntimeOnly – פשוט רושמות תלות בגרף תלויות. עדיין לא הצגנו את גרף התלויות של testRuntimeOnly.
בגריידל התלות ברירת-המחדל / המומלצת היא "implementation". יש תלות בסיסי נוספת לקוד פרודקטיבי הנקראת "api" – והיא מדמה את ההתנהגות ברירת-המחדל של מייבן (מה שנקרא במייבן "compile scope"):
נניח שאני כותב ספריה (להלן "My Library") שאפליקציה מסויימת משתמשת בה.
כאשר אני יוצר תלות מסוג "api" בספרייה Jackson, משמע שאני הופך אותה לחלק מה API שלי, ובעצם מעביר (propogate) לאפליקציה את התלות כאילו היא שלה. מעכשיו קוד באפליקציה יכולה להשתמש ב Jackson כאילו ייבאה אותה בעצמה.
כאשר אני יוצר תלות מסוג "implementation" ספריה Gson, משמע שאני משתמש בתלות לצורך המימוש שלי, אבל התלות לא מועברת הלאה. אם האפליקציה רוצה להשתמש ב Gson יהיה עליה להוסיף תלות בעצמה או להסתמך על פונציונליות שהספריה שלי מספקת, שמתשמשת מאחורי הקלעים ב Gson.
לשימוש ב api יש תופעה שלילית שנקראת Dependency Pollution:
  • קוד האפליקציה יכול להשתמש בקוד מספריות שלא הייתה כוונה להשתמש בהן.
    • המקרה הזה בעייתי במיוחד בקוד פנימי שלנו, שאנו רוצים במפורש להימנע מהשימוש בו – אך מפתחים עשויים לא לשים לב (כי ה package name תואמים).
  • בשל האופי הטרנזיטיבי של התלויות, מרחב הקוד שאני שחשוף לשימוש ע"י קוד האפליקציה עשוי להיות גדול מאוד: האפליקציה תלויה ישירות ב 3 ספריות, אך ב 50 ספריות בצורה טרנזיטיבית – הוא תסריט נפוץ.
  • יש עבודה מיותרת לתהליך ה build – בהבאת כל הספריות, טרנזיטיבית, ל classpath בזמן קומפילציה.
  • יש עבודה מיותרת לתהליך ה build – בכך שכל שינוי קוד בספריה שהובאה טרנזיבית, מעלה ספק אולי צריך לקמפל את הספריות שתלויות בו (טרנזיטיבית) – וכך קשה לבסס build cache יעיל.
בקיצור: ההעדפה בתלות מסוג "implementation" חשובה הן מבחינת הנדסת-תוכנה, והן משיקולי ביצועים של ה build.
אם אתם רגילים לעבוד במייבן "הסלחנית" במובן של תלויות – תתחילו להתרגל.
בטווח הבינוני – ארוך, בהחלט משתלם להתרגל להגדרות המדויקות יותר של התלויות כ "implementation". בבאזל, למשל, הגדרת התלויות דורשות דיוק רב אף יותר.
בגריידל יש מנגנון נוסף, המתבטא ב Dependency Configuration בשם "runtimeOnly" ו "compileOnly" הקבילים ל scopes במייבן בשם runtime ו provided – בהתאמה.
התלויות הללו מאפשרות לנו להיות תלויים בזמן הקומפילציה ב API בלבד (לא לבלבל עם ה "api dependency configuration"), ובזמן ריצה להביא מימוש ספציפי של אותו ה API – תוך כדי שאנו מבטיחים שאנו לא יוצרים תלות במימוש הספציפי.
למשל: ספריית SLF4J, מספקת API לקומפילציה בלבד וגם מימושים הכוללים את ה API + יישום ספציפי (למשל LogBack או log4J12).
בכדי להבטיח חוסר תלות ביישום ספציפי – אנו רוצים לא לכלול את המימוש הנוכחי שבחרנו בזמן הקומפיציה – אלא רק בזמן ריצה. אם כך עלינו להוסיף את ה SLF4J API ב "compileOnly" ואת המימוש העכשוי – ב "runtimeOnly".
נראה שאנחנו יכולים להסביר עכשיו את התלויות שראינו בדוגמה למעלה:
ספריית JUnit5 הפרידה את ה API לספריה אחת (להלן "testImplementation", אנו רוצים אותו זמין רק לבדיקות) ואת מנוע הבדיקות – שיהיה זמין רק בעת הרצת הבדיקות ("testRuntimeOnly").
כלומר: המקרה של JUnit 5 מעט שונה מזה של SLF4J – אבל קיימות גם וריאציות נוספות.
יצרתי סיכום של ה Dependencies Configurations הזמינים לנו בגריידל + Java Plugin:

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

סיכום

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

שיהיה בהצלחה!
—–
קישורים רלוונטיים:

המדריך המהיר לזום ברצינות

זום (Zoom) היא תוכנה שנפלה לחיינו כך פתאום, והפכה לאחת התוכנות שאנו מבלים במחיצתה הכי הרבה זמן.

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

  • אתם רוצים להזמין מישהו לפגישה שלא הוזמן מלכתחילה – כמה זמן לוקח לכם להזמין אותו?
  • שיתוף מסך/הקלטה – קרה לכם שלכמה רגעים לא מצאתם את הכפתור?
  • פגישה מרובת-משתתפים וקצת רועשת. איך מוצאים מי מרעיש? מה אפשר לעשות?
    • פגישות שחוויתי בבתי הספר של הילדים היו לעתים ממש קשות – כאשר ילד אחד התחיל לקשקש על המסך (annotate), ולא ידעו לאתר מי זה…
  • שיתוף וידאו מתוך Youtube קופץ / אודיו לא מסונכרן / אין אודיו
  • כאלה…
הייתי רוצה לכתוב את הפוסט הזה גם לחברי לעבודה, גם לעובדי חברות אחרות, וגם למורים בבתי-הספר של ילדי. נראה שקשה לקלוע לכולם – אך אנסה לכוון לאדם בעל אינטואיציה טכנית גבוהה, שלא הקדיש זמן להעמיק בזום ויכולותיו השימושיות.
למשל, אני, לא ידעתי להגדיר Virtual Background עד שהחלה הקורונה. עד עכשיו כבר ביליתי כמה מאות שעות בזום – מה שקצת קידם אותי.
בואו נתחיל.
״זום זום זום…״

קביעת פגישות: קישור קבוע או קישור משתנה?

קביעת פגישה בזום נעשית על החשבון (Account) של מי שהזמין אותה.
לכל חשבון יש Meeting Id אחד קבוע – שייחודי רק לו.
אני יכול לבחור אילו פגישות ייקבעו על ה Meeting Id הקבוע, ואלו על Meeting Id זמני.
  • Meeting Id קבוע – אנשים יכולים לשמור את הקישור או אפילו לזכור (בעזרת Alias – אפרט בהמשך). מספיק לומר בסלאק למישהו ״בוא ניפגש בזום״ – והצד השני יכול להתחבר מבלי לקבל מייל / לקבוע פגישה דרך זום.
    • יותר חשוב: אתם יודעים תמיד להיכן להתחבר אם זו פגישה שאתם קבעתם. ה Meeting Id הקבוע יהיה תמיד בזיכרון של אפליקציית הזום שלכם.
    • שימו לב שבארגונים, ה Admin יכול לחסום את השימוש ב Meeting Id קבוע, משיקולי אבטחה (לא שאני תומך בזה, לארגון שאינו ״חשאי״).
  • Meeting Id זמני – הוא טוב בכדי למנוע ״כניסות לא מתוכננות״ לפגישה. למשל: מישהו מהפגישה הבאה ביומן. לפעמים זה לא נוח ולא מתאים.
    • מקרה דמיוני לחלוטין: אמא שלכם מצטרפת באמצע פגישה רבת משתתפים…
    • עוד יתרון של Meeting Id זמני – אתם יכולים לעזוב לפגישה הבאה – ושאר המשתתפים ימשיכו בפגישה (ניתן למנות מארח אחר). זה בלתי אפשרי אם זו פגישה על ה Meeting Id הקבוע שלכם.
מה עושים? הכי חשוב להכיר את ה Tradeoff ולהחליט מה נוח לכם. אני משתמש כמעט תמיד ב Meeting Id הקבוע – כי יש לי עליו +5 פגישות ביום, ונוח לי כך להיכנס אליהן.
את ה Meeting Id שלכם אתם יכולים למצוא בהגדרות שבווב. אפליקציות הזום השונות כוללות בתוכן מספר הגדרות (אפליקציית המובייל – מעט הגדרות, אפליקציה למחשב – יותר הגדרות), אך עדיין חלק גדול מההגדרות נמצא רק בווב. ניתן להגיע אליהן ישירות בלינק או באפליקציה של זום ע״י Settings / General / View More settings.
הנה ההגדרות: כאן אתם יכולים למצוא מה ה Meeting Id שלכם, לקבוע Alias (קישור ה ״Customize״), או לקבוע אם פגישות מיידיות (שלא נקבעו מראש לתאריך) יהיו על גבי ה Meeting Id הקבוע, או לא.
שווה לציין, שאם קבעתם Person Link עם Alias טקסטואלי, באפליקציות מובייל יהיה על המשתמש ללחוץ על ״Join with a personal link name״ לפני שיוכל להקליד את ה Alias.

אני מציין, בכדי שתוכלו לתמוך באחרים.

הקלטת פגישות לצורך תיעוד / שיתוף

אחד הפיצ׳רים השימושיים בזום, שמבינים לאחר זמן מה – הוא הקלטה של פגישות.
כאשר אתם מקיימים פגישה, ומישהו לא הצליח להצטרף – אתם יכולים להקליט עבורו את מהלך הפגישה.
כל פעם שיש דיון חשוב (במיוחד רב משתתפים) – אני מקליט את הפגישה. אנשים לעתים חוזרים לפגישות הללו, ולא תמיד מישהו שחשבתם עליו בזמן הפגישה.
הקלטה של הפגישה יכולה להיעשות רק ע״י ה Host (ניתן גם למנות Co-Hosts עם הרשאות דומות ל Host) ורק ממחשב. ברגע שההקלטה החלה, תהיה אינדיקציה ברורה לכל המשתתפים – בדמות עיגול אדום בפינה של המסך.
ניתן להקליט את הפגישה מקומית למחשב או לענן של זום (האופציה הזו זמינה רק למשתמשים בתשלום).
הענן של זום שומר מעין ״פורטל״ של ההקלטות של הארגון – אבל כרגע הוא מאוד מאוד בסיסי. ישנן אינטרגציות לחברות שמתמחות בניהול וידאו (כמו קלטורה הישראלית, Panpoto, Knowmia, ועוד). מיד כשהוידאו מוכן – הוא יעבור לפלטפורמת ניהול התוכן של הצד השלישי ובעצם ינוהל שם.
מבחינת איכות, ההקלטה של זום היא בעלת דחיסה גבוה מאוד, המתבססת על הנחות שתוכנת דחיסה "גנרית" לא תניח אותן (ערוץ אחד של audio – מונו, הגדרות שמתאימות לתזוזה מעטה, מה שלא טיפוסי בוידאו, וכו׳). ההקלטה היא עדיין ברזולוציה של HD כך שמסך מחשב שמשותף יהיה חד וברור.
בהגדרות של Recording (חלקן רק בווב), ניתן לקבוע הגדרות שונות לגבי ההקלטה, למשל: תמלול של ההקלטה (אל תנסו עם מבטא ישראלי). 2 הגדרות נפוצות הם אפשור של HD Video ו Group HD (ברגע שיש יותר משני משתתפים בפגישה, זום מוריד את איכות הוידאו המועבר של המשתתפים, הגדרה זו תשאיר את הדובר, בכל רגע נתון, באיכות HD).

שיתוף מסך בזריזות, ומבלי לפגוע בפרטיות

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

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

פגישת עבודה שוטפת (שלא נצמדת למצגת מוכנה-מראש) תדרוש שיתופים קצרים שונים מאנשים שונים.
אני אישית משתף מסך בעזרת קיצור מקשים (מסתבר שיש כאלו בזום), וספציפית Cmd+^+S במק.
אליה וקוץ בה: קיצור מקשים יפעל רק כאשר החלון של זום הוא הפעיל בשולחן העבודה – וזה העיכוב הראשון שפתחנו בו.
לשמחתנו, הגדרה של צירוף מקשים כ Global יהפוך אותו לזמין גם כאשר החלון של זום איננו בפוקוס.
אלטרנטיבה נוספת: לגשת לפעולות שונות מתוך ה Tray Icon של זום (לי היה פחות נוח).
את שורת הפקדים של זום (Mute, שיתוף, צ'ט, וכו – שלפעמים מתחבאת) ניתן לקבע כך שתמיד תופיע (ואז תוכלו מהר יותר לגשת לכפתורים השימושיים). בהגדרות: Settings/Accessibility/Always show meeting controls.
מומלץ מאוד, לפחות במקומות עבודה, לאפשר בהגדרות הפגישה בווב – לכל משתתף להתחיל להציג גם כאשר מישהו אחר כבר משתף מסך. לי זה חוסך במצטבר כמה דקות ביום.

פרטיות
יופי! התחלנו לשתף מסך בזריזות – איך אנחנו מוודאים שלא בטעות שיתפנו אימייל פתוח שלא כולם צריכים לקרוא? אולי בזמן שיתוף המסך מישהו שולח לי הודעה פרטית / רגישה בסלאק / Whatsapp – ותחילת ההודעה נקלטת ומוקלטת, כחלק מהפגישה? 😱
לי התקלות הללו קרו מספר פעמים. לא נעים!
חשוב להבין שזום היא סביבה בעלת סיכונים לפרטיות המשתתפים. לדוגמה, פעם היה פיצ׳ר שעזר למארח לזהות מי מהמשתתפים לא בקשב – אך הפיצ׳ר בוטל, משיקולי פרטיות מובנים.
בגדול ישנן 2 גישות לשיתוף בזום:
  • שיתוף כל ה Desktop עם כל מה שכלול בו. 
    • יתרון: אפשר לעבור בין אפליקציות במהירות – והכל משותף.
    • חיסרון: כל מה שפתוח – ניתן לצפות בו. למשל: הלינקים שלכם בדפדפן. שולחן העבודה, וכו'.
  • שיתוף חלון ספציפי, בכל פעם.
    • יתרון: שליטה גבוהה במה משותף (אך גם כאן ייתכנו ״זליגות״ של מידע פרטי. שימו לב).
    • חיסרון: מעבר אטי בשיתוף מסכים מאפליקציות שונות (IDE, ואז Github, ואז Command Line – למשל).
זהו Tradeoff אמיתי, אישי לאדם ולסיטואציה – ואין באמת פתרון שטוב לכל המקרים.
הנה כמה עצות איך לעשות את השיתופים הללו קלים ובטוחים יותר:
  • שיתוף כל ה Desktop:
    • במידה ויש לכם מסך שני – אתם יכולים לרוקן אותו לפני שאתם משתפים – ואז לשתף אותו ולשלוט ביתר קלות / זהירות מה יופיע בו.
      • לי היו קיצור מקשים להעברת כל החלונות לDesktop הראשי + קיצור להעביר חלון ל Desktop ספציפי.
      • לצערי: מאז הקורונה, ריבוי ילדים בזום הגביר את הדרישה למסכים במשפחתנו. אני כרגע משתמש במסך יחיד.
    •  ההגדרה הבאה פישטה את חיי כליל. אני חושב שהיום זו הגדרת ברירת-המחדל, אך אולי למשתמשים ותיקים יותר – היא לא פעילה. חשוב!:
    • בכל רגע ניתן "להקפיא" את שיתוף המסך (קיצור מקשים במק Cmd+^+T). כאשר אתם לרגע, למשל, מחפשים במייל או בסלאק – זה הזמן ״להקפיא״ לרגע את השידור. שאר המשתתפים יראו את התמונה שהוצגה על המסך ברגע שלפני ההקפאה – ולא ידעו שבינתיים אתם רואים משהו אחר. עוד לחיצה על הקיצור – תחזור לשדר את התוכן העדכני.
  • שיתוף חלון ספציפי במערכת ההפעלה:
    • כאשר אתם רוצים לעבור אפליקציה ולהמשיך לשתף, עליכם להפסיק את השיתוף הנוכחי – ולהתחיל שיתוף חדש.
    • צירוף מקשים הוא ה״מלך״ כאן. אם אתם משתמשים בקיצור-מקשים תוכלו לבצע מעברים כאלו בזריזות ואלגנטיות. אם לא – ההתנהלות תהיה מסורבלת ואטית.

טיפול ברעשים

הנה אחד העניינים המטרידים בזמן פגישות זום: רעשי רקע מאחד (או יותר) מהמשתתפים. יש מה לעשות – וכדאי להכיר את זה.
כאשר יש הרבה משתתפים (+10) לא תמיד הקריאה ״מי שרועש שיעבור למיוט״ – עובדת.
בעיקרון ניתן לזהות מאיזה משתמשים מגיע אודיו מעל סף מסוים – במסך ה Participant (מסך שימושי לעוד כמה צרכים):
אתם תראו ״אנימציה של גל״ בתוך הצלמית של המיקרופון – לכל מי שממנו מגיע עוצמה מסויימת של אודיו, ובתור Host אתם יכולים להעביר אותו ל Mute.
מטעמי פרטיות המארח לא יכול לעשות Unmute למשתתף – אלא רק המשתתף עצמו (וטוב שכך).
אם הרעש הוא רעש רקע, בזום יש יכולת מובנה להפחתת רעשים. זום בוחר את רמת הפחתת הרעשים באופן אוטומטי – מה שהוא לא עושה היטב. ניתן בהגדרות ה Audio של אפליקציית הזום לקבוע את רמת הפחתת הרעשים ל״High״ מה שבהחלט יעיל. ההגדרה הזו עושה פלאים ויכולה להשתיק כליל רעשי רקע של שיפוץ, גנן, שכנים רועשים, וגם ילדים צורחים בחדר סמוך.
החסרון היחידי: כאשר יש שקט מסביב – ישמעו אתכם פחות טוב, ויהיה כדאי לחזור ל Low או Auto.
בקיצור: קביעת רמת הפחתת הרעשים בצורה אוטומטית ע״י זום – לא עובדת היטב, אך בקביעה ידנית הפיצ׳ר הזה מאוד יעיל.
יש אפליקציה בשם Krsip המיועדת לעשות עבודה טובה יותר. ניתן להשתמש בה כמה שעות חינם בחודש, או לרכוש מנוי ב $3 ומשהו לחודש. כמה אנשים שעובדים איתי ממש מרוצים ממנה – אך אני עדיין לא הצלחתי להבחין בהבדלים בין העבודה שלה ליכולת המובנה של זום (כאשר מכוונת ל High).
בחזרה לפגישות מרובות משתתפים: שווה להזכיר שלמארח יש יכולת להשתיק את כל המשתתפים מלבד זה שמציג (שוב: ממסך ה Participants). כל משתמש שרוצה ״לזרוק מילה״ יכול להשתמש במקש הרווח (Spacebar) ב "Push to Talk״ – ממש כמו מכשיר קשר שצבא.
זה יעיל ושימושי, אם כי יכול להיות שלחלק מהאנשים חסרה ההגדרה. אם מוצא חן בעינכם – פשוט שתפו בארגון.

לסיום: מגניבות לתחילים

(סבים וסבתות יקרים: זה מה שהנכדים שלכם עושים כל הזמן, שאולי קצת מבלבל/משגע אתכם)
ניתן להוסיף רקעים ואפקטים לוידאו שלכם מתוך תפריט ה Start/Stop Video של זום. רוב היכולות זמינות רק לאפליקציה של זום למחשב.
יש המון מקורות לרקעים זמינים ברשת (הנה רשימה של זום, ו Unsplash – עוד אתר פופולארי). אצלנו בחברה מדי פעם אנשים ״מתכתבים״ ברקעים, ומעבירים בעזרתם מסר / בדיחה קבוצתית. למשל: ביום ההולדת אדם צפוי לראות הרבה רקעים של ״יומולדת שמח״ או הקשורים לימי הולדת. יש רקעים של סרטים אהובים, ספורטאים נערצים, וכו׳. נסו להיות מקוריים (רק בבקשה הקפידו על רזולוציה סבירה של תמונה 😊).
שווה לציין שזום, כברירת מחדל עושה Mirroring לוידאו שלכם, ולכן רקע עם כיתוב יראה לכם הפוך (אך שאר המשתתפים יראו אותו בסדר). אפשר לבטל את ה Mirroring בהגדרות הוידאו.
מה שפחות אנשים מכירים, הוא את היכולת להוסיף קובץ mp4. (ברזולוציה סבירה) כרקע דינאמי – כמו הרקעים הדימנמיים המגיעים עם זום. רק חפשו "mp4 background״ בגוגל – ותמצאו אינספור מקורות. חשוב למצוא רקע שלא מושך יותר מדי תשומת לב: ללא תזוזות מהירות, ועדיף בפוקוס-חלקי. אלו רקעים נעימים שלא גוזלים יותר מדי תשומת לב (ורוחב-פס) מהמשתתפים האחרים בפגישה.
לאחרונה נוספו לזום, פילטרים ו Studio Effects שיכולים לשנות בצורה ניכרת את המראה שלכם.

זה משעשע לרגע – אבל לא כל-כך מתאים לסביבת עבודה. אולי יותר לשיחות בזום עם הסבים / סבתות.

אם אתם רוצים אפקטים מגוונים / מושקעים באמת – אזי Snap Camera היא כנראה הכתובת.

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