על תרבות ה DevProd

בחברות שבהן האתגר העיקרי הוא Hyperscaling – הגיוני ונכון שתרבות ה DevOps תהיה המפותחת והעיקרית. בחברות מוצר (רוב חברות התוכנה?) דווקא הגיוני לשים דגש רב יותר על תרבות ה DevProd – כי משם יגיע ה Impact, אבל נראה שזה לא מה שקורה.

אם אתם עובדים בחברת מוצר, חשבו: כמה פעמים אתם שומעים בחודש את המונח “DevOps” וכמה “DevProd”? האם היחס משקף את יחס הכאבים / הפוטנציאל בין השניים?

מהיכן מגיע יותר waste? מחוסר תקשורת / שיתוף פעולה בין מפתחים לאנשי Operations, או בין מפתחים לבין אנשי מוצר?

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

מדוע זקוקים לתרבות של שיתוף פעולה עמוק בין פיתוח (Dev) למוצר (Prod)?

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

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

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

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

They, they sleep in a coma, yeah, yeah, yeah

They, they speak in a code

I don’t under-under-under-understand

Talking ’bout the business man

Business Man / Mother Mother

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

תלונות נפוצות של אנשי פרודקט על אנשי הפיתוח:

  • אנשי פיתוח לא באמת מתעניינים במוצר או בלקוחות. רק מעניין אותם טכנולוגיה ו”העניינים שלהם”.
  • אנשי פיתוח לא יודעים “לחשוב בגדול”. הם לא יצירתיים (לפחות לא “בדברים החשובים”) ואין טעם באמת להתייעץ איתם. עדיף לעשות את עבודת החשיבה לבד.
  • אנשי פיתוח הם חסרי מעוף גם באזורים שלהם. כל בקשה נענית ב “זה מסובך” ועם שאלות פשוטות הם מסתבכים. באמת הם זקוקים לאיש הפרודקט בכל פעם שייכנס לדברים ויעשה להם סדר?!

תלונות נפוצות של אנשי-הפיתוח על אנשי המוצר:

  • אנשי פרודקט הם מעופפים, חולמים בהקיץ ולא מחוברים לקרקע. יותר מדי בקשות הן מופרכות מהיסוד, ומראה שהם לא מבינים את המערכת / מהי תוכנה / היכן אנו חיים. “זה שכתבת שורה במצגת בדקה לא אומר שזה דקה לפתח את זה. אולי שנה?”.
  • אנשי הפרודקט לא יודעים לקבל החלטות / לחתוך. לשאלה הקלאסית “אתם רוצים א’ או ב’?”, התשובה הקלאסית היא “גם א’, וגם ב’, …ובעצם גם ג'”. עדיף פשוט לא לשאול אותם.
  • אנשי פרודקט לא יסודיים ומעמיקים ולא חושבים על דברים עד הסוף. נפוץ לקבל דרישות סותרות – והם עוד מתקשים להבין מדוע זה סותר. באמת הם זקוקים לאיש פיתוח שיכנס למסמך הדרישות ויעזור להם לארגן אותו?

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

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

שורש הבעיה

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

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

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

שורש הבעיה, ש DevProd מצליח לגעת בו (to address) הוא יחסי-התקשורת בין מנהלי-מוצר לאנשי-תוכנה, שדיי התקבעו בתעשייה על הצורה הלא-פרודקטיבית הבאה:

This is NOT DevProd

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

This is how DevProd looks like, in theory

סימנים לקיום / אי-קיום DevProd

הנה דוגמאות לסיטואציות / משפטים נאמרים שמעידים על אי-קיום או חוסר בתרבות DevProd:

  • “הפרודקט קובעים ‘מה’, מפתחים קובעים ‘איך’ “.
    • זה מטופש! אנשי-מוצר לא יצליחו לקבוע “מה” בצורה נכונה בלי אנשי-התוכנה.
    • לדרוש מאיש המוצר לקבוע מה לעשות, עם ההבנה המוגבלת שיש לו בנבכי המערכת – זו הכשלה. אנחנו לא רוצים להכשיל את אנשי-המוצר, השותפים שלנו.
  • “טוב, נשאל את הפרודקט מה הוא רוצה שנעשה”
    • שאלת מוצר לא צריכה אוטומטית “לעבור” לאיש-המוצר. אולי אנשי-התוכנה יכולים בכל זאת לענות עליה (ולכתב את איש-המוצר, כדי לוודא). לסירוגין, לפחות להציע חלופות עיקריות (שכבר עברו סינון טכנולוגי ראשוני).
    • הפינג-פונג בין פיתוח לאנשי-המוצר – צריך להפסק. לא עוד “לזרוק דילמה” לצד השני – ולצפות שהאחריות / כאב הראש ירדה עכשיו מאתנו ובאחריות של מישהו אחר.
    • מיותר לציין שהפינג-פונג הזה הוא דרך מצוינת למרוח זמן, ולעכב את הפרויקט / דליורי / פיצ’ר.
  • גרסה נוספת: “ההנחיה מהפרודקט היא לעשות X”
    • אנשי-המוצר לא אמורים “להוריד הנחיות”, המינוח הוא לא נכון. א, חשוב לדייק ובעצם יש לומר “הדעה של הפרודקט היא שנכון לעשות X”. זו דעה חשובה ורבת משקל, אבל עדיין דעה.
    • נכון לבחון את דעת אנשי-המוצר, ולאתגר במידת הצורך. מהנדסים – הפסיקו להסיר מעצמכם אחריות.
    • “עשינו את מה שאיש-המוצר אמר אבל יצא מוצר חרא” – הוא לא טיעון שנכון לקבל אותו, לוגית אפילו. האחריות היא משותפת.
    • “הגדרתי מוצר מעולה, אבל הפיתוח דפק הכל ולא הצליח לייצר אותו” – הוא כשל לוגי באותה המידה. איש-המוצר חייב לרדת לקרקע וליצור את מה שאפשר, ולא ליהאחז ב”חלומות” שלא ניתן לממש (ולכן תמיד הרעיון ישמע טוב, אבל המימוש יכשיל אותו).
  • ה DeadLock המוכר בתכנון פרויקט / רבעון / ספרינט:
    • אנשי-מוצר: “אמרו לנו כמה זמן כל דבר ייקח – ונאמר לכם מה נרצה לעשות”
    • אנשי-תוכנה: “אמרו לנו מה אתם רוצים שנעשה – ונאמר לכם כמה זמן זה ייקח”
    • אנשי-מוצר: “אמרו לנו כמה זמן כל דבר ייקח …”
      • תכנון פרויקט / רבעון / ספרינט צריכה להיות פעילות משותפת, Pair Planning של מוביל טכנולוגי ואיש-מוצר. די כבר עם הפינג-פונג המטופש הזה, של הטלת אחריות לצד השני.
  • יחסים בין אנשי-מוצר לאנשי-פיתוח שדומים ליחסים של ספק-ולקוח. איש המוצר הוא הלקוח, מספק דרישות ורוצה את המוצר בזמן, וקבוצת הפיתוח היא זו שמחויבת לעמידה בזמנים / להתמודד עם התקלות שעולות בדרך. איש-המוצר – לא מרוצה ולוחץ על קבלת “הסחורה” בזמן, ולא מסייע להתמודד עם הבעיות. זה סוג היחסים הבעייתי יותר – שיש לעצור אותו מיד. הוא מוביל לתרבות כסת”ח, ושהמיקוד יהיה מסביב לזמני אספקה, ולא נכונות/הצלחת המוצר.
  • איש-המוצר “נעלם לשבועיים” להכין את ה PRD. לאחר שבועיים, אנשי-הפיתוח שרואים את ה PRD לא מבינים אותו ו/או מוצאים בו אינספור חוסרים / אי-דיוקים / סתירות.
    • PRD שנכתב במעמד צד-אחד הוא לא PRD יעיל. אפשר לקחת פסק זמן למחשבה ותהייה. אפשר לעבוד אסינכרונית. איש-מוצר שכותב PRD ומציג אותו לקראת סיום – הוא לא מצב שצריך לקבל. אלא אם אתם, כעיקרון, עובדים בגישת ה Waterfall – ומרוצים ממנה.
  • פרויקטים הנדסיים ה”נעלמים” מעיני אנשי-המוצר: הארגון חייב להקצות זמן לפיתוח, עדכון, והתאמת הארכיטקטורה של המערכת לצרכים המתפתחים / משתנים של הארגון.
    • מצב מציק אחד הוא אנשי-מוצר שמנסים לדלל / לדחות / ולדחוק בהשקעה הסופר-חשובה הזו. מצב בעייתי אחר הוא אנשי-פיתוח ש”מעלימים מהרדאר” פעילויות הנדסיות, כדי שאנשי-מוצר לא ישאלו / יציקו / “יסכנו” אותן.
    • ההשקעות ההנדסיות צריכות להיות גלויות לעיני הפרודקט. אנו רוצים שיסמכו עלינו שאנחנו עושים את הדבר הנכון – גם אם הם לא מבינים הכל. מובן. מצד שני – חשוב לאפשר ביקורת מצד אנשי-המוצר. כמו ועדה בכנסת שבוחנת ומאתגרת חברה ממשלתית. זה לא כיף (בעיקר לאנשי-הפיתוח), אבל זה חשוב מאוד לאמון ההדדי, ולצמצום waste – כי אנחנו יודעים “שפרויקטים הנדסיים” נוטים להסתבך ולגדול ב scope גם מעבר ל scope המינימלי ההכרחי. אם אתם תומכים בביקורת של בית-הנבחרים (כנסת) על הוצאות הצבא – רק הגינוי שתתמכו גם בביקרות של אנשי-המוצר על פרויקטים טכנולוגיים.

