הפילוסופיה של יוניקס

ראשית כל, בואו ונשאל: למה שתעניין אותנו הפילוסופיה של יוניקס? – פילוסופיה של חבורת אנשים מזוקנים משנות ה 60 וה 70 של המאה הקודמת?!

כלומר: אלוהים ישמור! למה שנתעניין במשהו בן 50 שנה? הרי אנחנו בתרבות שמקדשת את החדש-ביותר. כל מה שקרה לפני הקורונה – נראה היום כבר רחוק וכמעט לא-רלוונטי.

אפשר לטעון שיוניקס היא מערכת ההפעלה הטובה ביותר (הנדסית) שנכתבה אי-פעם. אפשר לטעון שהפילוסופיה של יוניקס הכילה את עקרונות ה SOLID, מיקרו-שירותים, Lean, ו Devops culture כבר בשנות ה-70, רעיונות שעדיין פופולריים גם היום – מה שעשוי בהחלט להקל עלינו את ההתעניינות בפילוסופיה הזו.

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

אז מה היא בדיוק הפילוסופיה של יוניקס?

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

קיצור (אגרסיבי) של הפילוסופיה של יוניקס

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

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

Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new “features.”

מילות המפתח שצצות כאן הן: מודולריות, פשטות, Single Responsibility, העקרון הפתוח-סגור, ו Lean.

ארכיטקטורת הבסיס של לינוקס היא ארכיטקטורה של קרנל:

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

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

אבל….

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

Expect the output of every program to become the input to another, as yet unknown, program.

פונקציונליות עשירה יותר, מורכבת ע״י ״הרכבה״ של אפליקציות פשוטות בזו על זו.

cat foo.txt | wc -l

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

אנו משתמשים מכאן ב Pipe (אם אתם לא יודעים מה זה pipe – לכו מיד וקראו!), על מנת להעביר את הפלט לאפליקציה אחרת, wc (קיצור של word count [א]) ורק היא תשלח את התוצאה שלה ל standard output.

תכנון ה pipe ע״י Doug McIlroy בשנת 1964. קצר, פשוט, ענייני.

wc מקבלת פרמטרים (במקרה שלנו l-) המאפשרים לה לפעול טיפה אחרת, ובעצם לספור שורות.

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

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

  • שטף של בתים (לא ביטים), בד״כ ב ASCII. כלומר: טקסט.
  • אפשר שיהיה לו סוף – בדמות EOF.
  • n\ סימן להפרדה בין רשומות (או ״שורות״)
  • +[t\ ] (כמות כלשהי של טאבים או רווחים) – הפרדה אפשרית בין שדות.

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

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

בגלל הסימנים הנוספים שהיא מוסיפה לטקסט בכדי להשיג את התסדיר של ה Syntax highlighting – היא כבר לא תעבוד טוב עם wc (או הרבה אפליקציות אחרות). היא חטאה לממשק הפשוט של ה Pipe.

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

פשטות אמיתית דורשת המון משמעת, כמו שנאמר: ״Simplicity is the ultimate sophistication״.

סיכום

אז מה היא הפילוסופיה של יוניקס, ולמה היא חשובה?

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

״Build a prototype as soon as possible. Most users looking for a new piece of software won't know what they really want until they see it, so requirements documents are often misleading about the users' real needs. The Unix design philosophy makes prototyping a central part of the methodology: give the user something, anything, up-front to criticise, and work from there.״

אתם מאמינים שהיא נכתבה במיליניום הקודם? אז מי המציא את ה Lean-Startup? אריק ריס או מפתחי היוניקס?!

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

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

לפני כעשור ועוד עבדתי עם מודלים אחרים של רכיבי UI, בעיקר Java Portlets או כל מיני תקנים של widgets ו gadgets אחרים. הממשק היה כמעט תמיד מורכב בהרבה – והקשה מאוד על קהילה רחבה לתרום למודלים הללו. זה לא היה פשוט.

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

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

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

—-

[א] הערת אגב: עם הכבוד הרב שאני רוחש לפילוסופיה של יוניקס – אני עדיין מאמין ששמות מאוד קצרים הם טעות גדולה של יוניקס שמחירה מודגם שוב ושוב. שמות מאוד קצרים קשה יותר לזכור, וקל מאוד לבלבל בינהם / לפספס את המשמעות.
יוניקס הייתה טיפה מוצלחת יותר אם ל cat היו קוראים concat, ול wc היו קוראים wordcount (או פשוט count – מכיוון שיש לה הרבה אופציות ספירה שונות), ול man היו קוראים manual. הקנאות לקיצור הרגה את הקריאות ביוניקס.


