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

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

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

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

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

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

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

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

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

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

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

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

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

לופ שלא נגמר.

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

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

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

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

YAGNI מול Design Patterns

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

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

SOA מול RPC

REST מול SOA

newer RPC (Thrift/gRPC) מול REST

GraphQL מול newer RPC

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

אז מה קורה שם?

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

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

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

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

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

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

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

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

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

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

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

מה עדיף?

הפתרון

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

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

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

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

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

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

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

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

אבל:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • המודל, ומכאן סכמת בסיס הנתונים משתנה מדי-פעם.
  • שכבות אפליקציה מטפלות בנתונים (בכתיבה, אבל גם בקריאה), ומפעילות לוגיקה-עסקית עליהם – שגם היא משתנה.
  • דרישות צד-הלקוח משתנות לאורך זמן.
גישה ישירה מצד-הלקוח לבסיס-הנתונים עשויה להתגלות ככדור (תותח) ברגל. הפיתוח יתקדם מהר, אבל אז:
  • יתגלה באג שלא ברור מדוע הוא מתרחש: מישהו הוסיף קוד אפליקטיבי שאינו חל על הקריאה הישירה מבסיס הנתונים.
  • שינוי פשוט לכאורה, נניח: שינוי במודל שדורש שינוי בסכמה של בסיס הנתונים – הופך לשינוי קשה, ארוך, ובעל הזדמנויות רבות לתקלות.
    • יש כאן גם חוסר-הוגנות ארגוני פוטנציאלי: מפתח אחד אפשר וחסך זמן בפיתוח הפיצ'ר שהוא אחראי אליו, בעוד מפתח אחר נאלץ לשלם את המחיר היקר שנגרר מאותו אפשור.
    • נתקלתי במצבים כאלו פעמים רבות – והם לא מסייעים לתחושת השותפות והידידות בארגון, במיוחד אם צוות אחד מאפשר תדיר על חשבון אותו צוות אחר שמשלם את החובות-הטכניים של אותו האפשור.
ריסון הוא הרצון להגן על המערכת ע"י הצבת חסמים מלאכותיים. במילה "מלאכותיים" הכוונה היא שיש דרך לבצע את הפעולה באופן קל יותר – אך אנחנו יוצרים חוקים / מגבלות פיסיות שלא יאפשרו זאת.
דוגמאות:
  • השימוש ב private על members בפרדיגמת ה OO – הוא ריסון ברמת המיקרו. למעבד אין בעיה לגשת לכתובת הזו בזיכרון – אבל אנחנו מורים לו לא לעשות זאת (ומשלמים תקורה בביצועי התוכנה / זמן כתיבת הקוד).
  • כלים לניהול תלויות בין מודולים / מיקרו-שירותים או נהלים להגבלת ספריות ה open source שנכנסות למערכת: אין למתכנת בעיה להוריד כל ספריה ולהתשמש בה – אבל הארגון רוצה לבדוק את הספרייה מבחינת אמינות / תמיכה / כפילות / רשיונות / היבטי אבטחה – לפני שזה נעשה.
  • קונבנציות של קוד – הן ריסון. הקומפיילר יקבל סגנונות שונים ומשונים – אבל הארגון מחליט שבנקודות מסוימות הוא מקבל סגנון רק מסוים.
  • לפעמים גם בחירה של שפת-תכנות היא ריסון. שפת Go מחייבת מבנה אחיד ופשוט הרבה יותר משפה משופעת באפשרויות כמו רובי. מעבר מרובי ל Go – הוא ריסון ניכר.
דוגמה קלאסית לריסון-יתר הוא Closed Layered Architecture מרובת שכבות. בכדי להתגונן בפני בעיות שונות בכתיבת מערכת, הומצא מודל השכבות בו כל שכבה יכולה לקרוא רק לשכבות מתחתיה. בגרסה ה"סגורה" של המודל, כל שכבה יכולה לקרוא רק לקוד באותה השכבה או בקוד בשכבה אחת מתחתיה:
כלומר: אם קוד בשכבה 4 רוצה לקבל נתונים משכבה 2 עליו:
  • לקרוא לשכבה 3 ולבקש את המידע.
  • שכבה 3 תקרא לשכבה 2 ותבקש את המידע.
  • המידע חוזר במבנה הנתונים ששכבה 2 מכירה (אך שכבה 3 יכולה להשתמש בה).
  • שכבה 3 לא יכולה להעביר את מבנה הנתונים של שכבה 2 לשכבה 4 (אסור!) ולכן היא מגדירה מבנה נתונים משלה למידע שאותו היא חושפת בפני שכבה 4, לאחר המרה, כמובן.
ייעצתי פעם לארגון שעבד עם מודל של 6 שכבות סגורות. המערכת לא הייתה עשירה כ"כ ב business logic ורובה ביצעה אינטגרציה בין מערכות.
מידע רב עבר בין שכבה 2 לשכבה 6 – ורוב הקוד בשכבות 3, 4, ו 5 פשוט תרגום שוב ושוב קריאות, הלוך ושוב, למבנים שקולים – אך שונים. חשיפת נתונים בין שכבה 2 לשכבה 6 דרשה כמות נכבדת של קוד? על מה?!
אני בטוח שמישהו הגיע עם כוונה טובה. הוא קרא מאמר או שמע הרצאה על מערכת שעבדה ללא סדר / שכבות – והפכה לסיוט לתחזוקה. אני מדמיין שהוא החליט לא ליפול לפח ולעשות את "הטוב ביותר". לא רק 3 שכבות – אלא 6! לא רק שכבות – אלא שכבות סגורות. Crème de la Crème!
הנזק כאן כמובן היה אדיר,שעות מפתחים ושחיקה רבה ממשימות בסיסיות. אני מקווה שהחברה בסוף יישמה את הרעיון הפשוט של לעבור למודל פתוח או לפחות פתוח-למחצה.
נ.ב. האוונגליסטית לשינוי הייתה דווקא מישהי שרק התמנתה לתפקיד ארכיטקטית. לארכיטקטים הותיקים והמנוסים בחברה היה blind spot למצב, ולאבסורד שבו.