אם אתם יושבים ב Open Space ו/או ב Open Zoom ושומעים את אמירות הללו / נתקלים בסיטואציות האלו, ואתם רוצים תרבות DevProd – אתם צריכים לעצור ולתקן אותן.

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

הנה סימנים חיוביים לתרבות DevProd, שיש להגביר ולאמץ:

  • איש המוצר משקיע “את הנשמה” ללמד את אנשי-התוכנה כל מה שהוא יודע, ואת כל התובנות הקטנות והמעניינות על השוק, המוצר, והלקוחות. הוא לא “שומר לעצמו” שום דבר מעניין. הוא ממש מרגיש כמו מנטור שצריך לרוקן את כל הידע והתובנות שלו החוצה.
  • אנשי-התוכנה מקשים באופן תדיר על איש המוצר. להקשות על איש המוצר בפן העסקי זה לא “מותר”, אלא מה שצפוי מעובדים טובים. קשה להיות איש-תוכנה מוערך אם אתה לא עושה את זה.
  • אנשי התוכנה דורשים מאשת המוצר עוד חיזוקים על היעדים העסקיים, כתנאי להשקעה משמעותית: “אבל איך את בטוחה שדווקא זה יעשה את האימפקט? מה זאת אומרת – זו הצלחה עיוורת? דברי במספרים גברת – דברי בנתונים!”
    • (כמובן שהפוסט מדבר על נשים וגברים כאחד, הפעם בחרתי בדמות אישה בשביל הציטוט/חרוז).
  • אנשי-התוכנה משקיעים זמן ומאמץ כדי לפרוס את הסיבוכים, העלויות, והתלויות בין הרכיבים בפיתוח המוצר עבור אנשי-המוצר. הם עושים את זה בדאגה ובאהבה כאילו זו אמא שלהם, שצריכה עזרה ב”איך להתחבר לאינטרנט” או ילד, שרוצים ללמד אותו משהו, לתת לו משהו ושיבין לעומק – למרות שיש לו עוד הרבה פערי-ידע.
  • מפתחים לא רק מציפים שאלות לאיש-המוצר (“זריקת אחריות מעבר לגדר”) אלא נוטים להציע פתרונות משלהם (שעוזרים להעביר לאיש-המוצר את המבט ההנדסי על הענין). ההחלטה, באידאל – באיזו אלטרנטיבה לבחור – היא משותפת. שום מפתח לא רוצה לשחרר פיצ’ר עם שימושיות לא-טובה ללקוחות.
    • לא פעם, הדרישות – אפילו של חווית המשתמש הן מורכבות לוגית: לחשוב על כל מקרי-הקצה ולארגן אותם. קל לאנשי-התוכנה “להשליך” את הבעיה לאנשי-המוצר, ואז להתאכזב מהם. אולי זה אפילו קצת מהנה / מספק הרגשת-עליונות בפתרון בעיות לוגיות?
      בתרבות DevProd – מצופה מאנשי-הפיתוח לזהות החלטות לוגיות מורכבות ו”להכנס בהן תחת האלונקה” ולעזור לאיש-המוצר להגדיר אותן ולהגיע להחלטה/פשרה הטובה ביותר. העיקרון הזה נקרא גם DBASH (קרי: don’t be a schmuck)
  • המידע העסקי זמין לכולם: ישנם מפתחים (בוודאי לא כולם או הרוב) אשר ניגשים לנתונים העסקיים, בוחנים אותם ומחפשים (ובשאיפה: גם מוצאים) בהם תובנות. כפי שה System Monitoring בתרבות ה DevOps לא זמין רק לאנשי ה Operations – כך הנתונים העסקיים (פלח שוק, מתחרים, תוצאות A/B tests) לא צריכים להיות זמינים רק לאנשי-המוצר. איש-המוצר הוא המומחה האולטימטיבי לנתונים (כמו איש ה Operations) – אבל הנתונים זמינים לכל מי שמתעניין ורוצה לעשות קצת מעבר.
    • Dashboard עסקי משותף על מדדי הליבה של הקבוצה / צוות / פרויקט – נשמע כמו צעד הגיוני ורצוי.
    • בתרבות DevProd מצופה בבירור מאיש-המוצר “לדחוף” את הנתונים לאנשי-התוכנה, ולנסות כל הזמן לעניין אותם בהם – ולא רק כתגובה לשאילתה. ברור, הכי נוח לשמור את הקלפים “קרוב לחזה” ולא להיות מאותגר בשאלות קשות – אבל זו לא תרבות DevProd.

מה עוד לעשות, ברמה הפרואקטיבית – לקראת DevProd?

תקנו את הטייטל (תֹּאַר)

הטייטל “Product Manager” הוא מטעה ובעייתי: איש-המוצר לא אמור “לנהל” לבד את המוצר, ובוודאי לא לנהל את הצוות. אבל זה מה שהרבה פעמים קורה, ונראה שהטייטל הוא חלק מהסיבה לטעות.

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

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

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

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

כיוון אחר הוא להסתכל מצד ה Engineering ולנסות להכניס את המונח Product Engineers לשימוש: מהנדסי-תוכנה שיש להם את שאיפה ויכולת לחשוב לא רק “איך” לבנות את המוצר, אלא להבין ולהתעמק ב”למה” לבנות את המוצר. כאלו שיכולים להתחבר לאנשי-המוצר, ללמוד מהם, ולעזור להם. לא כל מהנדס היה כזה, ואנו לא זקוקים שכולם יהיו כאלו – אבל אם כמנהלים נזהה אותם וניתן להם להתפתח לכיוון הזה, ולעבוד עם אנשי-מוצר, הם יכולים להיות ה backbone של תרבות ה DevProd בארגון שלנו.

שקלו להעביר את ניהול הפרויקט מאיש-המוצר

דיי נפוץ שבצוות / SCRUM / SQUAD יש כמה אנשי-תוכנה, אחד מהם כנראה מוביל או ראש-צוות, ואיש מוצר. משום מה, ניהול ומעקב אחרי הפרויקט (הגדרת milestones, מעקב אחריהם, תקשור פנימה לצוות והחוצה לארגון) – נופל לא פעם לידיו של איש-המוצר. מדוע?
כי הוא “מנהל”? כי הוא נתפס כ focal point של הפרויקט מול ההנהלה? כי לו אכפת יותר מההגעה ליעדים העסקיים? כל התשובות לא טובות.

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

