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

בתואר שני במדעי המחשב, היה קורס חובה בשם ״סיבוכיות״. עסקו בו במושגים כמו 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 טבלה משלו.

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

הרשו לעצמכם קצת אי-סדר בקוד (דעה)

כשמדברים על סדר ו״ניקיון״ קוד – יש סקאלה של מצבים אפשריים:

  • בקיצוניות אחת: ״בנה ותקן״ בו כותבים את הקוד המיידי ביותר האפשרי בכדי להפעיל את הפיצ׳ר הבא, מגלים באגים ואז הולכים לתקן אותן (וב 20% מהפעמים או יותר יוצרים באג חדש). כל העניין של סדר הוא ״למרובעים״ או ״אנשים בעלי זמן פנוי״.
  • בקיצוניות שניה: פדנטיות קוד, בה כל קבוצת שורות של קוד עוברות refactoring וסידור על בסיס יומי. מקסימום סדר, מקסימום encapsulation, שום גרם מיותר של אי-סדר בקוד.

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

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

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

כמו שגוף שמאיץ לכיוון מהירות האור הופך כבד יותר ודרושה אנרגיה גדולה יותר ויותר בכדי להמשיך להניע אותו ולהאיץ, גם ״סדר מופתי״ בקוד דורש מאמץ הולך וגובר עם הזמן.

כמו מהירות האור שהיא יעד בלתי-מושג לגוף עם מאסה, כך גם ״סדר קוד מופתי״ הוא יעד בלתי אפשרי לגוף בעל תפיסה-ביקורתית*.

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

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

מדע בידיוני, בינתיים.

מדוע בכלל לחתור ל״סדר מופתי״ בקוד?

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

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

כוונות מול מציאות

מה עושים?ֿֿֿ

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

האם אני מציע לוותר על ״איכות קוד״ כערך עליון?

זו דילמה קשה!

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

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

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

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

אז מה, בכל זאת, עושים?