—-

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

סיכום החוקים של הפילוסופיה של יוניקס. אפשר לקרוא ולהנות.

כמה מילים על הפילוסופיה של יוניקס מספר מ 1995. מופיע בוויקי המקורי – תמציתי ומעניין.

תכנון נכון של 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

עבודה מרחוק (וירוסים, קדמה, או סיבות אחרות)

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

השאלה הראשונה שעולה היא ״איך מתמקדים בעבודה ולא ׳זולגים׳ פעם אחר פעם לפייסבוק או קאנדי קראש (או מה שפופולארי עכשיו)?״

השאלה השנייה היא – ״איך לעזאזל אני יכול להתרכז בעבודה עם ילדים בבית?״

אני אנסה לרכז כמה פרקטיקות לשאלה הראשונה.

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

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

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

מה שונה בעצם בעבודה מבוזרת?

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

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

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

מה זה אומר, ״לעבוד יעיל״ מרחוק?

כלים

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

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

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

טריגרים

המוח שלנו עושה הרבה עבודה שאנו לא מודעים אליה. כאשר אנחנו עוברים context-switch במהלך יום העבודה, ישנם רגעים בהם אנחנו מסתכלים סביב ושואלים את עצמנו: ״שניה! איפה אני? מה קורה עכשיו?״. השאלה והתשבה הן דיי מיידיות – ואנחנו לא שמים לב אליהן.

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

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

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

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

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

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

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

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

גבולות

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

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

איזון

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

כשהעבודה מהבית מצליחה להיות יעילה – שבחו את עצמכם. זה הישג, וזה לא מובן מאליו!

תקשורת 

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

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

הרבו בתקשורת בפורום קטן (למשל, צוות), מעטו תקשורת בפורום הכללי.

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

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

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

שפרו תקשורת טכנית

Technical communication skill – היא מיומנות חשובה, בלי קשר לעבודה מרחוק:

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

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

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

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

ניהול אנשים מרחוק

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

החשש הגדול ביותר של מנהלים בעבודה מרחוק הוא כנראה: ״כיצד אני יודע שהם בכלל עובדים?״

שעות עבודה

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

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

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

יותר מזה: נסו לכבות את ה "online indicators״ בכלים בהם אתם עובדים, אולי תגלו שעדיף בלעדיהם.

השקעה מול תפוקה

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

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

  • סגנון פחות מוצלח: ״לא עשית commit היום! איך אני יודע שאתה עובד?״
  • סגנון יותר מוצלח: ״עבודה ב batches קטנים היא יעילה יותר בכלל, אבל היא גם עוזרת להישאר מחובר לארגון שלנו ולקצב העבודה. בוא נעשה מאמץ לשמר את הכלי הזה״.

מוטיבציה

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

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

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

Batches קצרים (ימים!, לא שבועות), הגדרת הפיצ׳רים בצורה מדידה, חיבור המפתח ללקוחות שלו (לקוחות קצה, או המפתח הבא שישתמש בפיצ׳ר), וכו׳ – הם כלים חשובים אפילו יותר בעבודה מרוחקת.

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

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

פערי זמן

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

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

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

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

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

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

סיכום

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

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

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

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

״תקועים בבית?״ – סט פעילויות איכותי לילדים של מכון דוידסון

זמן ותאריכים בתוכנה (ועל ה JVM בפרט)

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

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

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

זו באמת כזו בעיה?

זמן הוא דבר מורכב

בפועל הסינים לא ממש הסכימו לאכול בשעה 20:00 בצהריים. מסתבר שבני האדם, בכל העולם, מעדיפים לאכול צהריים ולתלות פושעים – בשעה 12:00.

למה דווקא שבאנגליה יהיה 12:00 בצהרי היום? (כי הם היו האימפריה הגדולה והחזקה בעולם בעת קביעת השעון הגלובאלי?)

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

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

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

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

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

אפילו לא נתנו ליבשת הקפואה והשוממת הזו להיות פשוטה…

דוגמה משעשעת אחרונה היא שבזמן הקיץ, גם ב Greenwich הזמן הוא GMT+1:00, כלומר שעה אחרי… זמן Greenwich. זה בגלל מנגנון בשם Daylight saving – שאסביר בהמשך הפוסט. מנגנון עם וריאציות משלו.

אזרוק פה כמה קישורים למי שרוצה להכיר יותר:

זמן ותאריכים על ה JVM

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