תרבות ה DevProd כיעד – להשיג ולהתגאות בו

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

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

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

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

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

סיכום

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

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

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


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

Standing Together: 7 Principles for Great Product/Engineering Relationship – מירי כוריאל

Design By Example III: Abstractions – חלק ב’

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

בכל אחת מהחלופות, ננסה לבחון את ההיבטים הבאים:

  • פתרון הבעיה העסקית – רמה #0 ע”פ מודל בגרות התכנון (אני מניח שאת רמה #1 ~בערך~ כיסינו בפוסט הקודם).
  • הכוונה / הצהרת כוונות – רמה #2 ע”פ מודל בגרות התכנון.
  • גמישות עתידית – רמה #3 ע”פ מודל בגרות התכנון.
  • עקרונות תוכנה – האם אנו מפירים איזו עקרון מקובל? זה סימן למשהו שחשוב לבדוק.

כמה הערות לגבי גמישות עתידית של המודל (Predicted Variations):

  • Predicted Variations הוא עקרון שעשוי לתרום, אבל להזיק – יש כאן בבירור Tradeoff:
    • אפשור היום לגמישות עתידית – הוא הימור. אם לעולם לא נגיע לידי שימוש בגמישות הזו – הרי שבזבזנו זמן, וסיבכנו את המערכת. השקעה / סיבוך היום, שלא יגיע לידי שימוש בעתיד – הוא בזבוז ברור. יש שיטענו ש Predicted Variations הוא דרך מבטיחה ל Overengineering.
    • גם השקעה היום, שניתן לבצע באותו עלות בעתיד (נאמר: שבוע עבודה היום, מול שבוע עבודה עוד שנה) – היא בזבוז.
    • השקעה משתלמת היום תהיה כזו ש:
      • חוסכת משמעותית עלות בעתיד. למשל: שבוע היום, מול חודש עוד שנה.
      • לחלופין: עוזרת להכווין את הדרך / לשמר אופציה עתידית חשובה. אולי תמיכה באנדרואיד (subsystem) ב Windows 11 היה קל לפתח בהתחלה ובסוף באותה המידה, אבל הצבת היסודות בשלב מוקדם מחדדת לכולם את המסר שזה כיוון אסטרטגי – ועוזרת לבדוק שפיתוחים אחרים אינם “חוסמים” את היכולת הזו.
    • כבני-אדם, בוודאי שאנו נוטים להערכת יתר של אפשרויות עתידיות. בדקו את ההיסטוריה של ההחלטות שלכם: אם אחוז ניכר של “ההכנות למזגן” (כינוי לא-מוצלח לגמישויות עתידיות) שיצרתם – לא הצדיקו את עצמן בבירור, זה סימן חזק שכדאי לכם להיות שמרנים יותר בהערכות העתיד שלכם. כל פיתוח שניתן לדחות לעתיד – עדיף. פיתוח שניתן לדחות – ולא יהיה בו צורך, על אחת כמה וכמה.