סיכום

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

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

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

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

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

איך לנצח את הסיבוכיות?

בתואר שני במדעי המחשב, היה קורס חובה בשם ״סיבוכיות״. עסקו בו במושגים כמו DTime, NP-Hard, NP-Complete או PSPACE. בעצם, סיווגו בעיות קשות מדי לקטוגריות ואבחנו אבחנות שונות לגביהן.

זו בהחלט גישה של מדעני תוכנה. תאורטית.
מהנדסי תוכנה אמורים לזהות מהר כשמשהו מסובך מדי – ו״לשבור״ לפתרון פשוט יותר (עם ה tradeoffs האפשריים). זה לא דבר שנלמד כמעט בשום אוניברסיטה (מלבד CS190 באוניברסיטת סטנפורד, אולי?) – אבל זו בהחלט התמודדות יומיומית חשובה של מהנדסי תוכנה. ראוי שזה יהיה קורס חובה לכל בוגר מדעי המחשב (בתואר ראשון) — וראוי בהחלט לפוסט!
סיבוכיות בתוכנה היא ריקבון. היא מזיקה – והיא נוטה, באופן טבעי, רק לגדול ולהחמיר.
באופן אירוני חלק מאנשי התוכנה גאים בה, ומקדמים סיבוכיות – וזה דבר שראוי שיפסק. לא פעם ראיתי מהנדסים שנפעמים מהקוד הסופר-מסובך שכתבו, כראיה לאינטלגנציה או יכולת גבוהה (לכאורה) שהם בורכו בה. בפועל הם חוגגים משהו שממש מזיק למערכת, וחשוב להעמיד דברים במקומם. זה התפקיד של כולנו.
יש לבני-אדם נטייה טבעית (?) להתפעם מהמסובך, מהבלתי-מובן – וכתיבת קוד מסובך היא דרך מוכחת של אנשי-תוכנה להרשים אנשים אחרים. בכדי להצליח לכתוב תוכנה טובה, באופן מקצועי – מובילי הדעה בארגון צריכים לקדם תרבות המאדירה קוד הפשוט – ומדירה קוד המסובך. זה לא יקרה מעצמו.
הגאווה בקוד שצריך לגרד בראש ולקרוא אותו כמה פעמים בכדי להבין אותו – צריכה להיכחד. היא מזיקה לארגון ומזיקה למערכת.
בתוכנה יש שני סוגי סיבוכיות: סיבוכיות נחוצה (essential complexity) וסיבוכיות מקרית (accidental complexity). את הראשונה ראוי למתן (אפרט בהמשך) ואת השנייה יש להכחיד.
Generics, למשל, עלולים בקלות להוסיף סיבוכיות, ויש מקום להעריך את מי שמצליח להימנע משימוש בהם. אני בטוח שזה counter-intuitive למפתחים צעירים.

זיהוי סיבוכיות

שלב ראשון בטיפול בסיבוכיות – הוא הזיהוי שלה.
סיבוכיות היא כל מה שמקשה על הבנה או שינוי של קוד.
  • אלגוריתם מורכב = סיבוכיות
  • ריבוי תלויות = סיבוכיות
  • קוד המפוזר באזורים שונים במערכת = סיבוכיות
  • קוד בלתי צפוי (פונקציות ״calc״ שמעדכנת ערך של האובייקט בבסיס הנתונים) = סיבוכיות.
  • שימוש בסגנון / קונבנציות / ספריות לא מקובלות = סיבוכיות
  • יישום Design Pattern מדהים היכן שאינו באמת נדרש = סיבוכיות
  • וכדומה….