בגרסה 1.0 ג'אווה יצאה עם אובייקט Date שבעצם לא מנהל תאריך, אלא ספירה של שניות החל מתחילת 1970 – כלומר: זמן יוניקס (קרי Unix Epoch, נקרא גם Epoch Time).

כמובן שגם Epoch Time אינו כ"כ פשוט, אם אתם מתעניינים במעט פיקנטריה:

  • Epoch Time החל במקור כספירה של cycles של מעבד (שהניחו שהם 60 הרץ) החל משנת 1971. רק מעט מאוחר יותר – הוצגה הגרסה שאנחנו מכירים.
  • כשבני האדם הגדירו זמן, הם גזרו את היממה מסיבוב של כדור הארץ סביב עצמו, ואז חלקו אותו ליחידות קטנות יותר (שעה, דקה,…) – ביניהן השנייה. הם לא ידעו שכדור הארץ לא מסתובב בקצב אחיד לגמרי, מה שדורש לבצע תיקונים בזמן מדי פעם, להלן דקה מעוברת (או leap second). זמן יוניקס כיום מתעלם מהסטייה הזו.
  • כאשר סופרים שניות מ 1970 במשתנה של 32 ביט, המשתנה מגיע לערך המרבי שלו בשנת 2038 – לא מאוד רחוק, מה שנקרא גם באג 2038.

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

בגרסה 1.1 יצאה גרסה משופרת שבצעה deprecation לרוב המתודות הקיימות של האובייקט Date, והחלפתן במתודות משופרות.

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

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

  • גם אחרי שנים, מפתחים מתבלבלים מתי להשתמש ב Date ומתי ב Calendar. ההסבר הפשוט ביותר שמצאתי הוא ש Calendar נועד לבצע שינויים / חישובים בתאריך, ו Date הוא ה Data Structure ששומר את המידע לאורך זמן. סוג של תכנות פרוצדורלי קלאסי. גם ההגדרה הזו לא מדויקת – אך אין טעם להרחיב.
  • הטיפול במורכבויות של תאריכים לוקה בחסר. הטיפול ב Timezones ו DST הוא קשה ו error-prone למדי. הדרך היחידה להתעדכן בחוקי אזורי-הזמן (שמתעדכנים כמה פעמים בשנה) היה לעדכן גרסה של ה JDK. בתקופת ה On-Premises זה היה פתרון לא מספיק טוב, ומערכות רבות רצו על גבי חוקים לא-עדכניים.
  • לספריה יש כמה Defaults מסוכנים מאוד. לא מזהה שם של Timezone? אין בעיה, נניח שמדובר ב GMT. נראה שהצבת 14 כמספר חודש? אין בעיה – נוסיף שנה לתאריך ונחזור לפברואר, בלי להודיע שדבר כזה קרה. יש אפשרות להחמיר את הבדיקות, אך כנראה בשל ה backward compatibility הנוקשה של ג'אווה, ברירת המחדל היא עדיין הגישה המקלה.
  • המחלקות בספריה הן Mutable, מה שאומר ששימוש חוזר בהן יוביל לבאגים. זה לא ברור מאליו שעלי לייצר Calendar חדש או Formatter חדש, בכל טיפול בתאריך שאני מבצע.
באזור גרסה 6 הפכה ספריית צד שלישי, בשם Joda-Time לסטנדרט במקובל בטיפול בתאריכים על גבי ה JVM. היא שפרה את הטיפול בזמן ותאריכים בכל ההיבטים, והציגה מודלים מוצלחים למדי, אולי אף אפשר לומר – חדשניים.
לא אפרט עליה, מכיוון שכל רעיונותיה המשמעותיים הוכנסו כחלק מהספרייה הסטנדרטית של ג'אווה 8 (השוואה שנייה), והיוצר של Joda-Time הכריז ש "תפקידה נגמר", ועודד את כל משתמשיה לעבור ל Java 8 בהקדם האפשרי.
גרסה 8 – ספריית java.time החליפה את Calendar ו Date, ובעצם הביאה את ה JVM למקום טוב הרבה יותר בכל הנוגע טיפול בזמנים ותאריכים. קפיצת מדרגה משמעותית.
הספרייה מציגה 2 מערכות זמנים שונות, בהתבסס על העבודה שנעשתה ב Joda-Time.

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

שני הרעיונות המרכזיים של java.time

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