חלופה 1

  • פתרון הבעיה
    • חסר הטיפול במקרה-הקצה של שאלה המופיעה ב Entity Hub.
      • אולי זה מקרה מספיק פשוט לפתור בהמשך הדרך, שלא סביר בכלל שישנה לנו את התכנון בצורה מהותית – ואולי זה בדיוק הדבר שעלול לסבך אותנו בעתיד. אני הייתי מעדיף לסגור את הנושא הזה לא בסבב הראשון של התכנון – אבל בהחלט לפני הגעה למימוש.
  • הכוונה
    • יש חולשה בייצוג של EntityHub המכיל רשימה של דפים. אנחנו לא אומרים כלום על הקשר בין הדפים הללו (מלבד שיש להם סדר) או על הדמיון הבלתי-נמנע בין השאלון כולו (Questionnaire) לסט הדפים הללו (שקל לדמיין אותם כ “sub-questionnaire” מאיזשהו סוג. בעצם אי אמירה על הקשר – אנחנו אולי אפילו רומזים שאין קשר בין השניים, ומובילים את הבאים אחרינו ליצור שני מנגנונים שונים.
      • ההחלטה ששאלון ו”שאלון ל Entity” צריכים להיות שונים – היא הכוונה. למשל המבנה הבא מספק אמירה: (אם היא רצויה – אדרבא)
    • המונח Step (“שלב”) היא הפשטה גבוהה. כלומר: מתירה הרבה מקום לדמיון: האם popup בנוסח “לא ענית על כל השאלות, האם תרצה להמשיך בכל זאת? כן/לא” הוא שלב? האם ייתכנו שלבים ללא ייצוג ויזואלי? (למשל: שמירת נתונים, בדיקת אימות בצד השרת)? האם לחזור לדף קודם הוא שלב? אולי זה מתאים, ואולי לא – חשוב לשים את הדעת על הבחירה הזו, בהפשטה גבוהה.
      • נדבר שוב על ההפשטה הזו בחלופה 2.
    • גם Element היא הפשטה גבוהה. בעצם – ברמה הגבוהה ביותר. “אלמנט” הוא אפילו יותר מופשט מ”אובייקט” (שבעולם מתייחס בדרך כלל לדבר פיסי, ולא לרעיון). נראה בחלופה 3 לאן זה לקח אותנו.
  • גמישות עתידית
    • הייצוג של תת-השאלון ל Entity כרשימה של דפים – מגבילה בראייה של גמישות עתידית. אולי יש צורך כזה, ואולי לא.
      • שווה לראות את הגישה של חלופה 4 לעניין.
  • עקרונות תוכנה – אני לא מזהה חריגה.

הפשטות גבוהות מול הפשטות נמוכות

בשנות ה 80 ו ה 90 העליזות, של C, Cobol ו Pascal – מתכנתים כמעט ולא השתמשו בהפשטות, ופספסו הזדמנויות מידול בקוד שלהם. תנועות ה Object-Oriented וה Patterns שינו את המצב מקצה לקצה – והיום יש רבים שעבורם “גנרי”, “הפשטה”, ו”גמישות” – הם בהכרח דבר טוב. חלון נפתח (מטאפורה לגמישות) בבית שלנו – היא גמישות חשובה, אבל חלון נפתח שבתוכו חלון נפתח ובו עוד חלון נפתח – הוא כנראה מתכון לגמישות מיותרת שבעיקר תעשה בעיות. חשוב למצוא את מידת הגמישות הנכונה לבעיה.

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

זה כמובן תלוי. Visual היא הפשטה מאוד גבוהה. היא תגרום למפתחים באזור לדמיין ולגשת לאפשרויות אחרות שההפשטות האחרות, הנמוכות יחסית, לא יאפשרו. מצד שני – היא יכולה “להכשיר” עיוותים בלתי רצויים בעליל. למשל: Visual נועד לציין תוכן (content) על המסך, אבל ההפשטה הגבוהה מתאימה גם ל control (רכיבי שליטה) כגון כפתורים בתוכנה, מסגרת, tooltip – וכו’, וכך הדברים מתערבבים. שימו לב כמה הכוונה יש בכל רמה של הפשטה, ואיזה כלי משמעותי זה להכווין את הבאים אחרינו – להיכן אנו רוצים שהדברים יתפתחו.

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

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

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

חלופה 2

לחלופה הזו יש הרבה חפיפה עם חלופה 1. נתמקד בשני ההבדלים המהותיים:

  • פתרון הבעיה העסקית – פותרת.
  • הכוונה
    • כל Step מכיל Elements. זו בעצם הגבלה – ההיפך מהפשטה.
      • ניתן להתפשר ולהחזיק רשימות ריקות / null כאשר לא נדרש – אבל המשמעות היא קוד מסורבל יותר, ומסר הרבה פחות ברור לגבי הכוונה.
      • עצם כך שרוב ה Entity Hubs (ע”פ ה narrative מהפוסט הקודם) לא יכללו אלמנטים – ואנחנו פה קובעים שכל Step מכיל Elements – בעצם יצרנו כלל שרוב הפעמים אינו נכון. זו הכוונה הפוכה. אפשר לומר: כמעט הטעייה.
        • כשדורשים מאתנו לחבוש מסיכות (רפואיות) תוך כדי אכילה – כנראה שנסיק שמי שקבע את הכלל לא ממש מבין. כאשר אנחנו נתקלים בהכוונה הפוכה – שמתנגשת עם המציאות – קורה אותו הדבר. עולים סימני שאלה לגבי איכות ההכוונה.
      • יש סתירה ברורה בין ההפשטה הגבוהה (“Step”) לבין ההגבלה שכל Step כולל Elements. נראה שניסו לקרוא ל Step בשם טיפה יותר מצומצם “QuestionnaireStep”, אבל מכיוון ש Step מוחזק ע”י Questionnaire – לא נראה שנוספה כאן משמעות (מקסימום השם עומד טוב יותר בפני עצמו). ככל שההפשטה גבוהה יותר, נצפה לפחות קביעות (הגבלות) על הפשטה. הגבלות / קביעות על ההפשטה הוא כלי שימושי להכוונה – אבל במקרה הזו זו פשוט נראית הכוונה לא טובה.
  • גמישויות עתידיות
    • EntityHub מכיל QuestionnaireStep – ולא Pages.
      • זו בעצם גמישות, שמאפשרת עץ מקונן של דפים ו EntityHubs.
      • הקשר בין EntityHub ל Page הוא פחות ברור אפילו מחלופה 1 (קשר עקיף).
      • אם יש צורך עסקי אמיתי באופק למבנה כזה – ייתכן וזה מודל טוב. על פניו מהתיאור בפוסט הראשון – זו נראית כמו גמישויות מיותרת המטשטשת את הכוונה.
  • עקרונות תוכנה – אני לא מזהה חריגה.

חלופה 3

החלופה הזו צורמת בעיני מהמבט הראשון, מכיוון שהיא מפירה את עקרון ה SLAP (Single level of abstration principle), מה שגורר הפרה של עקרון ה (POLA (principle of least astonishment. אני יודע בקרב המגיבים לפוסט הקודם – זו הייתה האופציה המועדפת, ואני מוכן להגן על עמדתי. טיעון שהועלה הוא “פשטות”, ואכן פשטות הוא יתרון אמיתי – אבל אני אנסה להראות שהפשטות שהחלופה הזו מציגה היא בעיקר מראית-עין, ולאורך זמן אני מעריך שהיא לא תחזיק מעמד. מצד שני – בצד ההכוונה, דווקא יש סיכון ממשי להכוונה לכיוונים לא מועילים. אפרט.

  • פתרון הבעיה העסקית – פותרת.
  • הכוונה
    • כפי שציינתי כבר בחלופה 1, המונח “Element” מספק הפשטה מירבית[א], מה ש”מתיר” להכיל: כלב, עץ, עוני, ורקורסיה – כ Elements נוספים במערכת. המונח Element לא סותר / דוחה את האפשרויות הללו מעצם שמו.
      בקיצור: הפשטה מירבית היא הכוונה אפסית. אין פה הכוונה. הכל הולך.
      • מה היה אפשר לעשות אחרת? לספק הכוונה מסוימת. למשל, השם “QuestionnairePageElement” כבר מגביל / מכווין אותנו הרבה יותר טוב. גם כלב, וגם רקורסיה – כבר בבירור אינם מתאימים. EntityHub – פחות מתאים, אבל עדיין יכול “להשתחל” עם קצת דמיון (כ “iframe ויזואלי”). אם היינו קוראים ל EntityHub בשם EntityPage – זו הכוונה נוספת, כי זה לא נשמע טבעי להכיל page בתוך page. מונח כמו “QuestionnaireComponent” יכול להיות הכוונה, אם המונח Component מתקשר אצלנו חזק לרכיב UI עצמאי (כך ב UI frameworks מסוימים). בקיצור: הייתי מנסה להחליף את המונח Element במונח שמכווין יותר את הכוונה.
  • גמישות עתידית – יש אפשרות להוסיף כמעט כל דבר כאלמנט – מה שנוגע בנקודה הבאה.
  • עקרונות תוכנה
    • כותרת (Title), שאלה (Question), תמונה (Picture), ועמוד ניהול ישויות (Entity Hub) הם לא באותה רמת הפשטה. אני מניח שזה בולט ברמה של תרגילי “מצא את יוצאי הדופן” הפופולריים בחוברות עבודה של הילדים שלי כשהיו בגילאים מוקדמים. (לא פעם אגב, הרגשתי לא שלם עם התשובה שהחוברת מציעה ל”יוצא הדופן”).
      • הם בסדרי גודל אחרים: חייל בודד מול פלוגה.
      • הם עצמאיים במידה שונה: אחד זקוק ל Container / מסגרת שתכיל אותו – והשני לא.
    • נטען שהכנסת כל הנ”ל לאותה הפשטה תאפשר קוד פשוט יותר (ריבוי-צורות / polymorphism) – אבל ריבוי-צורות לא עובד בפועל, כאשר הרכיבים השונים בו לא דומים מספיק זה לזה. התוצאה לרוב היא branching הולך וחוזר בקוד:
      • if type = EntityHub -> do x
      • else -> do y
    • כלומר: יצרנו הכללה (“Entity”) לפריטים שזקוקים לטיפול שונה מהותית, ולכן למרות היכולת להכיל אותם באותו מבנה נתונים (<List<Entity, למשל) זה לא יעבוד ברגע שנטפל בקוד אחרת – ובעצם נטפל, ברוב המקרים, בשתי קבוצות שונות של פריטים. כלומר: כאילו הייתה לנו הכללה, אבל בפועל הקוד נאלץ לטפל בשני מקרים נפרדים.
    • הבעיה הכי גדולה, היא “ההזמנה” להוסיף כל פריט נוסף להכללה הגבוהה של “Entity”. מכאן הקוד ילך ויסתבך. גם ב branching גדול יותר בקוד, אפילו יותר – באי-חלוקת הקוד לנושאים / אזורים מופרדים (אותו מחלקה תטפל בכל הסוגים השונים של הפריטים), והכי גרוע – פספוס ההזדמנות לחלוקה יותר הגיונית והכוונה יותר טובה של האזור הזה בקוד – לו היינו משתמשים בהפשטות טובות יותר.

חלופה 4

טוב, אני חייב להודות שזה המודל המאוזן והפשוט ביותר לטעמי, ע”פ הבנתי של ה narrative. עדיין יש לו חסרונות, בואו נראה:

  • פתרון הבעיה העסקית – לא טיפלנו בשאלה על EntityHub – וזה חסר.
  • הכוונה
    • כפי שציינתי, לפי דעתי הכי פשוט ומאוזן מכל החלופות האחרות:
      • Step הוא אחד משני מצבים – הנבדלים זה מזה.
      • EntityHub בעצם קשור לשאלון, ישות שמשמעותה ברורה.
        • כן הייתי מצפה שיכולות הנוספות לשאלון, ייתמכו גם ב”תת-השאלון”. אני מניח שגם משתמשים לא היו מבינים למה שרמה 0 (שאלון-העל) יש התנהגות אחת, ובתת-שאלון (רמה 1) – יש התנהגות אחרת.
    • עדיין Entity היא הפשטה גבוהה מדי, וגם Step עדיין פתוח לפרשנות (לטוב ולרע – תלוי למה אנחנו מתכוונים)
  • גמישות עתידית
    • בחינתי הצד הטוב הוא שימוש חוזר ביכולת ה Questionnaire גם לתת-שאלון.
    • הקוראים ציינו שהגמישות להכיל היררכיה של EntityHubs אינה נדרשת – והיא נראית כגמישות מיותר. אני מסכים – ומעדיף לחסום אותה.
  • עקרונות תוכנה – אני לא מזהה חריגה.

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

ככלל: כשאנחנו בוחרים בין חלופות, אל לנו להפסיק בבחירת החלופה. ניקח את החלופה הטובה ביותר בעינינו – ונפתח אותו כך שתהיה טובה אפילו יותר.

  • ניסיתי להגביר את ההכוונה בעזרת מונחים המובילים להפשטות נמוכות יותר:
    • Questionnaire Page במקום Step. לא נראה שצריך משהו יותר מזה בשלב הזה. להגביה את ההפשטה בעתיד – לרוב קל יותר מאשר להנמיך הפשטה.
    • Component במקום Element – בהנחה שברור שזה רכיב ויזואלי בודד בדף. זה שינוי חשוב בעיני.
  • הוספתי ל EntityHub Page שאלה אחת אפשרית. כלומר: יש טיפול מיוחד (אי שימוש חוזר בקוד ה Component) בשאלה על EntityHub – אבל זה נראה לי האופציה הפשוטה יותר בסה”כ.
  • הגדרתי שני סוגים של Questionnaire כדי לחדד שלא כל תכונה / יכולת של ה Root Questionnaire תהיה בהכרח ב Sub-Questionnaire, למנוע שלא נסתבך.
  • הוספתי constraint על ההורשה ש Sub Questionnaire לא יכיל Entity Hub Pages. אין צורך כזה – וחבל לסבך את המערכת.
    • איך ממשים את זה? תלוי בשפת התכנות. ניתן לבודד את Sub-Questionnaire שיחזיק רק QuestionnirePages – אבל אני חושש שהתרשים קשה יותר לקריאה:
  • אני שומע כבר ביקורת עולה: אבל הפתרון שלך יותר מסובך מכל האחרים. זו פשטות???
    • אני טוען: התרשים מורכב יותר – לא הפתרון. בכל מקרה בקוד (שיהיה מסובך עוד יותר, אני מניח) – נתמודד עם השאלות הללו. אני מעדיף לפתור אותן בשלב התכנון, ואני מניח שהתרשים המפורט / מורכב מעט יותר – בסה”כ יתרום להבנה משותפת של מי שעובד על הפיצ’ר. השאלות הגדולות הן שם – ובאופן דיי פשוט, לדעתי. למנהלים בכירים אפשר להציג בתור התחלה תרשים מופשט יותר (כמו התרשים של חלופה 4, עם מונחים המובילים להפשטות פחות גבוהות)

סיכום

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

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

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

מטרת הפוסט לא הייתה לדון בפתרון כזה או אחר – אלא בדרך להגיע לפתרון.

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


[א] – אני מודע לכך ש”מירבית” הוא כתיב לא תקני – אבל הוא נראה לי ברור יותר. כמו שפרי ברבים צריך להכתב פרות (Peyrot), אבל הגיוני יותר עדיין בעיני לכתוב פירות.

Design By Example III: Abstractions

בשני הפוסטים הקודמים של “תכנון מתוך דוגמה” (א.ק.א Design By Example), התמקדנו במציאת פתרון לבעיה קונקרטית. זו חלק קריטי בכל תכנון שהוא – אבל יש בתהליך התכנון גם מעבר לזה. בפוסט הזה נרצה להתקדם מעט יותר ב Liorson Software Design Maturity Model ולגעת ברמות השנייה (החצנת כוונות) והשלישית (התאמות לעתיד) של תכנון מערכת. הפשטות (Abstractions), הן כלי מרכזי בשתי הרמות הללו.

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

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

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

עבור שלב 2 של המודל (החצנת כוונות) עומדים לפנינו כמה כלים:

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

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

בואו נתחיל בתיאור של בעיה.

הבעיה שברצוננו לפתור

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

  • “שאלון” הוא רמת העל, מקצה לקצה – של כל מה שאנחנו שואלים את בעל העסק.
  • השאלון מורכב מדפים, שבכל דף כמה אלמנטים: כותרות, שאלות, ולעתים תמונות / דיאגרמות (התומכות בשאלות).
  • בשאלון אנו אוספים מידע על “ישויות” השייכות לעסק. למשל, ברשת מסעדות – אנו רוצים אוספים פרטים על הרכבים של העסק, ועל המבנים (Facilities, יכולים להיות גם מבנים ארעיים).
    • עמוד מיוחד, Facilities למשל, מציג את רשמית המבנים ומאפשר להוסיף / להוריד מבנים מהרשימה.
      • כל הוספת מבנה תציג סט של שאלות שיש לענות לכל מבנה. למשל: 10 שאלות לכל מבנה, המתפרסים על פני שני עמודים.
    • לאחר שמסיימים את איסוף הפרטים על המבנים, לוחצים בדף ה Facilities על הכפתור “Continue” וממשיכים ברצף השאלון.
  • אהה… ורק בדף של ה Facilities צריך גם להציג שאלה אחת “Include warehouses?” שמשפיעה על השאלות שישאלו לכל מבנה. זה אמור להיות דיי יוצא דופן, ולא להופיע כמעט אף פעם באובייקטים אחרים (רכבים, שותפויות/מועדוני לקוחות (לרשתות מזון), וכאלו…).

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

החלופות

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

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

חלופה 1:

  • Step הוא צעד בשאלון, שיכול להיות דף (Page) או צומת לניהול ישויות (Entity Hub). זה השלב שבו אנו יכולים להוסיף ולהסיר רכבים, מבנים וכו’. אשתמש בכל החלופות במונח Entity Hub בכדי לא לבלבל.
  • כל דף מכיל סדרה של אלמנטים. ב UML (שפת התרשימים לתיאור מבנה של תוכנה) סימן ה * מתאר יחס של רבים. כלומר: לדף יחיד – יש אלמנטים רבים (1 או יותר), ואולי מסוגים שונים – כפי שתיארנו למעלה.
    • על מנת לוודא שברור: גם דף יש יותר מאחד. השאלון (Questionnaire) מכיל צעדים רבים, ודף הוא Generalization (ראש חץ לבן חלול ב UML) של Step.
  • Entity Hub מחזיק קשר למספר דפים, אלו הדפים שצריכים להישאל לכל ישות שמטפלים בה: נניח 2 דפים של שאלות לכל Facility, כמו שראינו בתרשים למעלה.

חלופה 2:

  • הפעם אלמנט הוא תכונה של QuestionniareStep (האם זה שם עדיף? מה המשמעות של בחירה בשם זה או הקודם?) כך שבעצם יש לנו אלמנטים (שאלות, למשל) גם ב EntityHub. זה עוזר לכסות את מקרה הקצה ב Facility Hub, בו עלינו לשאול שאלה. מי יודע, אולי אם הזמן נגלה שאנו צריכים להציג עוד אלמנטים בתוך ה Entity Hubs?
  • ה Entity Hub לא מחזיק דפים, אלא Questionnaire Steps – מה שמאפשר לו להחזיר שורה של דפים (המקרה הידוע) אבל גם Entity Hubs בנים – מה שיכול לאפשר היררכיה. למשל, איסוף מידע על מחסנים (Warehourses) בכל מבנה (Facility). לא נדרש היום, אבל האם זה לא יהיה חכם לאפשר את זה במודל כבר מעכשיו?

חלופה 3:

  • במקרה הזה, אנחנו מגדירים EntityHub כסוג של אלמנט. כלומר: ייתכן שה Entity Hub הוא דף עם אלמנט יחיד: ה EntityHub.
    • הדבר מאפשר להוסיף את השאלה שאנו נדרשים ב FacilityHub, וגם מאפשר בצורה טבעית לשלב בין Hub לאלמנטים נוספים באותו הדף: כמה שאלות, כותרות, או תמונות. הדבר גם מאפשר לצרף 2 EntityHubs באותו דף. למשל: לאסוף רכבים ומבנים – באותו דף של שאלות (כמובן שכל ישות: רכב או מבנה תפנה לשאלות משלה).
  • ע”י כך שהפכנו את ה EntityHub לסוג של Element – ייתרנו את ההפשטה של Step/QuestionnaireStep – וכך בעצם פישטנו את התכנון!

חלופה 4:

  • המבנה הזה, האמת, דומה מאוד לחלופה הראשונה – אבל מתאר הבדל אחד גדול ומשמעותי: ה EntityHub לא מחזיק דפים, הוא מחזיק Questionnaire.
    • הדבר מאפשר לנו ליצור היררכיה של EntityHubs, דומה למה שתואר בחלופה 2 – אבל בדרך אחרת. (איזו דרך עדיפה? מה היתרונות / חסרונות של כל גישה?)
  • יש כאן משמעות עתידית דיי גדולה, בכך ש EntityHub מצביע לשאלון: במקום להחזיק “רשימה של דפים” עם ולהיות מוגבל ליכולות שלהן, כל Entity (רכב, מבנה, וכל אלו שעוד יגיעו) – יזכה לשאלון עם כל היכולות שיהיו קיימות. אלו עשויות להיות יכולות ברמת מבנה הנתונים (למשל: מוסיפים ל Questionnaire שם או צלמית ייחודית שמופיעה ב UI) או ברמת ההתנהגות (יכולת “לאפס” שאלון, יכולת לשנות באופן אקראי את סדר הדפים בשאלון) – שעכשיו יהיו תקפים לשאלון של רכבים / מבנים מעצם היותו של סט השאלות “שאלון” ולא “רשימה של דפים”.
  • (יש איזו מידה של עצמאות/כוח שאנחנו נותנים לשאלות של Entities. האם זה דבר טוב או מזיק? מה המשמעות של זה?)

השתתפות הקהל

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

Google Analyhtics טוען שכל פוסט בבלוג נקרא ע”י בין 500 ל 2,000 קוראים (בד”כ). הייתי שמח לבדוק אם אלו בוטים או אנשים אמיתיים 😉.

עוד כשבועיים (סוף נובמבר) – אפרסם את הניתוח שלי על הדברים. אתם תראו שהוא מאוד תלוי סיטואציה, אין פה באמת “נכון” או “לא נכון” – אלא בעיקר “מתאים לסיטואציה X” ו”מתאים לסיטואציה Y”.

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

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

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

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

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

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

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

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

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

Semantic Versioning

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

סיכום

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

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

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

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

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

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

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

Design by Example II

אני רוצה להמשיך ולדבר על בעיות Design לדוגמה, בתקווה שהן מעניינות מספיק. הבעיה שבחרתי הפעם: Rate Limiter.

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

  • היכן יישב המנגנון של ה Rate Limiting? בתוך המערכת – או כרכיב נוסף?
  • איך המנגנון יעבוד בפועל? איך יוודא שה Rate נשמר?
  • האם אנחנו צריכים כלים נוספים (Queue, Database, וכו’) – על מנת לספק את המנגנון?

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

מה שאנו נוטים לשכוח…

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

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


הספר ?Are your lights on מספר על מגדל “ברונטוזרוס”, מגדל משרדים לתעשיית הפיננסים – שבו הלקוחות מתלוננים על תור ארוך למעליות. הוא מציע מספר פתרונות:

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

מה דעתכם? איזה פתרון אתם מעדיפים? מה נראה לכם הכי הגיוני?

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

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

  • קביעת השכירות כפרמטר של מספר העובדים של כל שוכר – וכך צמצום מספר העובדים בבניין. כלומר: יהיה משתלם יותר לחברות עם פחות עובדים / פחות צפיפות לשכור. זה עשוי להשפיע גם על תחזוקה / חניה / עלויות אחרות.
  • יציאה בקמפיין שמבליט את המגניבות בלעבוד ב”מגדל ברונטוזרוס” – כך שהתורים במעליות יתקבלו ביתר הבנה: “זו זכות לעבוד בכזה מגדל, תראה כמה אנשים רוצים את זה”.
  • גיוון בתחומי העיסוק של השוכרים בבניין. אולי כל אנשי הפיננסים מתחילים לעבוד בין 7:00 ל 7:30, אבל ההגעה לחברות הייטק – עשויה להתפזר על טווח רחב יותר של שעות (8:00 עד 10:00) ולהוריד את העומס?
  • למכור את הבניין, ולהתמקד במומחיות של החברה – בנייה ושיווק נדל”ן (ולא ניהול נכסים).

למה לא חשבנו על הפתרונות האלו ברשימה הראשונה שהכנו? אולי כמה מהם טובים, לבעל העניין, (ואולי גם לאחרים) הרבה יותר מפתרונות “הנדסיים”?

נסו לחשוב, מה מוביל אותנו ל”ריצה” לפתרון ראשון? פתרון שהוא אולי לא הפתרון לבעיה הנכונה.

חזרה ל Rate Limiter

אוקי. אנחנו רוצים להבין טוב יותר את הבעיה (או הבעיות) שבגינן אנחנו רוצים Rate Limiter ל API שלנו.

השאלה החשובה היא השאלה העסקית: מי רוצה את ה Rate Limiter (או כל Design שאנחנו עובדים עליו),ולמה? איך אנחנו יכולים לבנות ביטחון שאנחנו פותרים את הבעיה הנכונה?

  • בווריאציה אחת נתכנן Rate Limiter שנועד להגן על המערכת שלנו בפני עומס. צד שלישי קורא למערכת שלנו בקצב גבוה (נאמר: אלפי פעמים בשעה), ו/או כל קריאה של הצד השלישי דורשת מהמערכת שלנו כמות נכבדת של עבודה – כך שמעבר לקצב מסוים, המערכת שלנו תגיב לאט מדי לכלל השימושים ואולי תקרוס.
    • להזכיר, גם כאשר יש auto-scaling – הוא לא מיידי. Peak גדול שמגיע ברגע אחד, יכול לגרום לאי יכולת לספק את הבקשות לזמן מה עד שהמערכת גדלה מספיק.
    • נניח שהצד השלישי הוא חלק משני בביזנס. כלומר: אנו מעדיפים לא לתת לו שירות על מנת להגן על המערכת / הביזנס העיקרי.
  • בווריאציה השנייה, אנו רוצים להגן על הלקוחות שלנו מפני עלויות-יתר. הם משלמים לנו על כל קריאת API אבל מצפים שנעזור להם לנהל את העלויות. אם לקוח לא רוצה להוציא יותר מ $5000 בשבוע – אנו נאפשר לו את זה ע”י דחיית בקשות שחורגות מהתקציב.

איטרציה ראשונה: Rate Limiter שמגן על המערכת

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


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

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

האם אתם מזהים כבר את הכשל הלוגי? הנה כמה:

  • נניח שהמערכת יכולה לעמוד בעומס של 60,000 בשעה. הוספה של לקוח נוסף תאפשר 100,000 בקשות בשעה.
  • כמה מהעומס מגיע מפנים המערכת? אולי בעצם השימוש העיקרי של המערכת כבר משתמש ב 20,000-30,000 בקשות בשעה? ואז – 50,000 בקשות נוספות כבר יביאו לקריסה.
  • כמה המספר הזה יציב לאורך זמן? מי יידע לעדכן אותו כאשר שינוי במערכת משנה את הקיבולת הזמינה (להעלות או להוריד)?
  • הכי גרוע אולי, אם מגיע לקוח שעושה מיליון בקשות בשעה – כמה זמן ייקח עד שייחסם?
    • 60,000 בקשות בשעה הן 1,000 בדקה, ו 16 בשנייה. מיליון בקשות בשעה, בממוצע, הן 270 בשנייה – הרבה יותר ממה שהמערכת יכולה לשאת.
    • מה אם מותר לעשות 50,000 בשעה – אבל לקוח בוחר לעשות את כל הבקשות שלו במשך שלוש דקות? התוצאה היא זהה.

זה אגב מה שקרה לנו אז: שעתיים אחרי ש API נפתח לצד השלישי – כל המערכת קרסה. ה Rate Limiter, מומש ונבדק – אבל לא עשה את העבודה. הצד השלישי ידע שלאחר 50,000 קריאות בשעה הוא ייחסם, אבל לא דאג להגבלות מצדו (הוא היה במוד ניסוי). מכיוון שהפנה בקשות בקצב גבוה מאוד, המערכת קרסה תוך דקות, “רק” אחרי כ 10,000 בקשות.

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


בואו נתחיל לחשוב על התכנון. הנה כמה שאלות שהעלתי בתחילת הפוסט:

  • היכן יישב המנגנון של ה Rate Limiting? בתוך המערכת – או כרכיב נוסף? – בהבנת הבעיה נראה לי שאני רוצה את ה Rate Limiting כרכיב נוסף. הוא אמור להגן על המערכת, ושייפול אם צריך – ויגן על המערכת.
    • יתרון נוסף – אוכל להשתמש ב Rate Limiter גם עבור שרתים (מיקרו-שירותים) אחרים. יש לי כלי לשימוש חוזר.
    • חסרון – עלויות. ישנם מקרים ספציפיים בהם זה שיקול מספיק לחבר את ה rate limiter לקוד השרת – אבל אנחנו לא שם.
  • איך המנגנון יעבוד בפועל? איך יוודא שה Rate נשמר?
    • דבר ראשון שברור שהרזולוציה צריכה להיות קטנה. תלוי מאוד אופי הבקשות, אורכן, תדירותן וכו’ – אך כברירת מחדל אני אלך על היחידה הבטוחה והפשוטה: שנייה. הגבלת קריאות בשנייה.
    • מכיוון שמה שחשוב לי הוא להגן על המערכת בפני קריסה – חשוב לי שהיה מונה (counter) מרכזי שידחה בקשות בכלל. בנוסף נניח שאנחנו רוצים גם מונה לכל לקוח – לייצר שוויון מסוים. נניח: 1,000 קריאות בשנייה בכלל, ו 200 לכל לקוח. נניח שיש כ 20-30 לקוחות. המספרים המדויקים פחות חשובים לתכנון, אבל קל יותר לדבר ולהתייחס למספר נתון.
      • המקסימום למונה הכללי צריך לנבוע מתוך יכולות המערכת. מספר שנקבע יהיה כלל אצבע שלא יתחשב בשינויים במערכת. לכן, היה עדיף למשל למדוד את ה CPU של השרת הפנימי ולהחליט לפיו (או בעצם: ממוצע CPU כי כנראה יש כמה instances וגם המספר שלהם עשוי להשתנות דינאמית). בכל מקרה, אנחנו באיטרציה ראשונה – ולא נכון לצלול לזה כרגע. נרשום את המחשבה בצד.
    • “האלגוריתם” עצמו לספירה, נשמע לי דיי פשוט ואתאר אותו מיד בהמשך.
  • האם אנחנו צריכים כלים נוספים (Queue, Database, וכו’) – על מנת לספק את המנגנון? האמת שתחושת הבטן שלי אומרת Redis (מנוע מבני-נתונים / אולי בסיס נתונים K/V – מבוסס זיכרון. כלי שכדאי להכיר). אני מניח על קצב גבוה כלשהו ובכל מקרה ברור לי שה Rate Limtier צריך כמה מופעים עבור High Availability (לא נרצה שנפילה שלו תפיל את השירות לצד-שלישי) – ורדיס הוא רכיב מרכזי.
    • חשוב בדזיין לפשט, ואולי אנחנו לא זקוקים לרדיס (בקצבים לא גבוהים, גם בסיס נתונים רגיל יכול לעשות את העבודה). תחושת הבטן שלי אומרת לי שכנראה שנרצה, וקל לי אישית לחשוב על הפתרון כך – ולכן אני מתחיל איתו. בסבבים מאוחרים יותר, אולי אמצא דרך לחתוך אותו החוצה, אבל אני הולך כרגע עם מה שהכי פשוט לי לעבוד איתו – ולהתקדם לנושאים נוספים.

הנה תרשים:

  • בקשה מתקבלת ע”י ה Rate Limiter ואני בודק את המונה המרכזי (global counter). אם חרגתי ממנו – אני יכול ישר לקפוץ ל 4b ולדחות את הבקשה.
  • את המונים אני יכול לתחזק ב Redis כ key/value, כאשר ה key הוא שנייה ב Epoch (עבור המונה הגלובאלי) או שילוב של לקוח (שאני מזהה ע”פ ה auth token) + שנייה ב Epoch.
    • ל Keys אקבע expiration של 10 שניות, למשל – כדי שהזיכרון של רדיס לא יתמלא.
    • הנחת יסוד שלי הוא שהשרתים מסונכרנים בשעונים. זו הנחה סבירה לחלוטין בתשתיות הענן הקיימות, ובאמזון, אם זכור לי נכון – אפשר להניח על סטייה מירבית של 1 ms לשרתים באותו Region – מה שלא בעיה עבורנו.
      • אפשר גם לקבוע cloudWatch alert על חריגה בשעונים של שרתים. לא יודע אם הייתי זוכר את זה בסשן Design ו/או ראיון – בטח לא היה לי זמן לחפש בגוגל ולאמת.
    • מי שמכיר היטב את נושא הזמן בטח חושב שיש כאן חור (קטן) בתכנון בגלל Leap Seconds. אני יכול להרגיע ש Epoch מתעלם מ leap seconds – ובכל מקרה זה גליץ’ מספיק קטן ונדיר להתעלם ממנו.

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

  • ההחלטה השרירותית כמעט לחסום לקוח יחיד ב 200 קריאות לשנייה אולי בעייתית. תלוי בביזנס – אולי לקוחות פונים מעט וב bursts – ואז חשוב לתת להם את מלוא הקיבולת האפשרית (1000 קריאות בשנייה?). בכל מקרה, מקום לשיפור האלגוריתם ובכל מקרה לא נשמע לי showstopper.
  • עלויות של חומרה: עוד 2-3 שרתים + Redis זה עלות לא מבוטלת. האם היא נדרשת?
    • צעד מידי ופשוט לצמצום עלויות יהיה לממש את ה Rate Limiter על גבי AWS Lambda (או מקבילה בענן אחר). נשמע כמו צעד מתבקש.
    • הנה שיפרתי משהו משמעותי מתוך ביקורת עצמית מובנית 😀
  • ה Scale דיי טוב. ככלל אצבע Redis יכול לטפל ב 100,000 בקשות בשנייה. אפשר לנהל דיון על scales אדירים – אבל נשאיר את זה בצד, עד שיוכח אחרת. במקרה הכי גרוע ה Rate Limiter “קורס” – אך המערכת עדיין מוגנת.
  • עקרונות תוכנה: SOLID / other practices:
    • No single point of failure – לרדיס אין redundancy, אם הוא ייפול (ויעבור מיד restart) – יהיו 10-30 שניות של downtime (הנחה) ומידע שנעלם מהזיכרון בכל מקרה יהיה כבר לא רלוונטי (מונים של שניות קודמות).
  • ייתכן ובאמת החסימה של תעבורה לכל לקוח היא מעבר למינימום ההתחלתי ויכולנו להתחיל עם מונה מרכזי וזהו. תודה לטל רום על ההערה.

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

איטרציה ראשונה: Rate Limiter לניהול עלויות של לקוחות

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

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

  • היכן יישב המנגנון של ה Rate Limiting? בתוך המערכת – או כרכיב נוסף? – במקרה הזה נשמע שעדיף לשבת בתוך המערכת. מדוע?
    • אולי יש מספר קריאות API שמפעילים את אותה פונקציונליות שאנו רוצים לגבות עליה. מנגנון הגבייה/מעקב לא יצטרך לעקוב אחרי הדרכים השונות להגיע ללב הלוגיקה (כלומר: API1,2,3,4 דורשים חיוב)
    • אם יש תקלה ולא הצלחנו לסיים את הפעולה (Exception) – כנראה שלא נכון לחייב. כלומר – אם החיוב היה ב gateway/proxy כמו במקרה הקודם, היה עליו לפעול רק בעת מתן תשובה תקנית.
    • פחות רכיבים במערכת. ייתכן ויש לנו מערכת גבייה (“commission service” או איך שלא ייקרא) – ואז הגיוני לשים את ה Rate Limiter שם. זה עדיין בתוך המערכת – רק במקום הנכון יותר.
  • איך המנגנון יעבוד בפועל? איך יוודא שה Rate נשמר?
    • רדיס כבר לא טוב לנו. מדובר בגבייה של כסף שזה עניין מדויק שאסורים בו פספוסים, ורדיס ש”נפל” ואיבד את כל המונים שלנו – גם אינו מצב סביר.
      • סביר יותר להתחיל בסיס נתוני (רלציוני, או מה שמהערכת עובדת בה) ומשם להמשיך.
    • המנגנון עשוי להיות דיי דומה, נניח Key/Value כאשר המפתח הוא יום Epoch + זיהוי הלקוח. כל זאת כל עוד איפוס המונה ב UTC הוא הגיוני. ייתכן ולא – ואולי יש צורך לנהל את ה offset של אזור הזמן לכל לקוח / או מדיניות אחרת כלשהי.
    • מה עם לקוח שלא השתמש בחיוב שלו במשך כ 3 ימים – וביום הרביעי יש לו חיוב של 125%? האם אנחנו רוצים לאזן מקרים כאלו איכשהו – או להישאר במסגרת נוקשה של יום?
      • בגלל שאנחנו באיטרציה ראשונה – נתחיל ב”מסגרת” נוקשה של יום ונדון בהרחבות אפשריות רק בהמשך.
  • האם אנחנו צריכים כלים נוספים (Queue, Database, וכו’) – על מנת לספק את המנגנון?
    • הזכרנו כבר בסיס נתונים, כי מידע צריך להשתמש persistent. אני לא רואה יותר מזה כרגע.

הנה תרשים שמתאר את המבנה:

  • אני מדמיין מבנה קיים כלשהו (בשחור), התוספות הן בכחול.
  • קודם כל, לפני הפעלת פעולה עסקית יש לבדוק האם ניתן לחייב (שלב 1). הוא יגיע ל Rate Limiter והוא מצדו יבדוק את המונה בבסיס הנתונים. הסכמה יכולה להיות key/value פשוט.
  • רק כאשר הפעולה הסתיימה בהצלחה (אני מניח? – תלוי איך המערכת עובדת), מבצעים פעולת charge שאותה נוסיף לחיוב ה counter.
    • אפשר לשאול: “מה יקרה אם יהיה exception ב increase counter? זו לא פעולה אטומית?” – התשובה היא שזה כנראה באג והתשובה כנראה מאוד תלויה במערכת הספציפית ואיך היא עובדת. נשמע לי לא רציני “לדמיין” כאלו פרטים בלי שניתן לבדוק. באופן כללי מכיוון שזה אותו בסיס נתונים – ניתן לצרף טרנזקציות (אם בסיס הנתונים תומך) ולהיות חלק מאותה פעולה אטומית בשלבים 2.1 ו 2.2.

לאיטרציה ראשונה זה נראה לי מספיק טוב. בואו נחשוב מה נקודות החולשה העיקריות / מה יכול “להכשיל” את התכנון הזה:

  • יכולים להיות מגוון דרישות כיצד לחשב את ה Limit של כל לקוח. כל דרישה – עם פתרונות אפשריים משלה. יש מגוון אלגוריתמים ל Rate Limiting בניתן למצוא באינטרנט. העניין הוא לספק צרכים ספציפיים – ובאופן הפשוט ביותר שניתן. אין טעם להשתמש באלגוריתם מורכב ממה שנדרש – זו לא הנדסה טובה.
  • מה קורה במידה ויש לנו Scale אדיר, שבסיס נתונים יחיד לא יוכל לעמוד בו? אולי – ה latency של בדיקת המונה (קריאה לבסיס נתונים ע”פ מפתח היא לרוב פעולה של מילי-שניות בודדות) יקרה מדי?
    • בסיס נתונים רלציוני לרוב יכול לטפל בכמה אלפי בקשות פשוטות בשנייה. אם צריך יותר ניתן לנהל אולי 2 רמות של עדכונים: אחת ברדיס מהירה, והשנייה מתוזמנת ומסנכרנת, נניח כל דקה, את הנתונים מרדיס לבסיס הנתונים. כאן נוצר הסיכון שלקוח שחרג מחשבונו ייחסם רק דקה מאוחר יותר (במקרה הגרוע). האם זו פשרה סבירה שניתן לקחת? אולי החברה יכולה לספוג סטיות כאלו – בכדי להשאיר את המערכת יחסית פשוטה?
    • המקרה של latency הוא דיי דומה. אפשר לנהל שכבר מהירה ושכבה איטית. אפשר להפוך את פעולות העדכון לאסינכרוניות ואפשר להחזיק cache מקומי על המונה וכאשר הוא מתקרב לקצה (נניח: 95%) רק אז להחיל בדיקה בכל קריאה. עוד אופציה חלופית לזו היא לנהל “כרטיסיות” של גישות כאשר שרת מבקש מבסיס הנתונים בקשה ל 100 בקשות ואותן הוא מנהל בזיכרון ורק כל 100 קריאות פונה לבסיס הנתונים. יש כאן מספר מקרי קצה שדורשים טיפול – גישה אפשרית אך לא מאוד פשוטה.
      • בכל מקרה נראה שצמצום latency ו/או הרחבת scale יגיעו עם tradeoff לגבי הדיוק של האכיפה. חשוב לזכור שהרבה מורכבות טכנית יכולה להיות מומרת בגמישות עסקית, למשל הסכמה לחריגה של לקוחות בכמה אחוזים מההקצאה היומית שלהם – על חשבון החברה.
  • מכיוון שמדובר בכסף, חשוב לבדוק את מקרי-הקצה השונים ולראות שאין פספוסים משמעותיים. אני לא חושב כרגע על בעייה עקרונית, אך הייתי משאיר את העניין כנקודה לבחינה.

סיכום

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

  • תהליך בניית תוכנה (גם תכנון וגם קידוד) הוא תהליך של גילוי, חשיבה, ולמידה מתמשכים. זו טעות לקפוץ למסקנות מהירות, להחליט מוקדם ולהתקבע על כיוונים ופתרונות. לסגור אופציות. בטח שגם לממש דברים שלא ברור לנו שהם נחוצים.
    • כמעט כל “הפתרונות” שסקרתי בווב (בעיקר עבור כתיבת הפוסט הקודם: URL Shortener) היו כאלו: החלטיים, “יודעים”, ומציגים תמונה של פתרון כאילו זה תרגיל בטריגונומטריה, עם התחלה וסוף חד-משמעיים. זה לא עובד כך באמת.
  • אם היה תכנון “מיטבי” (מלה שדיי צורמת לי, אגב, כשאני שומע אותה בהקשרים מקצועיים) לסגנון של בעיות: Cache, URL Shortner, או Rate Limiter – אז היה כבר Open Source אחד לכל בעיה שמשתמשים בו ודי.
    • אני מקווה שהצלחתי להדגים כיצד כל מצב עסקי מעט שונה – יהנה מתכנון שהוא קצת או הרבה שונה. ששתי הבעיות בפוסט – באמת דורשות פתרונות דיי שונים.
    • ברור שלפעמים נכון להתפשר, ולקחת פתרון קצת פחות מתאים – כי הוא זמין, אמין, או קל. אני עדיין בעד שימוש חוזר בשירותים וספריות (קוד פתוח, או בכלל).
  • טעות נפוצה שלישית היא צלילה לפרטים לפני בניית “Walking Skeleton” (קרי “שלד מהלך”). במקום לצלול לפרטים ובעיקר למימוש של סכמת בסיס-הנתונים (לרוב דומיין ידוע, שילמד אותנו מעט על התכנון והצרכים, אבל יקשה על שינויים מהותיים בתכנון) – חשוב יותר להרכיב “Flow” עובד מקצה לקצה (להלן: שלד) וגם לאמת אותו עם העולם – למשל להריץ עליו כמה בדיקות או אפילו תעבורה אמיתית של המערכת (להלן: מהלך).

אני מקווה שהצלחתי במשימה.

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