קוד שקשה (/מסוכן) לשנות אותו – הוא מסובך. קוד שקל לשנות אותו – הוא פשוט.
שאלה: כיצד ניתן להוסיף קוד, לקוד מסובך – ולפשט אותו?
תשובה ראשונה: בדיקות יחידה מפשטות את הקוד. הן מכריחות קוד (אם נכתבו נכון) להיות מודולורי יותר והן מבארות את הקוד, מוסיפות ביטחון לשינויים בו. הוספנו קוד – אך פישטנו אותו.
תשובה שנייה: קוד קצר ומחוכם (clever) הוא הרבה פעמים מסובך יותר מקוד ארוך יותר אך אינטואיטיבי וקל להבנה. פחות שורות קוד הן לא תמיד מדד לפשטות של תוכנה.
כלים, כמו IDE, עוזרים לפשט את הקוד (במעט).
העיקרון האובייקטיבי לסיבוכיות של קוד
אם דיי אנשים חושבים שהקוד שלכם הוא מסובך – אז הוא מסובך.
לא תמיד אנשים יאמרו לכם במפורש ״זה קוד מסובך וקשה לעקוב אחריו״. לפעמים הביטוי הוא אנשים שלא מפסיקים לשאול שאלות על הקוד ומה מתרחש בו. ביטוי אחר הוא שאנשים פעם אחר פעם עושים שינויים לא הגיוניים בקוד ו/או לא משתמשים בו נכון.
זו נקודה שלא תמיד הצלחתי להעביר בקלות, לכותבי הקוד המסובך.
״אבל אני מבין אותו״, ״צריך לעבוד איתו כמה שבועות ואז זה נהיה מובן״, ״אני לא מסכים – לי הוא נראה פשוט״ — הם טיעונים חלשים.
יש אנשים מאוד אינטליגנטים שקל להם לנווט בקוד מסובך, במיוחד קוד שלהם – ולכן קשה להם להכיר במסובכות של הקוד. כאן יש מקום גדול לקהילה מסביב לתת את הדין אם הקוד פשוט או לא.
הקוד הוא לא אישי. לא מדובר באופן שאני רוצה לסדר את חדר השינה (הפרטי) שלי. הקוד הוא חלק ממערכת וארגון – וחשוב מאוד שהוא נגיש לכלל האנשים. ״אבל אני מבין את הקוד״ – הוא פשוט טיעון לא מספיק טוב!
קיים גם הטיעון: ״זה נראה כמו סינית, אבל אם תלמד סינית – זה היה פשוט״ ואני מקבל אותו, לפעמים. לפעמים חשוב להבין את הדומיין / ההקשר בכדי לקרוא קוד, ואז קוד שנראה מסובך – עשוי להפוך לפשוט.
האם באמת הקוד קשה להבנה שינוי בגלל הבנה עמוקה שנדרשת בדומיין – או גם מישהו שמבין את הדומיין עלול להסתבך איתו? האם מתכנת מנוסה שמכיר את הדומיין צריך לקרוא את הקוד יותר מפעם אחת בכדי להבין אותו?
Code Review ו/או Code Inspection הם תהליכים יעילים לאיתור סיבוכיות בקוד.
זיהוי סיבוכיות בקוד לאחר מעשה
לפעמים אנו מבינים סיבוכיות של קוד רק תוך כדי עבודה על שינוי:
  • שינוי קונספטואלי אחד (״מעתה הרשה לפעולה להתרחש 3 פעמים, ולא רק 2״) – דורש שינויים במספר (גדול) של מקומות בקוד.
    • השאיפה תמיד בקוד היא ששינוי קונספטואלי אחד – ידרוש שינוי מאזור ממוקד אחד בקוד.
  • Design Weight – הקוד גורם לנו לעומס קוגניטיבי ומאלץ אותנו זכור ולקשר נקודות, מידע שקשה ״להחזיק״ בראש מבלי לאבד פרטים ולהזדקק לחזור ולקרוא בקוד. קוד מסובך הוא כמו ספר בפיסיקה, שכל פעם צריך לחזור לפרק הקודם ולהיזכר ולהבין מחדש במה בדיוק מדובר. קוד פשוט הוא כמו עיתון שניתן בכל רגע לקפוץ לכל עמוד ופסקה – ובקלות להבין במה מדובר.
  • תוך כדי שינוי שנראה פשוט בתחילה, אנו מגלים עוד ועוד נקודות שיש לקחת בחשבון. לעולם איננו יודעים בבטחון אם סיימנו 50% עבודה או רק 10% עבודה. יותר גרוע: כאשר ככל שאנחנו מתקדמים, הבטחון שלנו מתי וכיצד יראה הסוף – הולכים ומתרחקים.
קשה לי להאמין שיש מפתח שלא מכיר את הסימפטומים הנ״ל.
פישוט קוד, והימנעות מסיבוכיות מיותרת היא מלאכה חשובה, מעשית, ונדרשת – ובעצם מה שמבדיל בין אנשי תוכנה טובים – למצוינים.
בספר “A Philosophy of Software Design”, שהוא הטריגר לפוסט (מזמן רציתי לכתוב על הנושא, אבל הייתי זקוק לטריגר שכזה) – וגם חלק מהפוסט מבוסס עליו – מאפיינים דמות בשם ״הוריקן טקטי״ (לקחתי לי חופש תרגום מ Tactical Tornado) של מפתח החולף על פני המערכת, תוך כדי שהוא מספק פיצ׳רים במהירות, אבל פיצ׳רים שסגורים רק ב 80% – ובסופו של דבר דורשים עוד עבודה ו/או מסבכים את המערכת. המנהלים לעתים רואים אותם כגיבורים, ״למה אין לנו עוד מפתחים שמספקים תוצאות כל-כך מהר?!״ – אבל המפתחים שנתקלים בקוד, ועוקבים אחרי הדינמיקה שמתרחשת בפועל – מבינים במה מדובר.
עוד דמות חשובה היא ה״ארמגדון רב-החזון״, מישהו שמאמץ מירב אדיר של טכניקות הנדסיות (Patterns, כלים, טכניקות, ספריות) שאינן מתאימות או נדרשות לבעיה – ומשאיר מערכת סבוכה ללא צורך. הוא לא מהיר – אבל מזיק באותה המידה, ואולי אפילו יותר.
מאוד קשה לאפיין ולמצוא מדדים אובייקטיבים וקלים-לקריאה המעידים עד כמה פשוטה או מורכבת מערכת נתונה. רבים ניסו – בלי הצלחה מרובה, ואין לי לצערי בשורה חדשה. אני יכול רק להציע את Genchi Genbutsu – ללכת לשטח, להיכנס לפרטים, לטעום ולהבין במה מדובר – זו הטכניקה המעשית היחידה שאני מכיר, אם כי היא איננה scalable.

אז מה עושים?