למי שלא מכיר הכלל הראשון של עבודה בתאריכים הוא כזה: אם אתם יכולים להימנע בעיסוק ב Timezones – עשו זאת! הסיבוכיות הנלווה ל Timezones ומקרי הקצה האפשריים – הם רבים. java.time מאפשרת פרדיגמה נוחה, ליישום ה best practice הזה.

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

  • המחלקה LocalDate מייצגת תאריך, כמו 28/02/2019.
  • המחלקה LocalTime מייצגת רק זמן, כמו 16:09:00.
  • המחלקה LocalDateTime היא הרכב של שניהם (ירוק + אדום = חום?!)
  • המחלקה ZoneId מייצגת את אזור הזמן, כמו "Asia/Jerusalem"
  • המחלקה ZonedLocalDateTime – היא הרכב של כל הרכיבים: LocalDateTime + ZoneId (ירוק, אדום, וכחול = מרכיבים את הצבע הלבן).
באיזה אובייקט הכי כדאי להשתמש לרוב השימושים?
מחשבה הגיונית ונאיבית, תצביע על השלם – על ZonedLocalDateTime. אם זה "חינם", למה לא לקבל "הכל"?דווקא ההמלצה היא להיצמד לאובייקט ה LocalDate או LocalDateTime (אם נדרש). מדוע? בכדי לפשט את העבודה עם Timezones.Zoned vs. Local

כפי שאמרנו Timezones סבוכים, וקל להתבלבל בהם. ברוב התוכנות, רוב הקוד – צריך להשתמש באותה הפעלה ב Timezone בודד, אז למה להסתבך?

בפרדיגמה A, אנחנו משתמשים באובייקט ה"מלא" הכולל את כל הפרטים. יש לנו יותר סיכוי לעשות טעויות, ולבזבז זמן בחשיבה "האם אני עושה את זה נכון?". טסטים תמיד עוזרים – אבל אם אפשר לפשט בכלל?בפרדיגמה B אנחנו מזהים אזור בתוכנה ("Program Context") שבו כל הפעולות מתבצעות באותו ה Timezone. דוגמה קלאסית: לקוח. אנו רוצים לדווח ולתקשר עם הלקוח שלנו רק על גבי ה Timezone שלו. אנו שומרים את ה ZoneId פעם אחת בצמוד ל Context, וכל תאריך שנכנס ל Context לטיפול עובר המרה ל ZoneId הזה.
מכאן והלאה כל הפעולות הופכות לפשוטות ובטוחות יותר: אנו משווים תפוחים לתפוחים.
מדי פעם עלינו להשוות או לבצע חישוב עם ערך ב Timezone אחר או Instant – ואז אנו יכולים להרכיב חזרה את ה ZoneId לאובייקט – ולבצע את הפעולה.חשבו על LocalDate לא רק כמקומי לישות בעולם האמיתי ("לקוח"), אלא גם מקומי בקונטקסט המטפלל באותה הישות.

2 מערכות זמנים, זו לצד זו

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

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

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

מתי נכון להשתמש בכל מערכת?

מערכת ה Epoch

מערכת זו כוללת את ה Instant ואת ה Duration. היא פשוטה למדי ונצמדת ל Java Epoch. בג'אווה, מאז ומעולם, ספרו את הזמן מתחילת 1970 – אך ברזולוציה של מילישניות. הערך נשמר בשדה 64-ביט, ויכול לתאר תאריכים עד שנת מיליארד.

כל המרה בין Unix Epoch ו Java Epoch כוללת הכפלה / חילוק פשוט ב 1,000.
בכדי לתאום ל Unix Epoch, גם Java Epoch מתעלמת מ Leap Seconds.

מחלקת ה Instant מחזיקה את ה Java Epoch ומאפשרת עליה פעולות. המחלקה Duration מתארת מרחק בין שני Instants, ומשמשת לפעולות חישוב של הפרשי-זמנים.

טבלת השוואה בין שתי מערכות הזמנים של java.time

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

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

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

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

מהם הסימנים לכך שאנחנו משתמשים לא נכון ב Instant?

  • אם זמן שבן אדם מדווח עליו נשמר כ Instant – זה רמז לשימוש בסבירות גבוהה איננו נכון. טעות נפוצה.
  • עוד כלל אצבע שאני אוהב, הוא שאל לנו לבצע פעולת השוואה בין epoch.
    • תאורטית, epoch הוא ברזולוציה אינסופית ולכן ההשוואות הנכונות הן רק: האם זמן נתון הוא לפני או אחרי ה Instant. לכאורה, נדיר מאוד שיהיו שני Epochs עם ערך זהה בדיוק – ולא נכון להסתמך על זה.
    • המחלקה Instant עובדת כברירת מחדל במילי-שניות, אך ניתן גם לעבוד ברזולוציה של ננו-שניות.
  • אם אנו עסוקים בהמרות של Timezones ל Instants – אז כנראה שאנו עושים משהו לא נכון. Instant אמור לתעד אירועים שלא תלויים ב Timezone, וריבוי המרות שכזה מעיד שאנו עושים בו abuse.