טוב. אני מקווה שהצלחתי להעביר את הדילמה.
גישה אחת היא להמשיך בדרך המקובלת יותר בתעשייה – הצבת איכות כערך עליון של הארגון.
גישה נוספת שאני רוצה להציע היא ניהול דינאמי של מצב איכות הקוד:
  1. ננהל רשימה "חיה" של היבטים לא אופטימליים בקוד. את הרשימה כדאי לאסוף ממגוון חברים בארגון.
    1. אם תנהלו את הרשימה הזו ברצינות – מהר מאוד תגיעו לעשרות פריטים ברשימה, ויותר.
    2. אם לא הגעתם – סימן שלא חיפשתם מספיק טוב. זה כמו לשאול את עצמכם ״במה אני פחות טוב?״ – ולא למצוא שום דבר.
  2. מתוך הרשימה – מצאו פריטים בעלי Impact: עדיף impact עסקי, אבל גם impact טכנולוגי – פחות בעיות, יתר קלות לקוד שנוסף למערכת, בקרה טובה יותר על המערכת וכו׳.
    1. סביר שייקח לכם כמה סיבובים על מנת להבין היכן באמת נמצא ה Impact – רעיונות עלולים להישמע טוב, אך להיות חסרי impact לחלוטין בפועל.
  3. תזמנו זמן עבודה מוגדר לצורך שיפורים יזומים ופתרון הפריטים החשובים ביותר ברשימה. ה benchmark הבריא שאני מכיר הוא כ 20% מזמן העבודה
    1. פרשנויות שונות מה עושים בזמן הזה (כמו: ״באגים הם ברשימה״) – יכולים להפוך אותו ללא רלוונטי.
  4. שווה לעבוד בצעדים קטנים. אם יש בעיה גדולה, הרשו לעצמכם למצמצם אותה ולראות מה ההשפעה העסקית שנובעת מכך. למשל: תהליך ה deploy אורך 15 דקות? נסו לבצע שיפור (נניח: המקצר אותו ל8 דק') ולראות מה ההשפעה בפועל. זה יותר טוב מלהשקיע פי כמה עבודה בכדי להביא אותו ל 2 דקות – ולגלות שה impact לא כ"כ משמעותי.
  5. חשוב לערב את אנשי הצוות בהחלטות ובעשיה.
    1. זה לא רק עניין של engagement ושותפות-גורל. זה גם עניין של ״חלונות שבורים״: לחדד ולהזכיר לכולם שקוד טוב הוא ערך חשוב – ושאנחנו כן משקיעים בו. ושהשקעה בו נושאת פרי.

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

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

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

הנה כמה פריטים מרשימות כאלו שניהלתי – שאכן עשו impact:

  • פירוק של טבלה גדולה ב DB לכמה טבלאות – ע״פ דפוסי שימוש שונים.
  • העברת נתונים מבסיס הנתונים ל Redis.
  • שינוי הייצוג של הנתונים – למבנה קל יותר לעבודה.
  • פירוק שרת Redis לשני שרתים ע״פ דפוס שימוש: כזה ל cache וכזה לנתונים שחשוב שיישמרו. הפירוק אפשר לנו לנקות את ה caches ביעילות מבלי גרימת נזק.
  • כתיבת מודול או microservice מחדש. צריכות להיות סיבות טובות למדי – לכתיבה מחדש (ופעמים רבות – יש כאלו).
  • שינוי מבני של flow מורכב במערכת.
  • ניקוי קוד ישן / נתונים ישנים מבסיס הנתונים.
  • הוספת כלי ניטור על מצבים חשובים במערכת – שחסרה נראות שלהם (דברים מאוד ספציפיים)
  • Key Security Items
  • Key Performance Items
  • סידור נתונים ל BI
ברור שהיה עדיף למנוע מראש את היווצרות של המצבים הללו. בהינתן הדינמיקה הארגונית / הלחצים / וחוסר ידיעת העתיד – עד כמה זה באמת אפשרי?
ברור שזו רשימה ספציפית בהקשר ספציפי – שלא אוכל להעביר בפוסט.

הנה כמה פריטים שהיו ברשימות שכאלה, ולעולם לא הגיעו לידי מימוש:
  • סידור flows מסוימים בקוד בכדי שיהיו פשוטים וברורים יותר (הרבה מקרים).
  • HTTP being used internally (ולא https)
  • מקרים מסוימים בהם כשל במיקרו-שירות אחד – גורר כשל במיקרו-שירות אחר
  • Schemas שונים בבסיס הנתונים שנמצאים ב encodings שונים: אחד בשוודית והשני ב ISO.
  • ניקוי ה git repository והסרת 25MB מיותרים.
  • Various Security Items
  • Various Performance Items
  • סידור נתונים ל BI
  • וכו׳

האם הפריטים הללו ראויים לרשימה?!

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

סיכום

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

מה שאני כן ממליץ הוא לא להילחץ מאי-סדר יותר מדי, וליצור תהליך מובנה להדוף את הצדדים הבעייתיים ביותר שבו.

אני זוכר את התקופה שעבדתי ב SAP והיינו פותחים Critical Bug (הרמה הגבוהה ביותר) על כל מקרה בקוד בו עשו [3] catch Throwable (מעל ה JVM). היום אני מתעלם ממקרים כאלו – ואפילו כותב catch throwable בעצמי – אם זו הצורה המקובלת לתפוס Exception בבסיס הקוד בו אני עובד.

היה לנו איזה כלי בשם Sonar (ניתוח סטטי של קוד) שניסה לחשב את ה Technical Debt של בסיס-קוד שהוא סרק בצורה מספרית / דולרית. לכל Catch Throwable הוא נתן תג מחיר של  כמה עשרות דולרים (לא זוכר בדיוק). יכולתי לפתוח tickets חסרי impact לחלוטין, ברמת הדחיפות הכי גבוהה – ולהרגיש טוב עם עצמי שאני חוסך כסף לחברה. צעירות!

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

[2] על בסיס מדדים כמו cyclomatic complexity או מספר הפרמטרים המועברים לפונקציה.

[3] למרות שהייתה כוונה ברמת ה JVM יום אחד לעשות שימוש ב Throwable למטרה מעט אחרת – כבר עברו 23 שנה, ולא נעשה בו כל שימוש. כ״כ הרבה מפתחים השתמשו כבר ב Throwable כאילו הוא Exception לכל דבר – כך שכבר לא נראה לי שניתן לייחס לו בגרסה עתידית משמעות אחרת.

4 חטאים של פיתוח תוכנה בן-זמננו [דעה]

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

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

  • בספרי תכנות, למשל, הפרקים המאוחרים (לעתים מאוגדים כ "Advanced Topics") הם לרוב נושאים פחות שימושיים ביום-יום. ביום פקודה – אפשר להשלים את הידע נקודתית. זו גישה מאוד הגיונית.
  • היתרון מלהכיר עוד Frameworks הולך ופחות ככל שאתם מכירים יותר Frameworks. אם אני מכיר כבר שני Web Frameworks בצורה טובה – איזה יתרון באמת יהיה לי מללמוד את השלישי?!
  • אפשר ללמוד אינספור כלים וספריות, אבל אם לא עובדים בהם בצורה משמעותית – זה יידע שלא יעשה בו שימוש ו/או יישכח במהרה.
  • ישנם נושאים קצת יותר רחוקים מכתיבת הקוד עצמו, אך מספיק שונים בכדי לספק לנו "קרקע בתולית ללמידה". הרבה פעמים יש להשקיע בהם השקעה משמעותית מאוד – עד שנראה תמורה אמיתית ביום-יום שלנו. למשל: Machine Learning, מערכות מבוזרות, או Big Data. לא בטוח שזה אפיק משתלם עבור רוב אנשי-התוכנה.
 

—-

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

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

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

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

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

אז מה יש לנו?

TDD – איך כותבים בדיקות מוצלחות, ואיך כותבים קוד שקל לכתוב לו בדיקות מוצלחות.

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

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

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

מה הן בדיקות טובות? יותר קל לי להציג אלמנטים נפוצים של בדיקות לא טובות:
  • אנשים מגזימים בכמות הבדיקות המערכתיות (System Test, Integration Tests) על חשבון בדיקות ממוקדות. גלידה ולא פירמידה. זה כ״כ נדוש ושחוק, אך עדיין – טעות שממשיכה ונעשית.
  • אנשים לא מבודדים Pure Business Logic משאר הקוד לצורך unit tests – ואז באמת קשה מאוד מאוד לכתוב ולקבל את היתרונות של unit tests.
    • נתקלתי הרבה פעמים במצב הזה, וזו בעיה שיחסית קל לתקן, ברגע ש״נופל האסימון״ – ומשנים גישה.
  • אנשים כותבים יותר מדי קוד בדיקות – מה שמאט את העבודה שלהם, ומקשה על ביצוע שינויים במערכת:
    • גם בדיקות שהן overfit למימוש ארעי (situational), כלומר תנאי שמתקיים – אך אינו חשוב ועקרוני לפעולת המערכת / הביזנס. בהמשך הוא ישתנה, לא תהיה בעיה עסקית – אך הבדיקות יפלו וידרשו עדכון.
    • גם בדיקות שהן יתירות (בודקים את אותו הדבר שוב ושוב באופנים שונים). כל שינוי של מימוש קוד – ידרוש סדרה של שינויים בקוד הבדיקות – מה שיגרום לנו לרצות לעשות פחות שינויים.
      • יעילות מגיעה מניהול סיכונים נכון: האומץ לצמצם את כמות הבדיקות (לא לכתוב בדיקות מסוימות), מתוך הבנה אלו בדיקות חשובות ומשמעותיות יותר.
  • אולי הכי גרוע: בדיקות ועוד בדיקות שנכתבות (ומתוחזקות!) מבלי שהן מגרדות את פני השטח. הן בקלות יכולות לעבור – בזמן שמשהו עקרוני ולא טוב קורה ב flow. בקיצור: בדיקות לא-משמעותיות.
    • זכרו: אם הבדיקות שלכם אף פעם לא נשברות – זו לא סיבה לגאווה. זה אומר שבזבזתם את הזמן בכתיבת בדיקות שלא אומרות כלום.
  • אנשים שהתייאשו מבדיקות ו״למדו״ (אבוי!!) – שבדיקות הן נושא overrated ומיותר.
    • זהו מצב שמאוד קשה להתאושש ממנו.
לסיכום: Programmer Testing הוא כלי כ"כ חשוב, כל כך יישומי, ושמביא תוצאות כ"כ טובות ומהירות (כשעושים אותו נכון), ועדיין – רק אחוז נמוך בצורה מבהילה של אנשי-תוכנה באמת שולט בפרקטיקה הזו.

 

המחשבה שאם אתם מכירים את הספרייה שאיתה עושים בדיקות (JUnit5, Jasmine, RSpec, Sinon), אזי אתם יודעים "לכתוב בדיקות טובות" – היא שגויה מיסודה. חפשו את העלות/תועלת: כמה השקעה יש בכתיבת ותחזוקת הבדיקות – מול כמה זמן הן מקצרות בתהליכי ה debug ותיקון שגיאות.
 

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

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

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

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

Refactoring אקטיביסטי

הנה עוד דבר שעשוי להישמע מעליב: ״אני לא יודע לעשות Refactoring טוב מספיק? יש לך מושג כמה פעמים כבר עשיתי Refactoring? מה הבעיה בלעשות Refactoring?״

אני רוצה להדגיש מימד שקצת שנשכח לגבי Refactoring: האקטיביזם.

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

המון! מן הסתם.

ברגע הזה, שקורה לרובנו על בסיס יומי, עומדות בפנינו שתי ברירות:

  • להמשיך הלאה.
    • יש לי מנהל עם ״סטופר״ שיבוא בתלונות אם לא אדלוור פיצ'רים מהר.
    • יותר גרוע: שינוי בקוד הוא סיכון ליצירת באג. אם יש באגים שנוצרו על ידי – אני יוצא לא-טוב. (איפה ה Unit tests שלכם עכשיו, הא?)
  • לבצע Refactoring ולהחזיר את הקוד לרמה אופטימלית X (כלומר: רמה טובה, אבל לא מושלמת. שלמות היא בזבוז).
    • Refactoring אינו צריך, ועדיף שלא יהיה "פרויקט ענק". הוא יכול להיות בכל commit שלישי.
    • אם שומרים על רמת קוד טובה לאורך הזמן – יהיה הרבה פחות צורך בפרויקטי ענק.
אז מה אתם בוחרים?
 
לרוע המזל רוב אנשי-התוכנה בוחרים ב"דרך הבטוחה". זה עובד? – אז לא לגעת! 
אשמה גדולה היא בקרב המנהלים, שהם קצרי רוח לזמני ביצוע של פיצ'רים והופעות של באגים – אך יש להם מספיק סבלנות ל"פרויקטי תחזוקה", ופיצ׳רים פשוטים / חקירות באגים שמתארכות לאורך ימים.
 
החטא של המפתחים הוא שהם תורמים את חלקם למעגל המזיק הזה – ובעצם פוגעים באינטרסים שלהם.
התמריץ לשמר את הקוד ברמה "אופטימלית X"  הוא לא רק עניין של ערכים "אני בעד קוד יפה", חלילה!
יש פה אינטרסים מעשיים:
  • קוד שמתוחזק ברמה גבוהה – יאפשר להוסיף פ'יצרים נוספים בצורה קלה ומהירה יותר, ועם פחות תקלות.
    לאורך הזמן השאלה צריכה להיות: האם אתם רוצים לעבוד בקוד מתוחזק, או בקוד "עולם שלישי"? באיזו סביבה אתם חושבים שתתפתחו, אישית – בצורה טובה יותר?
  • כאשר בוחשים בקוד – רמת העומק וההבנה האישית שלנו את הקוד, ומה שקורה בו – צומחת בקצב אחר לגמרי.
    • אני לא יכול להדגיש זאת מספיק: מי ששובר את הקוד (או לפחות מסתכן בשבירה) – הוא מי שמבין אותו לעומק. "לשבת על הברזלים" זו אסטרטגיה נוחה לטווח הקצר – אך נחותה לטווח הארוך.
עוד אלמנט חשוב הוא היכולת שלנו לראות כיצד הקוד יכול ללבוש צורות שונות – והיכולת להעביר את הקוד בקלות מצורה לצורה: אולי functional? אולי לולאת foreach? אולי break ואולי exceptions.
  • בעיות שונות בקוד יפתרו באלגנטיות רבה יותר בעזרת צורות שונות של קוד. 
    • כאשר אנשים מקובעים לתבנית אחידה / סגנון אחיד – זה מגביל!
    • לאנשים רבים, גם כאלו עם ניסיון של שנים – חסרה ממש הגמישות הזו: קשה להם לקרוא ולהבין קוד בסגנון שונה, והם חוזרים וכותבים קוד בצורה "שהם רגילה אליה" – גם במקרים בהם היא מסורבלת וקשה לקריאה.
  • Refactoring תכוף – הוא דרך נהדרת ללמוד ולהתנסות בצורות קוד שנות. זה האינטרס האישי שלכם!
  • שווה לציין גם טכניקה בשם "Coding Dojo״ שאמורה לפתח מנעד רחב יותר של סגנונות קוד:
    • מתכנסים כמה אנשים בחדר ופותרים תרגיל קוד קטן כאשר מחליפים ידיים כל פרק זמן נתון (מעבירים את המקלדת מאדם לאדם). עוד נוהג הוא לעשות את אותו התרגיל – מספר פעמים. בכל פעם – תהיה תוצאה קצת אחרת.
    • נ.ב. אני נוטה להאמין שיעילות המפגש שכזה היא ביחס ישיר לאדם המוכשר ביותר בסגנונות קוד שנוכח בו.
בקיצור: האם יש ״סגנון שנוח לכם איתו״ ורק בו אתם כותבים?
האם אתם עסוקים ב"לשמור על הקונבנציות" יותר מאשר לחשוב ולהבין איזו צורה של קוד היא הטובה ביותר לבעיה?
האם אתם "חכמים מספיק לא להתעסק עם קוד שעובד". מעתיקים מדוגמאות קוד אחרות במערכת – מבלי לצלול לעומק מדוע הקוד הזה עובד?

 

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

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

Design to Go

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

  • עבודה ב Small Batches.
  • יצירה של Short and Effective feedback cycles.
  • בחינת אלטרנטיבות – מתוך ההבנה שיש יותר מדרך משמעותית אחת לסדר קוד ו/או לפתור בעיה.
    • ״קו האפס״ הוא פתרון יחיד שעובד – ומשם משפרים. 
    • אחרת: אנחנו עובדים על עצמנו. לא משנה כמה מלבנים ציירנו בדרך.
  • כאשר ״תקיעה״ בתהליך הדזיין, מובילה אותנו לוותר עליו – במקום לעבור ל Exploration.
כבר דיברתי הרבה בנושא בהרצאה שלי ברברסים. אין טעם לחזור.
 
מקור: Integrating and Applying Science" (pg. 136) – http://ian.umces.edu/press/publications/259/

 

 

Modeling

 

Modeling היא לא פרקטיקה נפוצה בקורות החיים של אנשים. 

המונח ״Medling״ כנראה מובן לרוב האנשים, אך הוא לא נתפס כנושא בעל חשיבות עליונה – שכדאי לפתח.
  • הזכרנו כבר שנקודת מפתח ב Design היא בחינת אלטרנטיבות.
  • החלק המשמעותי באלטרנטיבות הללו הוא לא ״אובייקט גדול״ מול ״שניים קטנים״ – אלא מידול שונה של האובייקטים העסקיים. למשל: ״תשלום, הכולל ניסיונות תשלום״, מול ״נסיונות תשלום הכוללים תוצאה״.
  • ״גמישות לדרישות עתידיות״, ו״פשטות״ הם BuzzWords – אך הם גם סופר-משמעותיים במבחן התוצאה. 
    • מודל פשוט וטבעי לביזנס – יכול בהחלט להכפיל את התפוקה של הצוות.
      מיומנות מעטות בעולם התוכנה עשויות לגרום להשפעה (impact) רבה שכזו!
  • היכולת לעשות modeling נכון נובעת מניסיון תמידי להבין את הביזנס והצרכים + הפעלה של חשיבה ביקורתית.
    • קל לצייר בראש מודל – שלא ממש מתאים לביזנס. חשוב לתקשר ולאמת אותו.
    • לא תמיד אנשי הביזנס יתחברו למודל – וחשוב גם לנסות ולאתגר אותם.
  • Modeling לא נעשה רק בשלב דזיין – אלא גם כתהליך refacotring, שינויים קטנים כל הזמן.
  • Modeling מתקשר בד״כ למידול של אובייקטים עסקיים, אך הפרקטיקה נכונה גם למודל טכני (מודל concurrency, מודל eventual consistency, מודל sevurity):
    • שואלים ומאתגרים כל הזמן מה הם הצרכים
    • מנסים למצוא מודל פשוט ואלגנטי ככל האפשר, פשוט ע״י איטרציות של שיפורים במודל.
    • מתקשרים את המודל – כך שיהיה רעיון משותף, ולא ״מחשבה פרטית״.
  • איך לומדים לעשות מודלינג?
    • ע״י צפיה בדוגמאות של מודלים. למשל הספר PEAA (דוגמה יפה: המודל של Money), או הספר המעולה (אך קצת מיושן): Analysis Patterns – של אותו המחבר.
    • ע״י בניית מודלים והפקת לקחים אישיים.

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

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

 

ה Killer instinct

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

כשאתם נתקלים ב״Killer Instinct״ – קשה להתעלם ממנו.

  • זה השילוב של הבנת ביזנס, חשיבה ביקורתית, קריאה נכונה של הארגון (מי מדבר שטויות, מי יודע), קצת חוצפה (ממי להתעלם, למי להתייחס), והאומץ לבצע שינויים / לכתוב קוד שיש לו חסרונות ברורים – לצד יתרונות ברורים, כמובן.
    • תמיד נתקלתי ב Killer Instinct בצמידות לנטייה לגעת בקוד ולשנות אותו. חוסר פחד, ביחד עם סקרנות ורצון לחולל שינויים.
      אני נוטה להאמין שיש פה גם אלמנט של סיבתיות: הניסיונות הקטנים לשפר את הקוד -> יוצרים הבנה עמוקה של הקוד (עם הזמן). הבנה עמוקה של הקוד -> מאפשרת את ה״מאסה הקריטית״ של העומק – הדרושה בכדי לבצע שינויים משמעותיים במערכת בזמן קצר.
  • ״להתעסק״ עם הקוד בלי שיש בדיקות טובות – לא כדאי. הקוד ישבר, וההתעסקות תהפוך לעניין כואב ומתסכל.
  • הבנה עמוקה של הקוד, ללא הבנה של הביזנס – עשויה לפספס את האימפקט:
    אתם עושים שינוי עמוק במערכת, שאף אחד לא האמין שאפשרי – אבל אז גם לאף אחד לא אכפת הוא נעשה, כי הוא פשוט לא מעניין.
  • בכדי ליצור אימפקט, חשוב להבין את הביזנס. הבנה של הביזנס נבנית מתוך Modeling.
  • בכדי שהתוצר יהיה טוב יותר, ומשמעותי גם לאורך זמן – חשוב גם לדעת איך לעשות Effective Design.
 
האם זה מספיק? האם זה המתכון הסודי והמובטח לשחרור ה״Killer Instinct״?
לא. מן הסתם זה גם עניין של אופי: חוצפה/תעוזה, הרבה אכפתיות ורצון עז להשפיע.

 

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

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