זה החלק הקשה. חפשו קצת באינטרנט – בטח תמצאו. יאללה ביי!
(זה מה שמתחשק לי לומר, אבל מרגיש לי קצת לא אחראי)
—-
לפעמים נדמה שיש מגוון כלים לפשטות של תוכנה – ופשוט צריך ליישם אותם:
קונבנציות, Linting, תהליך Design Review, דפוסי עיצוב או SOLID, וכו׳.
אני לא יודע להעריך כמה שנות אדם (רבות!) בעולם התוכנה מושקעות ביישום טכניקות ל״פשטות מובטחת״. יש מאחוריהן המון רצון וכוונה טובה – אבל הן לרוב לא העיקר. סדר בקוד הוא חשוב, אבל שמירה על קונבנציות היא לרוב רק קצה הקרחון.
אין לי פתרונות קסם, אבל אנסה לציין כמה כללי אצבע מעשיים לשיפור הפשטות בקוד.
צמצום שימוש בכלים מתקדמים
 
הורשה, Threads / co-routines, Generics, ועוד – הם כולם כלים ראויים שיש להם מקום במערכות פשוטות. עדיף לנסות להשתמש בהם בשימוש הבסיסי ביותר: הורשה בעומק של 1, Generics של פרמטר אחד, וכו׳.
פעמים רבות אנשים נוטים להאמין שהקוד יהיה טוב יותר אם נשתמש יותר ב״כלים מתקדמים״, וכך יש שרשראות הורשה ארוכות, Thread המקצרים זמנים בלתי-מדידים (מרוב שהם קטנים), או תסבוכת של טיפוסי Generics.
נסו להשתמש בכלים מתקדמים פחות. השאירו את הכלים המתקדמים כברירה אחרונה – מתי שהברירות האחרות אינן טובות דיין. נסו להשתמש כמה שיותר בכלים פשוטים, ומובנים היטב, שקשה מאוד להסתבך איתם.
האם תאבדו את ההזדמנות להרשים אנשים מסוימים ב״יכולתכם הגבוהות?״ – כנראה שכן. אני מקווה שבארגון שלכם, אלו הם לא האנשים הנכונים להרשים.
פיזור הסיבוכיות
כשיש ביטוי גדול ומסובך בקוד, כדאי לשבור אותו לכמה קטעים קטנים יותר עם הפסקות. תנו לקוראים שלכם לנשום, ולארגן את החשיבה. למשל:
היא פונקציה קצרה וממוקדת, ועוד בסגנון פונקציונלי שנחשב לפשוט וקל יותר לקריאה.
בכל זאת, היא מעמיסה על הקורא – ולא ברור בכלל שכל מפתח מנוסה ״יחזיק״ בראש את משמעות הפונקציה, ויזהה באג פוטנציאלי בקריאה יחידה של הקוד. הייתי אומר אפילו שזה understatement. זה לא קוד טוב, כי הוא סבוך מדי.
מתכנת שנתקל בכזו פונקציה לא צריך לומר ״אני לא חכם מספיק – אעבור את זה ואשתוק״. עליו/עליה לומר: ״זיהיתי קוד מסובך. אפשט אותו (או מקסימום אבקש מכותב הקוד לפשט אותו״. רק כך משפרים את המערכת!
הנה אותו קוד כשהוא ״מפורק״ ליחידות קטנות יותר, עם משתנה בכל פעם שמגדיר את השלב שאליו הגענו:
הוא עדיין לא פשוט לטעמי, אבל הרבה יותר נשלט / נסבל. הוא ראוי לעוד סיבוב של פישוט.
העיקרון הזה נכון בכל רזולוציה: פונקציה, תת flow, או flow: אם המציאות מסובכת, חשוב ליצור נקודות ״עצירה״ להבנה / בקרה / debug של המצב. להפריד מורכבות, ככל שניתן, לפנוקציות ומחלקות משנה – וכך לצמצם את ריכוז הסיבוכיות.
״החוכמה״ בליצור ביטוי קצר, תמציתי, ובלתי קריא – היא ״חכמת טיפשים״.
הכמסה של סיבוכיות
זוכרים שדיברנו על סיבוכיות נחוצה וסיבוכיות מקרית. גם אם נצליח להסיר את כל הסיבוכיות המקרית – עדיין נשאר עם סיבוכיות נחוצה. מה נעשה איתה?
ליבת ביקוע גרעיני היא רכיב מסוכן בתחנת-כח גרעינית. מסוכן – אבל נחוץ. אז מה עושים?
האם ביקרתם פעם בכור גרעיני וראיתם ליבות ביקוע גרעיניות מפוזרות מסביב כמו פחי-אשפה? אני מניח שלא.
את ליבות הביקוע הגרעיניות מבודדים. מקשים את הגישה אליהן. מבקרים אותן היטב.
זה גם מה שנכון לעשות עם הסיבוכיות-הנחוצה של המערכת שלנו – לבודד, להרחיק, ולהגן בפניה.
כלומר:
  • לא מערבבים קוד ״מסובך״ וקוד ״סתם״. את הקוד המסובך מפרידים למחלקות נפרדות, אולי חבילות נפרדות.
  • מתכננים את ה Flow במערכת כך, שיהיו כמה שפחות שינויים נדרשים בקוד המסובך, וכמה שיותר בקוד הפשוט.
  • מציבים תמרורי אזהרה. שמות ורמזים – שיפחיתו את החשק של אנשים לחבר את הקוד שלהם לקוד המסובך.
  • יוצרים ממשקים ברורים ופשוטים – שיפחיתו את הצורך של אנשים להיכנס לקוד המסובך.
  • בודקים את הקוד המסובך ביתר שאת – כך שאם ישנם שינויים — עדיין נוכל לבצע אותם בבטחה.
System Decomposition
באופן כללי, אני מחלקים את המערכת לתתי-רכיבים (components). אנו עושים זו משתי סיבות השלובות זו-בזו:
  • פירוק לרכיבים (decomposition) – המאפשרים ארגון הקוד ביחידות קטנות יותר, בעלות הכמסה, כאשר הפעולה מול הרכיב היא רק דרך ממשק מוגדר היטב.
  • מודולריזציה (modularization) – חלוקת המערכת לרכיבים/מודולים הניתנים לשימוש חוזר ולהחלפה ע״פ הצורך.
אני רוצה להדגיש את המשמעות הראשונה, ולכן אצמד למינוחים ״רכיבים״ ו ״פירוק לרכיבים״.
האופן בו אנחנו מפרקים את המערכת לרכיבים משמעותית מאוד לסיבוכיות שאנו ניצור. הנה שני אופנים אפשריים:
מתוך הספר A Philosophy of Software Design. תודה לתומר רוטשילד שהכיר לי את המטאפורה היפה הזו, ואת הספר.
כאשר אנחנו מחלקים את המערכת שלנו לרכיבים עמוקים, אזי אנו מחביאים הרבה מימוש (Functionality) משאר המערכת מאחורי ממשק רזה. השגנו הכמסה גבוהה – מה שעוזר גם לצמצם קשרים במערכת – ולהגיע למערכת פשוטה יותר. פחות קשרים, יותר הכמסה – זה מצוין לפשטות המערכת.
כאשר אנחנו מחלקים את המערכת שלנו לרכיבים רדודים (הרבה ממשק, כאשר הרכיב עצמו מוסיף מעט מאוד לוגיקה) אזי אנחנו מוסיפים בכל ממשק מעט מאוד ערך. התוצאה תהיה שכל flow יצטרך לעבור דרך הרבה רכיבים (שכל אחד עושה מעט) שמספקים מעט מאוד הכמסה. אנו נדרש לכן גם ליותר קשרים בין רכיבים. המצב הזה כבר לא רחוק מאוד ממצב בו כל הפונקציות וה state במערכת הם גלובאליים – מצב שלכולנו ברור שהוא מאוד לא בריא.
הנה פונקציה לדוגמה שלא מוסיפה הרבה ערך:
זו לא בהכרח פונקציה לא טובה. העניין הוא במאסה. אם המערכת שלנו מלאה ברכיבים / מחלקות המלאות בפונקציות שכל פעם עושות רק מעט – אזי אנו יוצרים סיבוכיות של המערכת. סיבוכיות של ריבוי רכיבים וקשרים.
דוגמה המופיעה בספר היא ממשק קריאת הקובץ בגירסאות המוקדמות של ג׳אווה:
האם מישהו מכם זוכר שהוא אי פעם נדרש לפתוח קובץ ללא BufferedInputStream? או הרכיב Streams אחרים?
מנגנון ה Streams של ג׳אווה מוזכר לעתים רבות כדוגמה מוצלחת למודולריות – היכולת להרכיב רכיבים קטנים זה על גבי זה בכדי להשיג פונקציונליות מורכבת יותר (ברוח הפילוסופיה של יוניקס)
השאלה שעולה היא: כמה מהמודולריות הזו באמת נדרשת בגישה לקובץ? האם פונקציה סטנדרטית אחת שעושה את הכל (רכיב עמוק) – לא הייתה מפשטת את הקוד שלנו יותר?
דווקא ביוניקס, יש ממשק עמוק לגישה לקבצים: הפונקציה ()open מאפשרת בפעולה אחת לפתוח קובץ, מתוך path מורכב שניתן לתאר בשורה אחת, ויש optional flags בכדי לשלוט בכמה חלופות שימושיות (למשל: לפתוח קובץ לקריאה בלבד).
סה״כ נראה שהממשק העמוק של יוניקס, מוביל לקוד פשוט יותר וקריא יותר, מסט הממשקים הרדודים של ג׳אווה.
הכל תלוי כמובן בסט הפעולות הנפוצות – בקריאה מקובץ יש דפוס פשוט ונפוץ, אבל במקומות בהם יש גיוון גדול יותר – דווקא לגישה המודולריות יהיה כנראה יתרון. בכל מקרה, אין מניעה לספק גם וגם: ממשק עמוק ופשוט – למירב הצרכים, וסט של ״כלים מתקדמים״ בדמות ממשקים רדודים ומודולריים – ״להרכבה עצמית״ המספקת צרכים ייחודיים יותר.
היופי של המטאפורה של ״הרכיבים העמוקים/רדודים״ בעיני, היא שהיא מוסיפה מימד מאוד חשוב לחלוקת המערכת לרכיבים, בצורה שקל מאוד לזכור ולאפיין. רבים מאתנו מיישמים את ההמלצה הטובה לחלק את המערכת לרכיבים, ואף רכיבים קטנים – אך האם אנו מחלקים אותה לרכיבים עמוקים או רדודים?
בפוסט שלי על המודל האנמי – ניסיתי בדיוק לתאר את הבעיה ברכיבים רדודים, מבלי שהייתה לי את המטאפורה הזו לשימוש.
ממשקים פשוטים הם חשובים אפילו יותר מקוד פשוט – כי הם מחביאים את המורכבות משאר המערכת.

סיכום

אמרתי זאת פעמים רבות, ואמשיך לומר זאת: בזמן שאנו עסוקים בהתרגשות ב״למידת הספרייה / הטכנולוגיה החדשה והמתקדמת ביותר״ – רוב הסיכויים שאת השיפורים המשמעותיים ביכולות ובתפוקות שלנו נוכל לעשות מהתמקדות בעקרונות בסיסיים, לא נוצצים – אבל מאוד חשובים.
אני מנסה לאזן במעט את תרבותנו המקדשת את ״החדש-ביותר״, תרבות שגוזלת הרבה מזמננו וכוחותינו למקומות לא יעילים.
כולם מסכימים שפשטות היא טובה, אבל מעטים באמת חותרים אליה בצורה יעילה ומעטים אפילו יותר מצליחים להרבות בה. אני מקווה שהפוסט הזה יגע בכמה לבבות בתעשיית התוכנה, ויעזור לנו להתמקד טוב יותר – בעיקר.
לינקים רלוונטיים
סיבוכיות: מקבלים את זה בחינם – פוסט עבר (קצר) בנושא.
Defining Errors Out Of Existence – פוסט של תומר רוטשילד, על רעיון נוסף מהספר ״A Philosophy of Software Design״

תכנון נכון של API

תכנון API הוא מלאכה עתיקה ולא-מוסדרת. כל מהנדסי-התוכנה מתכננים (או פשוט מיישמים) APIs. מחתימה של פונקציה, עד ל Public APIs בפרופיל גבוה ("Public APIs, like diamonds, are forever״).

אם הייתי קורא כזה פוסט, הייתי רוצה שלא יעסוק ב:
  • כללי ה REST. דיי נמאס! אחרי עשור ויותר, אפשר להניח שכל מפתח מכיר אותם היטב, או לפחות יש מספיק חומר סביר בווב (כולל הבלוג הזה) בכדי לכסות את הנושא לעייפה.
    • במקום העבודה הנוכחי שלי, ירדנו מ REST – לטובת Json over HTTP. זה היה לי קצת מוזר בהתחלה – אבל היום זה נראה לי כמו צעד נבון. אפרט קצת בסוף הפוסט.
  • GraphQL או Falcor = עוד באזז / טכניקה. כמו REST יש כאן שורה של כלים וכללים שאפשר ללמוד אותם – אבל האם זה באמת החלק החשוב ב APIs טובים יותר?!
  • כללים גנריים ובסיסיים ״צרו תעוד ל API״ , ״בדקו את ה API״, ״בחרו שמות טובים ל API״. אני מניח שאתם לא צריכים את הבלוג הזה בכדי למצוא את כללי היסוד של ההיגיון הבריא.
אז זהו. לא אעסוק בכל הנ״ל. הרשת גדולה – ואני אשאיר לאחרים לדוש בנושאים הנ״ל. עם כל החומר על ה API שיש ברשת (בעיקר: Rest, Rest, ו GraphQL) – נשארנו עם הרבה טכניקה, ומעט נשמה.
אם אתם גיקים אמיתיים – אתם בוודאי מאמינים בלב שלם שהטכניקה מספיקה. ש < שם של סגנון טכני של APIs [א] > הוא הפתרון השלם והטוב ביותר. אם תקפידו על הכללים הטכניים – יהיה לכם API מצוין! אולי אפילו תריצו כלי Lint שייתן לכם ציון 100. מצוין!
ראיתי ואני רואה הרבה API עם טכניקה ירודה עד מעולה – ואני באמת חושב שיש כמה דברים פשוטים שנוטים לפספס שאינם ב״טכניקה״. אנסה להעביר אותם בפוסט הבא.
A Developer in the process of integrating a 3rd Party API — Caravaggio, 1594

איך מתכננים API טוב?

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

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

API טוב מתוכנן מנקודת המבט של הלקוח – לא של היצרן

טעות ראשונה שאני רואה שחוזרת על עצמה היא API שעולם המונחים שלו משרת את היצרן (System A) – ולא את הלקוח.

בואו נראה צמד פעולות לדוגמה:

SimulatePurchaseWithoutSaving(…) –> simulationId
ApplyAndSave(simulationId, …) 

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

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

PurchaseABC(…) –> purchaseId
ConfirmPurchase(purchaseId, …)

חשוב מאוד שה API ישקף את האינטרס של הלקוח. מדוע הוא קורא ל API הזה בכלל? מה הוא רוצה להשיג?
חשוב להשתמש במונחים שרלוונטיים אליו, ולהימנע מכל מה שלא (למשל: האם אנחנו שומרים משהו או לא = אינו רלוונטי ללקוח).
מה יותר הגיוני? API בשם receiveEmailAndPassword או API בשם login?

כלל חשוב לזכור:

FOCUSING ON HOW THINGS WORK LEADS TO COMPLICATED INTERFACES

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

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

Leaking Abstractions

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

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

כאן גם הבעיה. העולם משתנה, הביזנס משתנה –> ולכן גם המערכת שלנו צריכה להשתנות.
שינויים רבים הם extensions – להוסיף עוד למערכת, שינויים שלא ״שוברים״ APIs.

אבל… גם שינוים ש״שוברים״ API יגיעו, ואז נגלה כמה רבים השימושים ב API, וכמה עמוקים וקשים לשינוי. המחיר יגדל באופן הבא:

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

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

בשני קצוות הקשת ישנן שתי גישות:

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

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

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

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

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

טכנולוגיות מגניבות כמו GraphQL לעתים מקלות על זליגת ההפשטות. המותג החזק ("GraphQL", ווהאו! פייסבוק! גוגל! זום!) מפחית את ההגנות שלנו בפני מה שעשוי להתהוות של ״סטטוס קוו״ שלא נצליח לעולם לשנות אותו.

מערכת שלא מתפתחת, ולא משתנה – היא מערכת Legacy. היא ״עושה את העבודה״, אבל לאט לאט היא לא מצליחה לעמוד בתחרות מול גישות ומגמות חדשות.

הבה נתייחס לתסריט הייחוס הבא:

המערכת (או מיקרו-שירות) שלנו היא מערכת A, ואנו חושפים את API a ללקוח כלשהו.
על מנת לספק את הבקשה, עלינו לפנות ל API b של מערכת B.

  • אל תחשפו ב API a רכיבים / אובייקטים מ API b. אתם קושרים את הלקוחות של Service A גם ל Service B – מה שיקשה מאוד על Service B להשתנות לאורך הזמן.
    • שכפלו אובייקטים. אובייקט Customer של API a יכול להיות זהה לחלוטין לאובייקט Customer של API b – וגם לדרוש העתקה. זו תקורה – אבל היא משתלמת לאורך זמן. כאשר API b ירצה להשתנות – הוא יכול, ורק יהיה צריך לשנות את לוגיקת ההעתקה בין האובייקטים.
  • אל תחשפו אובייקטים שלמים / עשירים מדי. יש משהו מאוד נוח, אך הרסני, בחשיפת API של קריאה / עדכון של אובייקטים שלמים של המערכת. האחריות של המערכת על האובייקטים שלה – פשוט אובדת.
    כאשר המערכת שלכם תצטרך לשנות את האובייקטים הללו בכדי לתאר התפתחות במערכת – זה יהיה קשה מאוד, ואולי בלתי אפשרי: שימושים שונים ותלויות מול האובייקטים הללו התפתחו בשאר המערכת – סוג של ״עבודה לא מתוכננת״, ואולי מאוד משמעותית – שנוספה לשינוי פשוט של API.

    • מדוע גוף האדם לא חושף את האיברים הפנימיים לעולם החיצון, שלא דרך מנגנונים מבוקרים? אולי אפשר ללמוד משהו מהטבע על תכנון מערכות מוצלחות.
    • צרו אובייקט נתונים עבור ה API (מה שנקרא גם DTO) ובצעו העתקה פשוטה בין האובייקט הפנימי (שיוכל להשתנות) לזה שאתם כובלים את עצמכם אליו לזמן בלתי-נשלט. זו השקעה טובה לטווח ארוך.
      • העתקה גם חשובה לצורך Immutability. במיוחד ב API של קוד באותה המכונה – אתם לא רוצים שמישהו יקבל אובייקט פנימי ואז ישנה לכם אותו.
    • חשפו באובייקט של ה API רק מה שהלקוח צריך. אל תהיו ״נדיבים מדיי״. צריך בכלל הזה גם לא להגזים:
      • אפשר לשלוח אובייקט עם 6 שדות – גם אם הלקוח זקוק רק ל 2, כל עוד אלו 4 שדות שהיה הגיוני לשלוח, לו הלקוח היה מבקש.
      • אפשר לשתף אובייקטים בין APIs שונים בתוך Service A. פיצול אובייקטים בתוך אותו שירות – הוא לא שינוי קשה מדי.
      • הגזמה ברמת הדיוק של ״לשלוח בדיוק מה שהלקוח צריך״ – תגרום לעיכובים בהתפתחות המערכת, דווקא מתוך התגוננות יתר. גם זה לא מצב טוב.
  • זכרו ש API הוא חוזה מחייב — אבל זה לא חוזה שמכסה את כל הפרטים. ה compiler יצעק עלינו אם נשנה טיפוס או נסיר שדה מה API. הוא לא יצעק אם נזרוק Exception במקום שלא זרקנו בעבר או נחזיר מרחב ערכים שונה (גדול?) מזה שהחזרנו בעבר. כלומר: יש שדה מסוג String – אבל התחלנו להחזיר ערכים חדשים שהלקוחות לא יודעים להתמודד איתם.
    • כדי לוודא שה API לא משתנה ופוגע בלקוחות שלכם – צרו Automated Tests לבדוק את האלמנטים ב״חוזה״ שהקומפיילר לא יודע לתפוס.
    • התאימו את כמות הבדיקות והיסודיות שלהם – לכמות הלקוחות / חשיבות ה API. בדיקה שלעולם לא תכשל – היא בזבוז זמן לכתיבה. אנו עוסקים בניהול סיכונים.

כמה דילמות נפוצות

API אחד המחזיר היררכיה גדולה של אובייקטים – מול API לכל אובייקט קטן בהיררכיה?

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

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

אם מדובר ב API בין Front-End ל Back-End ואתם מאחדים לאובייקטים גדולים כדי לחסוך latency של קריאות ברשת – אז עשו את החיבור ברמה של API Gateway – ובטח לא ברמת ה API של ה Back-End.

״למתוח״ API כדי להתאימו לצרכים, מול יצירה של גרסה חדשה?

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

מתי לזרוק Exception מ API? מתי להחזיר ״אובייקט כישלון״?

אפשר לעסוק בשאלה הזו בלי סוף. מה שחשוב הוא:

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

יש לי שני לקוחות ל API – כל אחד צריך מידע מעט אחר. האם להוסיף את הלקוח כפרמטר ל API או להגדיר שני APIs שונים?

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

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

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

למה ב Next-Insurance בחרנו לא להשתמש ב REST?

ב Next-Insurance ניסינו (כמו רבים אחרים) להיצמד לכללי ה REST בכדי ליצור אחידות ב APIs הפנימיים, ולהפוך אותם לצפויים יותר לשימוש. ל REST יש כמה בעיות ידועות כמו המגבלה להעביר payload גדול על קריאת get, או ההגדרות הלא מדויקות והניתנות לפרשנות של REST כרעיון (מעולם לא היה תקן מסודר, ומעולם לא היו פיתוחים מוסכמים ל REST מעבר למסמך הראשוני). למשל: קשה מאוד להתיישר על סכמת URLs עקבית בקבוצה גדולה של אנשים. לאנשים רבים יש פרשנויות רבות כיצד URL שהוא RESTful צריך להיראות.

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

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

מפה לשם, עברנו ל JSON over HTTP בתצורה מסוימת. תמיד משתמשים ב HTTP POST ותמיד ה API מגדיר אובייקט (DTO) בקשה ואובייקט תשובה שעליו ישבו הפרמטרים. ה URL הוא רצף מילים שקשור לפעולה, לפעמים RESTFul – אבל לא בהכרח.

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

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

עוד כמה קלישאות (נכונות):

  • API צריך להיות קל לשימוש, וקשה לשימוש לא-נכון.
  • בכדי לעצב API לא צריך ניירת או תהליכי חשיבה ארוכים. צרו טיוטא מהירה של ה API וכתבו קוד שמשתמש ב API הזה. זו הדרך הטובה לחשוב וללטש את ה API.
  • צייתו ל Principle Of Least Astonishment (בקיצור: POLA).
    • ה API לא צריך להפתיע את המשתמש. ככל שהמשתמש של ה API רחוק מכם יותר – הכלל הופך לחשוב אפילו יותר.
  • השתמשו ב APIs בתבניות (Format) הנוח ללקוח, ולא למערכת. יש מין טעות כזו ש API צריך להיות נוח ליצרן ולא ללקוח.
    • אולי אתם מייצגים זמן ב Java Epoch, אבל ללקוח יהיה הרבה יותר קל לעבוד בתאריכים קריאים, קרי 2020-04-19.
  • API הוא רכיב קריטי באבטחת המערכת. זה זמן טוב להזכיר שיש OWASP Top 10 ל APIs.

סיכום

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

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

—-
[א] סגנונות API נפוצים הם Falcor, gRPC, SOAP, RPC, GraphQL, REST או סתם JSON over HTTP.

—-

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

Public APIs, like diamonds, are forever

בחירות בתוכנה: איך לבחור נכון?

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

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

האם נכון ״להרכין את הראש״, ולהכניס את הפיצ׳ר תוך יצירת עיוות במערכת?

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

אם אתם מתכנתים בארגון גדול – כנראה שיש לכם ״גב״ להחלטות שכאלו. הולכים לארכיטקט / מנהל טכני – והוא יחליט.

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

איך בוחרים?

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

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

"אם מדובר במודול קריטי של בקרה על כור גרעיני – אסור להתפשר בכלום על האיכות!" היא דוגמה מלומדת ששמעתי כמה פעמים בהקשר זה. מעולם לא פגשתי במתכנת שפיתח מודול קריטי של מערכת שתלויים בתקינותה חיים רבים. למתכנתים הללו יש כנראה guideline דיי ברור. מה איתנו, כל שאר 99.999% מאנשי התוכנה?

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

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

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

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

אז מה עושים?
איך אתם לוקחים החלטה שכזו?

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

התשובה היא כמובן: It depends.

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

זה תלוי ברוח הארגון, וב DNA שלו: ישנם ארגונים, גם לא קטנים, המקדשים את ההתקדמות והיכולת לבצע שינויים, גם במחיר סיכונים מסוימים. "Move fast and break things".
הערה: אני פותח פה פתח ל Self-Suggestion לקורא שרוצה להימנע מעימותים ופשוט יאמר לעצמו "אצלנו ב DNA הארגוני רוצים רק פיצ'רים – אז אני אזרום עם ה DNA הארגוני". אני לא רוצה לעודד סוג כזה של מחשבה – אלא שתחשבו מה נכון לארגון שלכם באמת. עד כמה הדחיפה לפיצ'רים באמת עובדת טוב, וכמה זמן פיתוח באמת "שוקע" בגלל עיוותים במערכת.

זה תלוי בגודל העיוות: אי אפשר לכמת עיוות, אבל אפשר לומר ש"לשמור גם אובייקט Y בטבלה שנועדה לשמור אובייקטים מסוג X" [א] הוא ככל הנראה עיוות חמור יותר מלהחזיר null או 1- בכדי לחוות על מצב לא-תקין ב API (פעם זה היה best practice, אפילו).

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

חטאים…

כמה אנחנו מסוגלים להעריך את הנקודות הללו בצורה אובייקטיבית וטובה?
כארכיטקט צעיר ב SAP (וטיפוס דקדקן) חשתי רוב הזמן שאנו במצב רע טכני, ועושים יותר מדי פשרות. לא הייתה לי פרספקטיבה מאוזנת להבין שבצם זה היה [ב] ארגון שדווקא עשה הרבה יותר מדי הנדסה ופחות מדי Business. בדיעבד הייתי חוזר בזמן ומעודד לאפשר ולקבל משמעותית יותר "עיוותים" במערכת. לדחוף את הארגון לראות כבר לקוחות פעילים!

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

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

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

חשוב גם לזכור:

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

סיכום

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

אז מה אני מציע לעשות כאשר יש דילמה בין הוספת פי'צר למערכת במחיר עיוות למבנה המערכת?

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

—-

[א] נתקלתי במקרה כזה כי רצו לחסוך יצירה של טבלה בבסיס הנתונים (?!). כמה שבועות מאוחר יותר הבינו שזה היה חיסכון דבילי (לא קיבלו את דעתי הראשונית) – ויצרו לאובייקט Y טבלה משלו.

[ב] נקודתית, בשעתו – לא יודע כיצד הוא עכשיו.