הנה דוגמה קטנה שתעזור להמחיש את הנקודה. יכולים לנחש מה הקוד הבא עושה?
instant.minus(10, ChronoUnit.YEARS)
נכון! הוא זורק Exception. יש משהו לא נכון, בביצוע פעולות ברזולוציה של שנה על Instant. זו לא הייתה כוונת המשורר.
מהו שימוש סביר? על יחידת זמן שקטנה מיום – תתקבל בברכה. ימים הם עקביים ואחידים ב Java Epoch. חודשים – כבר לא, ולכן כל יחידת זמן של חודש ומעלה – תזרוק שגיאה.
מדוע לא חסמו את האפשרות, ע"י הגדרת Enum מקביל ל ChronoUnit, המתאים רק ל Instant?
אני מניח שהשיקול הוא החיבור בין שתי המערכות הזמנים. כפי שתראו – המעבר בין שתי המערכות הוא מאוד טבעי ונוח, והגדרה של ChronoUnit שונים – היה מקשה על המעבר.
מערכת ה Date/Time
 

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

מה הסימנים שאנחנו משתמשים לא נכון במערכת ה Dates?

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

סיכום

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

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

פשוט קוד: טיפול בשגיאות

אני עומד לעסוק בנושא בסיסי מאוד: Exception Handling. בסיסי – אך שיש מה לעסוק בו.

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

הערה: אני מניח שלא מדובר במערכת Embedded  או Realtime שם נמנעים מ Exception בשל העלות של stack unwinding. בשפות / מערכות אחרות (כמו Golang) – החליטו לא להשתמש ב Exceptions על מנת להיות צפויים ומבוקרים יותר. למשל: כאשר Exception נזרק ממקבץ של 4 שורות קוד – אני לא יודע בוודאות איזו שורה זרקה את ה Exception, ואולי אף איני לצפות את ה Exception שעלול להיזרק מכל שורה – מה שיוביל אותי לטיפול כללי ופחות מדויק בחריגה.

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

  • שפת ג'אווה מתייחסת בשונה ל Checked Exceptions (היורשות מהמחלקה Exception) – אשר יש חובה קוד להתייחס אליהן, ו Unchecked Exceptions (היורשות מהמחלקה RuntimeException) המתנהגות כמו Exceptions ברוב שפות התכנות, ואפשר להתייחס אליהן – או להתעלם ואז הן יעברו לטיפול מי שקרא לי.
    • הרעיון החדשני והמעניין של Checked Exceptions (המושפע מרעיון בשם checkers בשפת OCaml) הוכר בפועל כהחמצה, ולא הועתק (למיטב ידיעתי) לשום שפת תכנות נפוצה אחרת.
    • גם בשפת קוטלין "ירדו" מהרעיון, ואין צורך להתייחס אחרת לחריגות היורשות מהמחלקה Exception.
  • Error הוא ענף אחר בהיררכיית ההורשה, ושגיאה (Error) שתיזרק לא תיתפס כחריגה (Exception) – כי היא מחלקה מטיפוס אחר. ה Error נשמר למצבים חמורים שהאפליקציה לא אמורה לטפל בהם – ובעיקר נזרקים ע"י ה JVM (כגון OutOfMemoryError). לא זכור לי שאי פעם נתקלתי ב Error בפרודקשיין, בטח לא כזה שקוד היה יכול למנוע / להתמודד איתו.
    • IntellJ (ה IDE) מימש משפטי TODO (כאלו שלא נכתבים כהערה) ככאלו שיזרקו Error. אם שכחתם לכתוב את הקוד שהתכוונתם אליו – ואתם מריצים את הקוד, אנחנו לא רוצים שהפרט הזה ייתפס כ Exception ויכתב לשורה ללוג אשר קל לפספס. הנה שימוש נכון ל Error.
 
חזרה לסיפור שלנו: אז איזה סוג של Exception כדאי לזרוק?
  • ברור לי שזה לא Error. (למרות שנתקלתי במקרים בהם אנשים זרקו Errors ב flows אפליקטיביים).
  • אני לא מתעסק בהבחנה בין Checked ל Unchecked Exception – וטוב שכך.
  • יש מן עצה כזו שאומרת "Favor the use of standard exceptions", אך עדיין נותר לי מבחר גדול של Exceptions רק מתוך הספריות הסטנדרטיות של ג'אווה:
הרשימה באמת, היא אפילו יותר ארוכה
 
אני רוצה לכתוב קוד טוב יותר וקריא יותר – אבל איך בוחרים?
 
צעד הגיוני ונפוץ יחסית, הוא ללכת ולהתייעץ בכל מיני "מילונים" לסוגי החריגות השונות, ומתי להשתמש בהן:

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

  • יש כאלו שיבחרו ב IllegalArgumentException – כי הפעילו את המתודה עם ארגומנט לא חוקי.
  • יש כאלו שיבחרו ב IllegalStateException – כי בכלל ארגומנט לא חוקי לא אמור להגיע כך ללב המערכת. זו תקלה במצב המערכת. "אם הגענו עד לכאן – זו כבר תקלה ב state".
  • יש כאלו שיבחרו ב NotFoundException – באמת לא מצאתי את הלקוח הזה.
  • יש כאלו שיבחרו ב UnsupportedOperationException – זה לא חוקי לשלוף פרטי לקוח עם מזהה לא נכון (?!).
  • יש כאלו שפחות מתאמצים ופשוט זורקים RuntimeException (החריגה ארעה בזמן ריצה) או פשוט סתם Exception.
  • יש כאלו שיותר מתאמצים ומגדירים טיפוס חדש של חריגה למצב הספציפי, למשל: CustomerNotFoundByIdException. 
  • אפילו ראיתי מקרה בו בתסריט כזה נזרק SecurityException. אני מניח שטיפול ב id שגוי של משתמש הוא גם בעייתי בהיבטי האבטחה של המערכת.

אם מפתחים שונים בוחרים חריגות מטיפוסים שונים לאותו המצב, אז:

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

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

בחזרה למהות

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

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

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

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

  1. Rollback של טרנזקציה / פעולה, או פעולות cleanup – זה קורה לפעמים.
  2. כתיבת הודעת לוג (שגיאה / אזהרה) – בכדי ללמוד עליה ולטפל ידנית במצב ו/או לשפר את המערכת שמצב זה לא יקרה בשנית. את זה עושים כמעט תמיד.
  3. הצגת אינדיקציה למשתמש-הקצה ב UI, ברמת פירוט כזו או אחרת, שמשהו השתבש – אם קיים משתמש כזה.

זהו. זו בגדול התוצאה.

ברוב המקרים – הטיפול האמיתי ב Exception יהיה מעבר ל Flow בקוד:

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

הנה קוד לדוגמה שתופס Exception:

try {
...
} catch (e: Exception) {
  logger.error("something went wrong $e")
}

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

היי, רגע!

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

  • לא שלחנו את ה exception עצמו ל logger. זה אומר שלא יודפס ה stack-trace – וזו בעיה חמורה, כי יהיה קשה מאוד להבין מה השתבש בחקירה שתגיע מאוחר יותר.
    • אני מבקש בזאת מכותבי ה logging framework הבא לג'אווה שהחתימה למתודה (…)logger.error תחייב לספק ארגומנט מסוג ?Exception. השכחה לשלוח את ה Exception ללוגר היא מספיק מזיקה. אם אנו רוצים לכתוב ללוג error שלא קשור ל Exception – אני מעדיף שנשלח ערך null.
  • בלענו את ה Exception. האם זו הייתה הכוונה?
    • לפעמים כן – וזה בסדר. מספיק שכתבנו ללוג (אבל נוסח ההודעה יהיה יותר כמו "failed to do…") ומישהו יטפל בזה אח"כ.
    • לפעמים לא – וזו יכולה להיות ממש תקלה. 
      • גם במקרה כזה הייתי שמח אם ברירת המחדל של השפה הייתה להמשיך ולזרוק את השגיאה, עם אפשרות להוסיף משפט break אולי swallow או משהו – במקרים בהם לא רוצים במודע להמשיך לזרוק את השגיאה.
  • הדפסנו את האובייקט של ה Exception – ולא את הודעת השגיאה (e.message).
    • ברוב הפעמים, מימוש ברירת-המחדל ל ()toString של מחלקת ה Exception יהיה message – ואז הכל בסדר.
    • במקרים בהם המימוש הוא אחר, למשל: כתובת האובייקט בזיכרון – איבדנו את ההזדמנות לאסוף מידע שימושי לגבי השגיאה.

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

fundoA() {
try {
  ...
  doB()
  ...
  } catch (e: Exception) {
    logger.error("something went wrong: $e.message", e)
    throw e
  }
}

fundoB() {
try {
  ...
  doD()
  ...
  } catch (e: Exception) {
    logger.error("Dubi: something din't work as expected: $e.message", e)
    throw e
  }
}

fundoD() {
try {
  ...
  } catch (e: Exception) {
    logger.error("doD: WTF?! $e.message", e)
    throw e
  }
}

קרתה שגיאה אחת, אבל בלוג יכתבו 3 stack trances שעשויים להראות כמו 3 שגיאות. ה stack traces הם כמעט חופפים, כאשר בכל פעם נוספת רק עוד שורה אחת.
אפשר בקלות לבזבז זמן בחקירה של הלוג שנכתב ע"י דודי (כלומר: doD) או הלוג שנכתב ע"י דובי (doB) – בעוד חסר לנו ההקשר החשוב שהתחלנו לפעול בעצם מ doA.

בגדול:

  • ב catch clause בחרו: או שאתם מדפיסים הודעה ללוג – או שאתם זורקים שגיאה הלאה.
    • כל Framework / WebServer שמכבד את עצמו יתפוס את כל השגיאות שהתרחשו – ויכתוב אותן ללוג.
    • כתיבת ה Stack trace ללוג ברמה הגבוהה ביותר – תספק את מירב המידע, ולכן זו השגיאה שאנו רוצים לחקור. חבל לכתוב הודעות כפולות ללוג.
  • הערת משנה: אם יש stack trace אז יהיה כתוב איזה פונקציה נקראה. עדיין המון אנשים אוהבים להוסיף בעצמם את שם המתודה (ולפעמים גם את שם המחלקה) להודעת השגיאה – וזה דיי מיותר. נחשו מה קורה כאשר עושים rename לשם המתודה?
    • בהודעת השגיאה חשוב לציין מידע חדש ומעניין. למשל: פרמטרים מסוימים של הריצה – שלא תהיה לנו גישה אליהם מאוחר יותר.

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

fundoA() {
try {
  ...
  doB()
  ...
  } catch (e: Exception) {
    throw Exception("failed ... id=$id", e) // = java's "new Exception"
  }
}

fundoB() {
  ...
  doD()
  ...
  // if you have nothing smart/new to say - spare our time.
}

fundoD() {
try {
  ...
  } catch (e: Exception) {
    throw Exception("Failed DoDing: x=$x, y=$y", e)
  }
}

ה Framework הוא זה שיתפוס את ה Exception ויכתוב אותה, ואת ה stack trace המלא – ללוג. זה ה Best Practice שנקרא global exception handling.

שווה לציין, שב Web Frameworks ההתנהגות המקובלת היא שה Framework תופס את ה Exception ומוציא החוצה הודעה לקונית בנוסח "HTTP 500 internal server error". אנחנו לא רוצים לשלוח ברשת payload גדול של כל ה stacktrace, ומבחינת אבטחה אנחנו לא רוצים לחשוף החוצה את ה internal של המערכת שלנו וללמד את התוקף הפוטנציאלי מה קורה. לכן, כברירת-מחדל, הודעות השגיאה ששקדתם עליהן יגיעו ללוג – ולא ללקוח.

בואו נחזור לשאלה האחרונה ששאלנו:

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

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

התשובה

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

מי הלקוח שעומד לתפוס את החריגה מהטיפוס המסוים, ומה הוא עומד לעשות עם המידע הנוסף שהטיפוס הזה סיפק לו?

זו שאלת המפתח, שלצערי כמעט ולא נשאלת.

אם מי שעומד לתפוס את ה Exception הוא ה Framework בכדי לכתוב אותו לוג – אין כל טעם לשגר Exception מטיפוס מיוחד.
אם מי שעומד לתפוס את ה Exception הוא קוד, בכדי לבצע באופן גנרי rollback / resource cleanup – אין כל טעם לשגר Exception מסוג מיוחד.

המצב היחידי שבו יש טעם לשגר שגיאה מסוג מסוים – הוא אם מישהו יחכה לסוג הספציפי של ה Exception.

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

אני אצטט לרגע אנשים שחקרו את הנושא עמוק ממני:

"Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result – decreased productivity and little or no increase in code quality." — Joseph R. Kiniry

ובכן זה התסריט המשמעותי היחיד לשימוש בטיפוס ספציפי של Exception:

try {
...
} catch (e: CustomException) {
  // do some cleanup / revert logic
} catch (e: Exception) {
  throw Exception("Failed DoDing: x=$x, y=$y", e)
}

כאשר קוד מתייחס לטיפוס הזה, ומריץ איתו branching אחר של הקוד.

חשוב לציין ש cleanup logic צריך ברוב הפעמים לשבת ב finally clause – ולרוץ ללא קשר לסוג התקלה שהתרחשה.

התקדמנו.

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

נחזור רגע להמלצה המוכרת: "Favor the use of standard exceptions".
שימו לב שאם CustomException יהיה Exception סטנדרטי כמו IllegalArgumentException אזי הוא יוכל להיזרק לא רק מהקוד שלנו – אלא מכל קוד אחר במערכת שבחר "להשתמש ב Standard Exceptions". אולי אלו 3rd Party, אולי גם הספריות הסטנדרטיות של ג'אווה.

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

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

מתי, אם כן, אפשר להשתמש ב Standard Exception?

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

שווה לציין שגם הספריות הסטנדרטיות של ג'אווה אינן עקביות לגמרי ב Exceptions אותן הן זורקות, או אפילו לגבי השימוש ב Checked ו Unchecked Exceptions.

השאלה היותר חשובה, היא מתי לתפוס Standard Exceptions?

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

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

int day = Instant.now().get(ChronoField.DAY_OF_MONTH);

יזרוק חריגה "סטנדרטית" (כי הקוד הוא של ג'אווה [א]) מסוג UnsupportedTemporalTypeException.

למה? בכדי להדגיש ש Instant (המייצג של epoch ב Java Time APIs) נועד לשימוש ע"י מכונה, ולא בכדי לייצג מידע על תאריך שיגיע למשתמשים בני-אדם.

האם זה נכון, לזרוק שגיאה בשל "שימוש שנראה שגוי בספריה"?

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

אני רצה לסיים ולציין תנאים מומלצים לשימוש ב Standard Exception:

  • החריגות הסטנדרטיות הללו הוגדרו במערכת שלכם, ולא יכולות להיזרק בטעות ע"י ספריית צד-שלישי שנכתבה ע"י מישהו שלא מכיר את הכללים שקבעתם לשימוש בחריגה.
  • ישנם כללים ברורים וידועים – מה אומר השימוש בחריגה הזו.
  • לחריגה הספציפית הזו, יש משמעויות ברמת המכונה / קוד במערכת – והן לא נועדו בכדי להסיר אחריות מהודעת השגיאה שתדווח.
דוגמה אחת היא javax.ws.rs.WebApplicationException
  1. היא אמנם "סטנדרטית בתעשייה" ולא ייחודית לכם. לכן, נכון לזרוק אותה – אבל כדאי להימנע ולהסתמך עליה בלכידת Exception לצרכים אפליקטיביים. היא עשויה להיזרק ע"י כל אחד.
  2. יש לה סמנטיקה משמעותית וברורה ברמת המכונה: Web Applications Servers יתפסו אותה ברמתם – אבל יעבירו את הודעת השגיאה שהוגדרה בה, כמו שהיא, ללקוח. אפשר גם לציין במחלקה הזו גם HTTP Status קוד שיעבור גם הוא ללקוח. למשל: HTTP 403 – הוא קוד בעל משמעות חשובה.
    1. מי שזורק את WebApplicationException, חשוב שיבין את הסמנטיקה שלה – ולא יכתוב בה הודעת שגיאה עם מידע פנימי שלא אמור להגיע ללקוח שביצע את הקריאה.

בדוגמה אחרת, הגדרנו ב Next-Insurance טיפוס Exception פנימי בשם RobinUserInputException.

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

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

סיכום

אני מקווה שהצלחנו להאיר ולהדגים כמה עקרונות חשובים לגבי Exception Handling.

לצערי, הרבה חשיבה מסביב ל Exception Handling מוקדשת ל Tooling (מחלקות שונות של Exceptions) – ולא למהות השגיאה, וכיצד לטפל בה.

חדי העין אולי שמו לב שההמלצה שסתרתי בפוסט, "Favor the use of standard exceptions" – מגיעה מספר מכובד (של מחבר מכובד) בשם Effective Java. הנה סיכום הדברים:

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

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

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

—–

[א] אני קצת צוחק, כי זו באמת חריגה ממוקדת, שלא משתמשים בה מחוץ ל Java Time API – אבל יש כאלו שרואים כל חריגה של הספריות של ג'אווה כחריגות "סטנדרטיות".

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