על המגבלות של REST – בשירות מערכות ווב בנות-זמננו

כבר כתבתי על REST בבלוג: ב 2011 (בהרבה התלהבות) וגם ב 2016 (במתינות) – מה ששיקף תהליך אישי של התלהבות רבה מ REST, והתפכחות לאורך הזמן.

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

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

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

על אלו מגבלות אנחנו מדברים? הרי REST הוא כמעט-מושלם, לא?

למי שלא זוכר / לא היה בתעשייה בשנות ה-2000 אזכיר בקצרה את ההיסטוריה:
בתחילת שנות ה-2000, כאשר התברר לתעשיית התוכנה שמערכות תוכנה הולכות ונעשות מורכבות יותר, ויותר – ולכן רבות מהן הופכות למערכות מבוזרות (יותר משרת אחד) – ומכאן צמח רעיון ה Service-Oriented-Architecture (בקיצור: SOA) לארגון של מערכות מבוזרות שכאלו. מייקרוסופט הציעה את פרוטוקול ה SOAP (לשעבר XML-RPC) כבסיס לתקשורת בסוג הארכיטקטורה הזו, מה שהתקבל במהרה ע”י יבמ, ושאר חברותיה.

החברות שהובילו אז את התעשייה (IBM, HPE, Microsoft, וכו’) קידמו את רעיונות ה-SOA לכדי מציאות, ועבדו על Suite של פרוטוקולים בשם *-WS שיפתרו את כל הבעיות התקשורת והאינטגרציה של מערכות Enterprise (מה שהיה מעניין באותה תקופה).

סט הפרוטוקולים המרכיבים את *-WS

בחבילת הפרוטוקולים של *-WS היה כמעט הכל, לקחו בחשבון כמעט כל צורך של Enterprise Software, מלבד אחד חשוב מאוד: פשטות.

לא כולם אהבו את SOAP ו *-WS, אחד מהם היה רוי פילדינג אחד ממגדירי התקן של HTTP. בעיניו – HTTP היה “הפרוטוקול של האינטרנט” ועל אף ש HTTP נועד להגשת “דפי ווב”, הוא חשב שפרוטוקול אפליקטיבי צריך להשתלב בצורה הרמונית (אולי “טבעית”) עם HTTP וה Web Standards הקיימים, ולא להתעלם ולהחליף אותם. זו הייתה מן גישה מוקדמת של “קיומיות וובית” – שהייתה חדשה ומרעננת באותה התקופה. היום קוראים לזה “Web-native”.

פילדינג פרסם באותה תקופה עבודת דוקטורט, שבה פרק מס’ 5 (המפורסם!) הציג סגנון ארכיטקטוני בשם Representational State Transfer (בקיצור: REST) למערכות ווב, ארכיטקטורה שמשתלבת באופן מאוד טבעי עם ה World Wide Web ו HTTP.

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

תוצר לוואי של “ארכיטקטורת ה REST”, כפי שהוגדרה בעבודת הדוקטורט של פילדינג, היה צורת התקשורת בין חלקי המערכת, מה שנקרא כיום RESTful APIs. כלומר: צורת תקשורת של העברת הודעות JSON (במקור: XML) על גבי HTTP תוך השתלבות הרמונית באופיו והתנהגותו של HTTP והווב. ל HTTP יש מנגנונים כגון Caches, ו resource naming (להלן ה URL) הנכונים גם לתקשורת אפליקטיבית – שאם משתמשים ב HTTP, לא צריך לפתח מחדש. העולם מלא בכלים (למשל: דפדפן, Firewalls, Postman, swagger) שעובדים כבר עם HTTP, והמבנים של HTTP (כלומר: URL, Status Codes, Methods, Headers, Cookies) – מוכרים היטב* בתעשייה, ומקלים על אימוץ הטכנולוגיה.


* האמירה ש”כולם מכירים HTTP” היא סופר-מקובלת בתעשייה, אבל פה אני צריך להשתעל קצת – ולהתוודות שהיא לא לגמרי נכונה. גם כיום, בעידן הידע, ה Geektime, ושנים לאחר ש RESTful APIs הפכו לסטנדרט הלא-מעורער של התעשייה – אני נתקל בחוסרים גדולים של מהנדסי תוכנה בהבנה בסיסית של פרוטוקול ה HTTP. נכון יותר לומר ש HTTP מתועד היטב ויש עליו אינסוף חומר ו References – ולכן הוא נגיש יותר מפרוטוקולים proprietary אחרים.

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

להיכן ממשיכים מכאן?

REST איננו תקן (אלא פרק בעבודת דוקטורט) והפרשנות ל RESTFul APIs אף יותר מזה – היא פרשנות, שכל אחד יכול לקחת למקום שונה. עם הזמן צצה הביקורת על “גנבי-הדעת” שפשוט מעבירים XML על גבי HTTP וקוראים למה שעשו “RESTful API” – מבלי באמת לכבד את פרוטוקול ה HTTP ו/או להשתלב בעקרונות הווב המקובלים (resources, openess, linking). מודל הביקורת שתפס יותר מכל להערכת “יישום RESTful APIs” הוא Richardson’s Maturity Model (בקיצור RMM):

הפסים שאתם רואים בשכבה הנמוכה הם בוץ, רפש – שדובקים בכל מי שמסתפק ב “XML/JSON over HTTP” ועוד קורא לזה REST.
גם אני, כשהייתי עוד REST Evangelist – כעסתי על כל מי שנוהג כך. מה, אין כבוד?

ע”פ מודל ה RMM, לשלוח XML או JSON על גבי HTTP הוא נקודות האפס הבסיסית ביותר (והמוערכת הכי פחות). משם אפשר להתקדם לעיצוב “נכון” של URLs, שימוש “נכון” ב HTTP Verbs, ועד הגביע הקדוש של Hypermedia transformations. השתלבות המערכת עם “טבע ה HTTP והווב”.

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

שאלה דומה היא שאלת ההשתלבות ב HTTP/Web ומידת ההצמדות לרעיון ה REST: יישום של RESTful APIs ברמה 3 לפי ה RMM אולי תעניק לכם חיים בעולם הבא בקרב “קהילת נאמני ה REST” – אך כנראה גם תתברר כעבודה הנדסית בינונית עד איומה, ברוב המקרים. למשל: רמה 3 טוענת שכל קריאה לשרת צריכה לבוא מתוך “גלישה טבעית בווב”. באתר ווב אני מתחיל מאתר הבית – ובסדרה של קישורים מגיע לדף בו אני מעוניין. באופן דומה, הטענה היא ששרתים שמדברים זה מזה – צריכים להתקדם למטרה בצעדים. לא לשאול: “כמה כסף יש בחשבון של לקוח X?” אלא תמיד יש להתחיל בשאלה “מי אתה? אלו לקוחות יש לך? איזה מידע יש לכל לקוח? האא… ורק אז לשאול: אז כמה כסף יש בחשבון של לקוח X?”.

אם יש לנו שרתים “קרובים” (אותה מערכת) שמתקשרים הרבה, ההבדל בין 4 קריאות (3 “נימוסים” + 1 תכל’ס) על כל בקשה מול קריאה בודדת (רק תכל’ס) – היא משמעותית. רק במקום עבודה אחד בחיים ראיתי יישום של Hypermedia transformations – ואולי עשינו HATEOAS, אבל סיבכנו מאוד את המערכת. ממש ברמת העוול / הטיפשות.

מכאן עולה השאלה המתבקשת: “כמה RESTFulness” כדאי לעשות? ואיך יודעים לאמוד מתי ההצמדות ל RESTfulness היא תורמת, ומתי היא כבר מזיקה?

אז מהן המגבלות העיקריות של RESTFul APIs?

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

Hypermedia Transformations / HATEOAS – ברוב המקרים, מבחינה הנדסית – זה פשוט טירוף לשמו. יש להימנע – וחבל להמשיך ולעסוק בעניין.

HTTP Verbs הם שפה מגוונת ועשירה למערכות פורומים / Wikis – אך דלה ומוגבלת עבור מערכות ווב מודרניות ומורכבות. הנה ההגדרות מתוך וויקיפדיה:

והנה כמה מגבלות יום-יומיות ,שכנראה שכולנו נתקלים בהן:

  • קריאת GET איננה כוללת Body. כאשר אנחנו רוצים לעשות קריאה שהיא safe (לא משנה את state המערכת), אך רוצים להעביר פרטים מגוונים של הבקשה – אנחנו חסומים לשימוש ב URL לבדו. זו נשמעת מגבלה קטנה, כי על URL אפשר גם לשים 6,000 ויותר תווים בדפדפנים של ימנו – אבל זה לא כ”כ מעשי:
    • Query String הוא נחמד, וקל להשתמש בו בהפשטה של מילון (<Map<String, String) – אל זו עדיין צורה מגבילה להעברת מידע. אין היררכיה, וקשה לתאר מבנים מורכבים יותר. בעיה.
    • על זה נוסיף את הצורך ב URL encoding, כי יש תווים שאסור להציב על ה URL. למשל: רווח (להלן ה %20 שאתם רואים ב URLs). מהר מאוד ה URL הופך ללא קריא, ולכן פחות שימושי.
    • אפשר לקודד JSON (מבנה גמיש והיררכי) על ה URL – אבל זה מקרה קיצוני יותר מהאייטם הקודם. זה כבר לא פשוט, לא שקוף, ולא Web native. פספסנו לגמרי את הרעיון של שימוש ב HTTP בגלל “קריאות ופשטות”. כבר כבר יותר RPC.
    • אותה בעיה – קיימת גם עבור פעולות Delete (לכאורה Body הוא optional – אבל כלים רבים יחסמו אותו).
    • הפתרון הנפוץ הוא לבצע פעולות POST עבור קריאות שהיו אמורות להיות GET. ב POST אפשר להעביר body – אבל בעצם השימוש ב POST התרחקנו מההקפדה על הסמנטיקה של HTTP – שזה כל העניין. לא ניתן להבחין כבר בין POST שהוא safe לכזה שלא, ללא מידע נוסף. ראיתי מגוון גישות להתמודד עם החריגה הזו (POST as GET) – כל גישה, וה tradeoffs שלה.
  • חוסר בהירות בין POST ל PUT
    • פעולת PUT ב HTTP משמשת להחלפה של משאב שנמצא ב URL בתוכן הנשלח ב Body, וזו פעולה Idempotent – ניתן לבצע את אותה הפעולה מספר פעמים, מבלי שהתוצאה תהיה שונה מביצוע יחיד של הפעולה.
    • פעולת POST ב HTTP מעדכנת / שולחת מידע למשאב שנמצא ב URL – שזו הגדרה כללית הרבה יותר. הפעולה בהגדרה איננה Idempotent (כלומר: חלק מהפעמים היא עשויה להיות – אך אסור להסתמך על זה).
    • מכאן ועד תרגום של ה Verbs של PUT ו POST ל Create/Update/פעולה אחרת – יש מגוון של פרשנויות, לפעמים פרשנויות סותרות המתבססות על דקויות נכונות מתוך התקן של HTTP. במערכת Wiki, קביעת גבולות ברורים בין PUT ל POST היא דבר קל – במערכת מוצר מורכבת ומסועפת, תקן ה HTTP משאיר אותנו “חשופים” לדילמות, פרשנויות, והתלבטויות.
    • אם אתם לא מכירים את הדיונים הרבים, הנה מאמר תמציתי אחד שמסביר מדוע POST ו PUT הם לא פשוט Create ו Update.
  • בלי להכביר במילים, חוסר בהירות דומה קיימת גם בין PUT ו PATCH. הנה מאמר של המחבר הנ”ל לגבי הדילמה הזו. יש גם מאמר על PUT vs. PATCH vs. JSON-PATCH, ועוד אחד, ועוד אחד, ועוד אחד – כל אחד עם טענה מעט שונה.
הנה פרשנות אחת (ומקובלת) מתי להשתמש ב PUT ומתי ב POST ליצירת אובייקט. יש עוד כמה פרשנויות סבירות והגיוניות.
  • אולי המגבלה הכי קשה באימוץ “REST על מלא-מלא” בהיבט ה Verbs הוא הדלות ואי-העדכניות של ה Verbs המקוריים של HTTP לצרכים ועושר-הפעולות של מערכות תוכנה מודרניות. REST גרס שלא צריך להמציא “שפה חדש” – ואפשר להשתמש ב HTTP, אבל בשפה הזו יש רק 9 פעלים (עוד כ 20 נרשמו כהצעות) – אך אנו זקוקים ל 300 פעלים בכדי לקיים שיחה אינטליגנטית בדומיין העסקי שלנו? דומיין שהוא הרבה יותר מ CRUD?
    • אולי עבור מערכת וויקי, 9 פעלים מספיקים: עמוד יכול להיווצר / להיכתב מחדש (PUT), אפשר לכתוב עליו הערות (POST), ניתן לעשות בו תיקונים קטנים (PATCH) ואפשר למחוק אותו (DELETE) – הנה הסתדרנו!
    • במערכת של חברת ביטוח (למשל), בה על פוליסת ביטוח ניתן לבצע מגוון של פעולות:
      • לחתום על הפוליסה
      • להתחיל את הפוליסה
      • להקפיא / להפשיר את הפוליסה.
      • להוסיף מוטבים.
      • להוריד עותק של המסמך
      • לשלוח את המסמך לצד שלישי
      • לשלוח אישורים על המסמך לצד שלישי
      • לעדכן פרטים של בעל הפוליסה
      • לעדכן פרטים לגבי תוכן הפוליסה
      • לשנות כיסויים בפוליסה
      • לבדוק את הכיסויים בפוליסה
      • לבדוק סימולציה של מקרה – האם מכוסה ע”י הפוליסה
      • לבצע ביקורת על הפוליסה (Audit)
      • ועוד פעולות רבות…
      • הפוליסה מתוארת בצורה RESTFul למהדרין, כ URL כמו customers/{customer_id}/policies/{policy_id}/ אבל אין לי סיכוי לתאר את מגוון הפעולות הנ”ל בעזרת POST/PUT/PATCH בלבד. כל הרעיון ש “נראה את ה verb בלוגים השונים – ונבין מה התרחש” – מאבד ערך אם עבור כל הפעולות הנ”ל אני משתמש ב POST verb (פתרון מקובל אחד). יתרה מכך: אני צריך להשלים את הסמנטיקה שמבדילה בין הפעולות השונות איפשהו (על ה URL? על ה body? כ Header? כ Query Param) – אין מקום מוגדר וברור לעשות זאת. מגוון אפשרויות, עם tradeoffs וחריגות שונות מתקן ה HTTP והכוונות מאחוריו.
  • גם ברמת ה URI, אנו נתקל במגבלות: תיאור של URL היררכי לכל אובייקט במערכת – הוא לא דבר חד-משמעי ופשוט כפי שהיה במערכות פשוטות פעם (שוב: וויקי/מערכת פורומים). אקצר מאוד כדי לא להאריך את הפוסט אבל בעיקר:
    • לא תמיד יש היררכיה בין אובייקטים. לפעמים יש אובייקטים שהם שכנים “Siblings” – ואין דרך טובה לתאר את הקשר בהיררכיה (על ה Path).
    • לפעמים פרטי הזהות של אובייקט הם מורכבים / ארוכים מדי בכדי להיכנס ל URL.
    • ב REST/HTTP אין סמנטיקה לתאר Bulk operation – פעולה על מגוון אובייקטים. ישנן workarounds אך הם שונים ומגוונים, ולכל אחד – המגבלות שלו.
  • ב REST/HTTP אין דרך מוסכמת לתאר גרסאות ועתיות של APIs. באופן אישי אני משוכנע שהשתתפתי בעשרות שעות של דיונים בנושאים הללו, בארגונים שונים ועם אנשים שונים – כי אין תשובה פשוטה של REST שניתן לגשת ולהציג אותה. הנה סדרה של גישות ודיונים בנושא, למי שרוצה לצלול… הנה גם תקציר על 4 גישות מקובלות.
מישהו ריכז כאן דוגמאות וגישות ל versioning ב REST (והיד עוד נטויה). רק להדגים שהנושא אינו “סגור בטון” ומקובל על כולם באותו האופן.
  • עוד “שפה” שהייתה טובה / עשירה בזמנה – אך היום היא מוגבלת מדי היא ה HTTP Status codes. קודים כגון OK 200 או 401 (הקוד נקרא “Unauthorized” אך הכוונה היא בעצם ל Unauthenticated) – טובים ומתאימים לשכבת התשתית, אך מזמן כבר דלים מכדי לתאר את שכבת האפליקציה. להזכיר: HTTP הוגדר עבור ניהול דפי וויב (“documents” או “resources”) ולא לתיאור של אפליקציות עסקיות מורכבות. למשל:
    • האם 404 מתאר endpoint שלא קיים, אובייקט שלא נמצא, או פריט מידע לפעולה שלא נמצא? איך מבדילים בין המקרים אם חזר רק הקוד “404”? דיון. לא פעם מפתח אחר מתכוון לדבר אחד, ומפתח אחר – מבין דבר אחר.
    • עוד דוגמה קלאסית היא המשמעות של 400 (הקרוי Bad Request).
      • ע”פ התקן – השרת לא יכול או לא יטפל בבקשה מכיוון שלא הוגשה בקשה נכונה מצד הלקוח (פורמט שגוי, שליחה ליעד הלא נכון)
      • השימוש הנפוץ הוא הרחבה של הקונספט “משהו לא טוב בבקשה” (ולא רק בפורמט/יעד) למשל: נשלח id שלא קיים של אובייקט. אבל…
        • אם נשלחו בבקשה כמה Ids – אז איזה מהם?
        • או אולי, אי אפשר לבצע את הפעולה מכיוון שעבר התאריך.
        • או אולי, לא ניתן לעשות פעולה בסכום נכון, כי החשבון מוקפא, או כי אלו פרטים אחרים אינם תואמים?
      • בקיצור: שום קוד סביר לא בצד הלקוח לא יכול להסתמך על קוד HTTP 400 להחלטה משמעותית של שינוי ב Flow ללא פרטים נוספים. נדרשים קודים מדויקים למגוון גדול של תקלות אפשריות, אז מה החשיבות להחזיר גם קוד HTTP 400? ראיתי שנים אנשים שהקפידו ותיקנו שוב ושוב קוד שהחזיר HTTP 500 להחזיר HTTP 400 – אך מעולם לא נתקלתי בערך שצץ מהפעולה הזו… פשוט עבודה לחינם.
  • עוד נושאים שאפשר לציין ש REST/HTTP לא נותנים להם מענה:
    • תקשורת אסינכרונית
    • Notifications – התקשורת של HTTP היא בקשה/תשובה. אם השרת המרוחק רוצה להודיע לי משהו ביוזמתו – כיצד הוא יעשה זאת?

אלו צרכים בסיסיים, שכנראה רוב מערכות הווב של ימנו זקוקות להם.
חשוב לציין ש HTTP/2 הוא כבר פרוטוקול בינארי, עם יכולות notifications, ואסינכרוניות (Pipeline/streams) – אך עדיין לא נתקלתי ב RESTful APIs שמשתלב ביכולות הללו בצורה הרמונית וטבעית.

סיכום

חזרנו והתבוננו ב REST בראייה מפוכחת, לא רק “REST זה טוב, SOA זה רע” – פזמון הפאנק של אמצע שנות ה-2000.

אני לא ממליץ לאף אחד לחזור ל *-WS או SOAP חלילה, הם כבר עתיקים ולא רלוונטיים. ייתכן ו RESTFul APIs זו הגדרה מספיק טובה לסט מצומצם של Public APIs של חברה (והרי ב Public API יש יתרון גדול בשימוש בתמות מוכרות), אך דיי ברור שלצורכי התקשורת הפנימיים של מערכת מורכבת / מיקרו-שירותים, RESTFul APIs היא גישה מוגבלת מדי ודלה בסמנטיקה – שחייבת הרחבה.

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

לא ענינו על התשובה לשאלה: “כמה RESTFulness כדאי לעשות? ואיך יודעים לאמוד מתי ההצמדות ל RESTfulness היא תורמת, ומתי היא כבר מזיקה?” – אל אני מניח שכל ארגון תוכנה בימנו ש”מתבסס על REST” צריך לשאול את עצמו את השאלה הזו – ולהמציא תשובות.

יש ניסיונות כנים ומשמעותיים לנסות ו”לתקן” את REST (בצד התקשורת; צד הארכיטקטורה הוא מאוד ספציפי ומתאים רק למיעוט שבמיעוט שבמערכות) למשל JSON API או להיצמד ל Guidelines של מישהו שעשה עבודה משמעותית, למשל ה Microsoft RESTFul API Guidelines. פעם מישהו הציע לי לאמץ בארגון את JSON API ושללתי את זה על הסף (“כל ארגון יגדיר את מה שמתאים לו”). היום הייתי מתייחס לאופציה הזו ברצינות גדולה יותר. לא כ “תורה מסיני שתפתור הכל”, אלא כדי לא להתחיל את תהליך ההגדרה הארגוני מאפס.

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

תנועות כגון Forget HTTP + הרצאה או HTTP Hell-no אולי מביעות הרבה תסכול מה”אכזבה” של REST בפועל – אך מנסות להציע גם חלופות ותיקון.

טכניקות כגון Polling, Long-Polling ו Webhooks הן דוגמאות פשוטות להרחבות טכניות פשוטות – המאפשרות עוד הרבה מרחב תמרון במקומות ש HTTP/REST מוגבל בהם (“client קורא ל server”, וזהו). יש לי הסברים מפורטים על הטכניקות הללו – אניח לרגע שכל הקוראים מבינים אותן. כן גם משמרות את האופי של HTTP/REST כמעט ולחלוטין – זו רק הרחבה.

הפתיחות לאמץ אלמנטים של RPC בצורת העבודה של הארגון (להתגמש ב Resource modeling, להוסיף קודים של הארגון לכל תשובה, ו/או מבנה סטנדרטי של בקשה/תשובה לכלל המערכת) עשויים לשרת אתכם היטב, גם אם “קהילת נאמני ה REST” – ידיחו אתכם מחברות בארגון / חיי נצח / ההבטחה ל 70 מהנדסות תוכנה (טובות!) בעולם הבא.

(אם מישהו ישאל, JSON-RPC הוא פשטני ונאיבי מדי לטעמי, וגם אותו תאלצו מהר מאוד להרחיב ולהגדיר בעצמכם. RCP בינארי כמו Thrift או gRPC הוא אטקרטיבי לשיפור ביצועים / אמינות התקשורת – אך מהלך כבד מדי אם כל מה שאתם מחפשים הוא סמנטיקה וכללי עבודה טובים לבעיות מידול ה Endpoints במערכת שאינה very high scale, הפשטות הרי חשובה).

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

אם אתם עובדים ברמה של HTTP בצורה ניכרת (או ב RESTFul APIs או ב RPC כלשהו, למשל home-made) – חשוב ללמוד את התקן הפשוט והחשוב הזה, ולהבין אותו. אני רוצה להאמין שזה מאמץ קטן ומשתלם. יש את הפוסטים שלי (HTTP Basics + מבנה ה URL) אלו ואלו, יש את הרפרנס הנחמד הזה של for-get-HTTP – HTTP References שמצאתי במהלך כתיבת הפוסט – ונראה לי שאשתמש בו עוד בעתיד.

מחשבות, הערות, תוספות – יתקבלו בברכה!

REST ו RESTful APIs – גרסת 2016

איי שם בשנת 2011 כתבתי פוסט על REST ו RESTful APIs. פוסט שנצפה דיי הרבה פעמים, וגם עורר שאלות ותגובות. לפני כשבוע שאל אותי עוד מישהו על אחת הדוגמאות שנתתי. דוגמה כיצד לא-לכתוב REST Endpoint.

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

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

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

רעיונות ה REST באמת גרמו לסחף אדיר בתעשיה, ושינו בתוך שנים ספורות את האופן בו אנו מגדירים APIs בין מערכות / תתי-מערכות.

מה כ”כ חזק ברעיונות ה REST שגרם לשינוי האדיר הזה?
מדוע פרוטוקולים “נבונים” יותר ו”מעודכנים” יותר, כמו GData, OData, או JSON API – לא זוכים לכזו תהודה?
מה סוד הקסם של REST?

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

כלומר: אפשר לומר שמרבית התעשייה עובדת עם “פסדו-REST”, ורק חלקים קטנים יותר עובדים עם “REST תקני” או אפילו “REST תקני חלקית”.
אם נשווה את מספר המשתמשים ב “REST תקני (חלקית)” – סדרי הגודל של המשתמשים דומים כנראה למספרי השמתמשים בפרוטוקולים חלופיים, קרי JSON API, OData, GData, או Thrift (שפותח במקור ע”י פייסבוק).

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

מה הרעיון ב REST?

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

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

  • ל HTTP יש המון ספריות, לכל שפת תכנות בערך, והרבה אנשים מכירים אותו לעומק. (אם אתם לא מכירים – זה זמן טוב לקורא את הפוסט בנושא!)
  • אם נפעל בהתאם ל HTTP, נוכל להינות בהשקעה קטנה מצידנו מ Caching שרכיבי הרשת השונים כבר מספקים (Proxies, gateways, CDNs, וכו’).
  • רכיבי אבטחת רשת מקובלים (IDS או WAF) מכירים HTTP. הנה לנו פרוטוקול שכבר יש לו תמיכה בלא-מעט כלי אבטחה.
  • HTTP הוא פשוט ויעיל. מתחת למכסה המנוע הוא פשוט כמה headers שנשלחים לפני ההודעה על גבי חיבור ה TCP.
  • HTTP הוא stateless (ברובו הגדול), וה Scalability שלו היא מוכחת! (ה world wide web, דאא)
  • HTTP קל לניטור והבנה (יש הרבה כלים + הוא מבוסס clear text).

ע”פ ההגדרות הפורמאליות, REST אינו בהכרח קשור ל HTTP. הוא מבסס עקרונות (או Constraints) של ה API של המערכת, שדומים מאוד לאלו של רשת האינטרנט:

  • תקשורת שרת-לקוח
  • ה API הוא Stateless
  • ניתן לבצע Cache לבקשות
  • ממשק אחיד, לכלל המערכת:
    • מבוסס מיפוי של משאבים (כלל מנחה: שמם מורכב משמות-עצם)
    • על כל המשאבים ניתן לבצע סט מוגדר של פעולות: קריאה, כתיבה, עדכון, וכו’. (דומה מאוד ל HTTP GET, HTTP POST, וכו’).
    • ה API מתאר את עצמו – ולקוח ה API משתמש בתיאור זה בצורה “גנרית” על מנת להשתמש ב API.

כיצד הסתירה הזו מתיישבת? נסביר בהמשך.

היסטוריה: כיצד נולד ה REST?

רעיונות ה REST הופיעו בפעם הראשונה בעבודת הדוקטורט של Roy T. Fielding, חבר בצוות שהגדיר את פרוטוקול ה HTTP וכן co-author של ה Apache Web Server.

עבודת הדוקטורט של פידלינג עסקה באפיון של רעיון שנקרא “סגנונות ארכיטקטוניים”, מן דפוסים מרכזיים וחוזרים בארכיטקטורות של תוכנות שונות. בפרק מספר 5 בעבודה הוא תיאר סגנון ארכיטקטוני בשם (Representational State Transfer (REST – אימוץ העקרונות מאחורי ה world wide web, על מנת לתקשר בין שרתים.

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

כמה שנים לאחר מכן, ב 2003 עד 2005 בערך – העולם נכנס לסערה של Service Oriented Architecture (בקיצור: SOA) והרעיון של Web Services (רשימת תקנים בשם *-WS).
אפשר להשוות את ההשפעה של SOA לאופנה הנוכחית של Microservices. לי, כמפתח צעיר, הרעיונות נשמעו מלהיבים וגדולים – ובחברה שעבדתי בה באותה התקופה החליטו “לא לפגר מאחור – ולאמץ SOA עד הסוף”. כמובן שלי ולחברי לא היה מושג בדיוק כיצד להוציא מ SOA את המיטב – ופשוט מימשנו SOA ו Web Services.

Web-Services היו הפשטה גבוהה ועשירה/מסובכת מעל הפרימיטיביים של הווב. היה שם XMLs גדולים ומורכבים, שנעטפו בתוך XML נוסף (ה envelope) שהיה חלק מפרוטוקול התקשורת – SOAP, ותוארו ע”י פורמט שנקרא WSDL והופץ על גבי פרוטוקול בשם UDDI ואז היה ניתן להשתמש בכלי בשם DISCO על מנת למצוא אותם. וזה היה רק הבסיס.

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

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

ב 2004 ה W3C (ארגון התקינה של האינטרנט) הוציא מסמך שדן בארכיטקטורות אינטרנט. לקראת סוף המסמך, הופיעה פסקה קצרה המרחיבה את ההגדרה של Web Services: היא גם ציינה את REST כבסיס התיאורטי לפעולת ה Web Services, וגם ציין שהם יכולים להיות בלתי-תלויים בפרוטוקולים כמו WSDL או SOAP.

יריית הפתיחה נורתה, ושני מחנות החלו להיווצר:

  • אלו התומכים ב Web Services ע”פ סט תקני ה *-WS.
  • אלו התמוכים ב RESTful Web Services, כלומר: עלינו רק לתאום לכללי ה REST – וזהו Web Service בר-תוקף, בהכשרת ה W3C!
על עצם הצורך בהגדרה של Web Services, או של SOA – אף אחד לא ערער. זה היה באזז חזק שלא ניתן להתכחש לו.
היום כמעט כבר אין ממש זכר ל *-WS, ומעט מאוד מ SOA. הקונצנזוס הגדול התעשייה הוא סביב REST – כאשר עדיין יש הרבה חוסר הבנה מסביב לעקרונות הללו.

קצת עזרה מידידנו ריצארדסון

את חוסר ההבנה ב REST, ניסה קצת להבהיר / לסדר בחור בשם לאונרד ריצרדסון.

ריצרדסון הוא המחבר של שני ספרים מוכרים (הוצאת O’Reilly):

  • RESTful Web Services – 2007
  • RESTful Web APIs – 2013

בהרצאה בכנס QCON-2008, ריצרדסון הציג מודל בגרות של אימוץ רעיונות ה REST:

רמה 0 – הביצה של POX (קיצור של Plain Old XML. פורמט ה XML היה בזמנו יותר פופולרי מ JSON)

רמה זו מתארת שימוש ב HTTP בכדי להעביר קבצי JSON או XML, נניח תמיד במתודת HTTP POST. זה יישום פשוט, Freestyle (שזה סוד הקסם שלו) – אך זה לא REST.

זה כמובן לא מפריע לקרוא לו REST – ויש אנשים שבאמת מתייחסים לכל פורמט סטנדרטי (נניח JSON) שעובר ל HTTP – כ REST. לא נבוא איתם בחשבון 🙂

רמה 1 – מיפוי משאבים

ברמה זו אנו מקפידים על תיאור של ה URIs כך שיתארו משאבים. עבור הווב של משתמשי הקצה משאבים הם מסמכים: html, js, או css. עבור API – משאבים הם דומים יותר לאובייקטים, או חלקים בתוך האובייקטים.

בעוד שיכולנו לתאר API של מערכת לניהול כנסים כ:

  • POST /create_session
  • POST /set-session-title
מיפוי של משאבים יתייחס לקריאות הללו בצורה בה מתייחסים למשאבים:
  • POST /sessions/create
  • POST /sessions//title
מכיוון שה title שייך ל session, אנו מרכיבים את ה URI בצורה היררכית – כך ש session קודם (ולכן “מכיל”) את ה title. כלומר: כל “/” מתאר בהכרח רמה היררכית של מבנה המשאבים.

מיפוי המשאבים מציב כל מיני דילמות, שאין להן תשובה חד משמעית:
  • כאשר אין קשר היררכיי ברור בין ה resources  – מה הסדר הנכון?
    • למשל: הזמנת-נסיעה ונהג – מי מכיל את מי? (בהנחה שכל אחד מהם הוא אובייקט שחי בפני עצמו. הזמנת-נסיעה נוצרת בלי קשר להשמה לנהג).
  • רזולוציה: האם למפות כל תכונה של אובייקט (למשל: title) לresource שיש לו URI משלו, או פשוט להסתפק ב sessions/update/ עבור כל התכונות יחדיו (בהתאם לפרמטרים שנשלחים בבקשה)?
  • כיצד לתאר גרסאות שונות של ה API? כחלק מה URI או כפרמטר?
  • ועוד…
רמה 2 – שימוש ב HTTP Verbs (ו Status Codes)
שימוש ב Method (נקראים גם Verbs) של פרוטוקול HTTP בכדי לתאר את הסמנטיקה של הפעולה:
  • GET – על מנת לבקש את ה state של המשאב אליו מפנה ה URI.
  • POST – על מנת לשלוח content (“נתונים”/”הודעה”) למשאב לצורך עיבוד, עיבוד שייתכן וישנה את ה state של המשאב. לעתים הודעה זו תיצור תת-משאב (ואז נהוג להחזיר Status Code של 201 – “נוצר”).
  • PUT – על מנת להחליף (rebind – “רישום מחדש”) של התוכן של המשאב.
  • PATCH – על מנת לבצע עדכון חלקי של ה state של המשאב.
  • DELETE – על מנת למחוק את המשאב.
ניתן כמובן להשתמש גם במתודות האחרות של פרוטוקול ה HTTP,קרי HEAD, OPTIONS, TRACE – אולם הגדרת ההתנהגות שלהן עבור REST API היא פחות ברורה ויותר פתוחה לפרשנות.

כלל חשוב נוסף הוא שאין חובה לתמוך בכל 4 הפעולות הבסיסיות על כל משאב. ייתכן משאב שעליו ניתן לבצע רק GET ומשאב אחר שעליו אפשר לבצע רק POST. מצד שני הגדרת ה URI מחייבת שכל צומת מתאר משאב שניתן לגשת אליו. לדוגמה, אם הגדרתי משאב כ:

אזי גם ה URIs הבאים צריכים להיות ברי-קיום:
יתרון גדול בשימוש ב HTTP Verbs היא “קבלת Caching” בעלות מזערית. רכיבי רשת רבים: שרתי ווב, CDNs, רכיבי פרוקסי ו Gateway – מודעים ומכבדים את ה Verbs הללו. אם אנו עושים הרבה GET (ובעיקר: מוסיפים גם headers נכונים) – התשובה תהיה cached ב”רשת”. כאשר נבצע POST לאותו משאב – ה caches יתנקו.
יתרון נוסף הוא שרכיבי אבטחת רשת IDS, WAF, ועוד – מודעים לבקשות הללו ול HTTP error codes – יודעים על פיהן להסיק ולזהות התנהגות חריגה / מסוכנת ברשת שלנו.
מה שכן דיי מעצבן הוא שלעתים רכיבים האבטחה מניחים שתשובות עם סטטוס 404 הם שגיאה ברורה – ונדירה מטבעה, אבל יכול להיות APIs שמחזירים 404 בצורה דיי תדירה (נניח: 10% מהפעמים) – ואז צריך או להרגיע (= לכוונן) את רכיב האבטחה או להימנע משימוש בקוד 404 ב API שלנו…

ההתנהגויות המוגדרות של HTTP verbs השונים.

עוד מידע על התנהגות ה HTTP הצפויה מה verbs השונים, ועל שימוש ב Status Codes – ניתן למצוא בפוסט על פרוטוקול ה HTTP.
כפי שתיארו את זה ריצרדסון ורובי בספר, הכוונה היא שפרוטוקול ה HTTP (או פרוטוקול אחר, רעיונות ה REST לא מתייחסים בהכרח ל HTTP) ישמש באותה המידה משתמשים אנושיים, ומכונות (צרכני API) באותה המידה ו(כמעט) באותו האופן:
“…Reunite the programmable web with the human Web… a World Wide Web that runs on one set of servers, uses one set of protocols, and obeys one set of design principles”
 — Richardson & Ruby, RESTful web services, 2007.
רמה 3 – Hypermedia Controls
שמות נרדפים: Hypermedia transformations או Hypermedia As the Engine Of Application State (בקיצור: HATEOAS)
רעיון זה טוען שעל פעולה ב REST למשאב צריכה לתאר את פעולות ההמשך האפשריות כ links לפעולות הללו.
כשאנו גולשים באינטרנט, אנו הולכים למשאב מרכזי, למשל: https://www.bankhapoalim.co.il – ומשם אנו משתמשים בקישורים על מנת לבצע את שאר הפעולות: כניסה לחשבון, ביצוע העברה בנקאית, שליחת טופס לנציג הבנקאי האישי שביצענו טעות בהעברה הבנקאית (!!) – וכו’…
באופן דומה, הרעיון ב REST הוא שהתוכנה בצד השני (ה Client) לא “תדע” אלו פעולות זמינות, אלא תלמד אותן מהתשובה. למשל:
התשובה שלנו לכל קריאה, מכילה גם links שהם תיאור של הפעולות האפשריות. self למשל היא הפעולה הנוכחית, כאשר אני גם יכול לבצע product.search – ויש בידי את ה URI המתאים לביצוע התחזוקה.
עוד השלכה חשובה של ה HATEOAS היא שכל פנייה לשרת צריכה לפתוח בקריאת GET (או OPTIONS – לדקדקנים), ורק אחריה קריאת POST או PUT וכו’. יש מערכות שמסתמכות על ההנחה הזו.
משתמש אנושי יכול באמת להבין מה המשמעות של “product.search”, אבל מה מכונה מבינה מזה? ישנן שתי אופציות:
  • המכונה מכירה את כל ערכי ה rel האפשריים – ולכן יודעת לפעול על פיהם.
  • המכונה לא מבינה.
הפער הזה נקרא ה Semantic Gap, עניין שאנשי ה Semantic Web (מספרם קצת הצטמצם בשנים האחרונות, לא?) – מנסים לפתור.
ישנם פרוטוקולים כמו XMDP או ALPS לתיאור סמנטיקה של פעולות API. מן שפות של meta descriptions.

מי שממשיך ומתקדם מעל רמה 3 – ראוי להיכנס להיכל התהילה של REST, כמי שיישם את מודל ה REST בצורתו הטהורה ביותר!

ספר על REST? – יש עשרות רבות…

מודל ה REST בפועל

האם זה הגיוני ליישם את שלב 3 במודל של ריצ’רדסון, Hypermedia Controls?
אם אתם מפתחים מערכת ווב טיפוסית – ברור שלא!
אם הגעתם ליישום של שלב 3 במערכת שכזו, כנראה שנכשלתם: נכשלתם ליישם מערכת פשוטה, שניתן לפתח במהירות.

פידלינג טען בתוקף, שכל יישום של פחות מרמה 3 – איננו ראוי להיקרא REST, הנה הטקסט. הוא כנראה צודק. אנו משתמשים בתעשייה ב REST בעיקר באופן שונה ממה שהוא תכנן אותו. בצורה הרבה יותר מינימליסטית ופשוטה.
דווקא ריצרדסון שהגדיר את מודל ה maturity ציין בראיונות מאוחרים יותר שהוא ניסה לספק שלבים כיצד לאמץ REST בהדרגה, ומבחינתו עצירה ברמות 1 או 2 – הן גם טובות.לדעתי האישית, אין טעם ברמה 1 ללא רמה 2: מיפוי משאבים ללא שימוש ב Verbs הוא אנמי וחסר. הייתי ממליץ על רמה 2 או 0, עם הסתייגויות.

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

הנה כמה כללים שאנחנו נוהגים להתעלם מהם במערכת הקיימת שלנו (בידיעה):

  • הפער הגדול ביותר: אנו משתמשים בפעולות POST גם לצורך קריאות שלא משנות את ה state של האובייקט. פשוט כמו GET שמכיל payload גדול יותר, אפילו אם לא מתבצע processing מורכב מאחורי הקלעים.
    • זה אומר ששברנו כמה כללים של ה HTTP: חלק מקריאות ה POST הן עכשיו cacheable ו idempotent.
    • מצד שני: שרשור של פרמטרים ה URL זה פחות קל, ופחות קריא. הכי מעצבן היה לגלות שוב ושוב חסימות לאורך ה URL ברכיבי הרשת השונים: פעם ב WAF, פעם nginx – ופעם ב Load Balancer. אמנם אין הגבלה בתקן ה HTTP עצמו, אבל כנראה שבעקבות הדפדפנים שנהגו להגביל את אורך ה URL – נראה שגם רכיבי הרשת אמצו, כברירת מחדל, את המסורת…
  • לא כל חלק של URI הוא בר-תוקף. ייתכן וישנו URI בשם {companies/{id}/roles/{role_id/ מבלי שיש ל companies/{id}/roles/ כל תוקף. למה לנו לפתח API שאף אחד לא הולך לקרוא לו?
  • אנחנו משלבים שמות של פעולות, ולא רק משאבים ב URI. אנחנו משלבים אותן בסוף, כמעט-תמיד. למשל:
    areas/by_point ו areas/by_rectangle.
  • אם פעולה יצרה משאב חדש, אנו לא מחזירים header של location עם ה URI למשאב שנוצר. הרבה יותר פשוט להחזיר id של האובייקט החדש בתוך ה JSON של התשובה.
  • ויש כנראה עוד…
קצת פרספקטיבה: כללי ה REST הם לא עבודה שצמחה מתוך עשייה של API, אלו לא כללים שהתפתחו וגדלו עם הזמן.
כללי ה REST הופיעו בדוקטורט (של בחור מאוד חכם, שמבין HTTP יותר טוב מכולנו) בשנת 2000 – ומאז לא השתנו. רק הפרשנויות אליהם השתנו מעט עם הזמן.כמו שאני נוהג להטיף, גם כאן אני מציע: השתמשו בכל כלי בצורה שהכי מועילה לכם, ולא ע”פ כלל שרשום איפשהו.

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

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

  • למדל בצורה סמנטית (קרי – ברורה מפתח) ובחשיבה על resources במבנה היררכי את URIs שאנו משתמשים בהם.
  • להקפיד על שימוש ב Verbs של HTTP ועל Status Codes בסמנטיקה שהוגדרה להם. אנו לא משתמשים ב Verbs ו Status Codes שאינם חלק מתקן ה HTTP (על אף שתקן ה HTTP עצמו – מותיר זאת).
  • להיות “Good Citizens”, ולהתאים את התנהגות ה APIs שלנו לכללי ה HTTP במידת האפשר.
הנה במצגת הזו, תוכלו למצוא בין היתר דוגמאות לשימוש “לא תואם ל REST” ב APIs של חברות מוכרות.
הייתי נמנע מרוב ה”שגיאות” שמוצגות, אך כנראה גם הייתי סולח ל.. עד מאמץ כמה מהן.
המצגת היא מ 2012, וחלק מה APIs שמוזכרים השתנו מאז, או אפילו לא קיימים יותר.

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

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

הבלוג של מרטין פאוולר, על Richardson Maturity Model ו REST:
http://martinfowler.com/articles/richardsonMaturityModel.html

מצגת נחמדה שמתארת כמה מהבעיות שעלולות לצוץ ביישום REST (הרשאות, גרסאות שונות של API, טיפול בשגיאות, וכו’):
http://www.slideshare.net/MasoudKalali/masoudkalalirest-new-format

ריילס: routing

נדבך חשוב בריילס הוא ה routing, המיפוי איזה Action (=מתודה) של איזה Controller תופעל ל URL נתון.
הרכיב שעושה את ה routing נקרא ActionDispatcher.

ה routing מתבצע באפליקציה בקובץ בשם config/routes.rb בעזרת סט פקודות מיוחדות (בעצם: DSL) שמגדיר את ה routes. סט הפקודות הזמין הוא עשיר ומגוון למדי, ולרוע המזל – אינו מתועד בצורה נוחה ללמידה. לא כ\”כ באנגלית – ובוודאי שלא בעברית.

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

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

כפי שנראה ה routing של ריילס מבוסס עמוקות על עקרונות ה REST – ניתן לקרוא בקצרה על עקרונות ה REST בפוסט הזה על REST או בפוסט על HTTP.

    לפני ריילס 3, הגדרת ה routes הייתה מסורבלת למדי. אני מתעלם מתחביר זה לחלוטין, ומתמקד במה שזמין בריילס 3 ו 4 (או ליתר דיוק: מתייחס למצב בריילס 4.2).

    האנטומיה של route

    הנה דוגמה ל route טיפוסי:

    1. HTTP verb / method
    2. pattern של URL יחסי, המכיל בתוכו:
    3. segment key (אחד או יותר) – המוגדר כ \”symbol\” ב path.
      segment key ממפה ארגומנטים שמועברים ב URL (או כ Query String[א]).
    4. יעד המיפוי, בפורמט: \”controller#action\”. שם ה controller מופיע ללא המילה Controller ובאותיות קטנות (בכדי לקצר בכתיבה). Action הוא שם המתודה ב controller שמטפלת באירוע.
    5. רשימת אופציות ל routing. במקרה שלנו יש אופציה אחת בשם \”as:\” עם ערך של \”purchase\” (הסבר על אופציה זו – בהמשך).

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

    בריילס 3, היה מקובל להגדיר routes פשוטים בעזרת פקודת match, למשל:

    match \’products/:id\’ => \’products#show\’, via: :get

    via הוא פרמטר שמגביל את ההתאמה ל HTTP verb/method מסוים (אפשר גם לשלוח רשימה – במערך), והוא היה אופציונלי עד גרסה 4 של ריילס. בגרסה 4 זו הפכה לחובה (RuntimeError ייזרק אם לא הוגדרה http method, אפשר להשתמש גם ב any:, אם כי לא מומלץ).

    הדרישה להגבלת ה http method נולדה משיקולי אבטחה ואמינות של המוצר. מומלץ תמיד להגדיר HTTP verb יחיד ל route, ולצורך כך נוספו הקיצורים get, post, put וכו\’ – שהם כתיבה מקוצרת ל <match… via:<method.
    מעתה והלאה נעבוד רק איתם, אך כדאי לזכור שבמקור הם קיצור תחבירי ל match ושאת הפרטים על האופציות השונות הזמינות ל route – יש עדיין לחפש בתיעוד של match.

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

    match \’products/:id\’, to: \’products#show\’, via: :get
    match \’products/:id\’, controller: \’products\’, action: \’show\’, via: :get

    אני מזכיר אותן, כי ייתכן שתתקלו בפרמטרים של to: ו controller: ב routes – ושתבינו את משמעותם / מקורם.

    בקיצור, היום נכתוב את ה route הזה באופן הבא:

    get \’products/:id\’ => \’products#show\’

    משמעות ההגדרה הזו היא:
    אם יש קריאת GET עם URL המתאים לדפוס \”<products/<x", קרא ל controller בשם ProductsController (שנמצא בתיקיה app/controllers/) ולמתודה בשם show, ושלח כארגומנט את המחרוזת x כערך של הפרמטר \”id:\”.
    את הערך ניתן לקרוא בתוך ה controller בעזרת המתודה params, למשל:

    params[:id] # string \”x\”

    כיצד זה עובד?

    הנה דוגמת קוד מינימלית לשימוש ב routing, controller, ו view:

    • בקובץ ה routes.rb, הגדרנו route בסיסי – שאתם אמורים כבר להבין
    • ה Controller (הקטן ביותר האפשרי, בערך) מכיל מתודה (= action) בשם show שאליה ה route שהגדרנו יפנה. היא מחפשת במודל את המוצר ושומרת אותו כ product@.
    • כשה Controller יוצר את ה View, ריילס באופן \”פלאי\” (הסבר) מעתיק את ה instance variables, על ערכיהם, מה Controller (כדוגמת product@) ל View. משם, ניתן לגשת לשדות השונים בתוך ה product.
    • שימו לב לפקודה בשם link_to אותה תראו הרבה ב views של ריילס. היא מייצרת עבורנו link עם הכותרת שהגדרנו (\”Show Details\”) לפעולה מסוימת של ה Controller. כיצד זה עובד? – נסביר בהמשך.

    אופציות מתקדמות יותר להגדרת routes

    Segment Keys אופציונליים
    ממש כמו optional parameters בפונקציה, יש גם Segment Keys אופציונליים ב route. למשל:

    get \’products/:id(/:facet)\’ => \’products#show\’

    יתאים ל 2 צורות של url, למשל:

      במקרה הראשון יהיה ערך רק לפרמטר id: (הערך = \’441\’), ו facet: יהיה nil.

      במקרה השני facet: יהיה שווה \’specifications\’

      שימו לב לצורה הנפוצה הבאה:

      get \’products/:id(.:format)\’ => \’products#show\’

      המשמעות שלה היא matching ל url מהנוסח הבא:

      כאשר הפרמטר format: מקבל את הערך \’json\’.
      הנקודה שליד שם הפרמטר מתארת את חלק מה Path.

      ספציפית לגבי format:, זהו פרמטר עם התנהגות מיוחדת: פקודת respond_to שבשימוש בתוך ה controllers בודקת את הערך שלו, ולפיו מחליטה כיצד לפעול (האם להחזיר HTML או json – בד\”כ).

      Redirect
      ניתן להגדיר route שיבצע redirect ל URL אחר ברשת. למשל:

      get \’products/:id\’, to: redirect(\’v2/products/:id\’)

      המילה to: מגיעה מתוך התחביר הישן של match שהזכרנו למעלה.
      הערך של to: הוא Rack Endpoint שיכול להיות קוד inline (בשימוש בפונקציות lambda או proc) או שם של אפליקציית Rack אחרת (שיושבת בתיקייה app/metal/), או סתם URL אחר (כמו במקרה שלנו).

      Constraints
      אופציה זו מאפשרת לנו להציג תנאים נוספים (בדמות RegEx) על ה matching של ה route. למשל:

      get \’products/:id\’ => \’products#show\’, constraints: {:id => /\\d+/}

      תתאים רק כאשר ה id: הוא מספר (ולא כולל אותיות, למשל).
      בגלל ש constraint הוא כ\”כ נפוץ על שדה שנקרא id:, נוצר לכך קיצור מיוחד בריילס. את הדוגמה הנ\”ל ניתן לכתוב גם כך:

      get \’products/:id\’ => \’products#show\’, id: /\\d+/

      ניתן להשתמש ב constraints על מנת לעשות בדיקת קלט למשתמש – אך זה לא מומלץ!
      הכלי הוא יחסית מוגבל, וקובץ ה routing הוא לא בהכרח המקום הנכון לעשות זאת.
      הכלל המנחה הוא להוסיף constraint על route, אם יש לכם route אחר שיתאים במקום. למשל:

      get \’products/:id\’ => \’v2/products#show\’, id: /\\d+/
      get \’products/:id\’ => \’v1/products#show\’

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

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

      Wildcard Segment
      אם מבנה ה URL מכיל מידע, ויכול להופיע בווריאציות שונות, ניתן להשתמש ב wildcard segment. כלומר, ה route:

      get \'products/*other\' => \'products#show\'

      יתאים ל URL כמו:

      וישלח ל controller משתנה בשם other: שערכו הוא \’my/id/366/type/special\’.

      ניתן להרכיב Wildcard Segments, עם Segment Keys רגילים. אם אתם זקוקים לעוד מידע בנושא חפשו את המונחים Wildcard Segment או Route Globbing.



      רשימת יכולות שכיסינו כאן כנראה מכסה את רוב השימושים הנפוצים.
      ניתן לקרוא בתיעוד הרשמי, Rails Routing from the Outside In, על אפשרויות נוספות.



      Named Routes

      מנגנון ה Named Routes בא לפשט (אפילו יותר) את העבודה עם routes.

      כאשר נותנים ל route את השם \”abc\”, ייווצרו שתי מתודות הזמינות לשימוש ב Controllers וה Views: אחת בשם abc_url, והשנייה abc_path.

      • המתודה abc_url היא תייצר url מלא שיתמפה ל route שהגדרנו.
      • המתודה abc_path תייצר את את חלק ה path של ה url, בלי protocol/host/port.

      מנגנון ה Routing של ריילס בעצם משמש לשני כיוונים: גם לפענוח URL והתאמתו ל route (ומשם ל controller#action), וגם לצורך generation של URL (או path) שיוביל ל route שהוגדר.

      הנה דוגמה:

      get \’products/:id\’ => \’products#show\’, as: :show_product

      תיצור את 2 המתודות show_product_url ו show_product_path על אובייקט ה app שזמין ל controllers וה views.

      עכשיו אפשר ליצור קיצור ל path הזה מתוך אחד ה views בעזרת פקודת link_to. הפקודה, בגרסתה הארוכה, מקבלת hash כפרמטר:

      link_to \”הצג מוצר\”,
        controller: \”products\”,
        action: \”show\”,
        id: 12
      אך בעקבות השימוש ב as:, יש לנו דרך מקוצרת להפעיל אותה:

      link_to \”הצג מוצר\”, show_product_url(id: 12)

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

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

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

      link_to \”הצג מוצר\”, show_product_url(12)

      בד\”כ זה לא יהיה ערך עצמו אלא משתנה. למשל product.id, כאשר product הוא המודל שהכנתם ב controller. במקרה כזה ניתן לקצר אפילו יותר ולשלוח פשוט אובייקט:
      link_to \”הצג מוצר\”, show_product_url(product)
      וריילס יידע לחפש אם יש עליו שדה בשם id: ולהשתמש בו.
      בעצם, אתם יכולים להשתמש בגישה זו לא רק לשדה בשם id, אלא לכל שם של שדה או רצף של שדות.
      עבור debugging, ניתן להריץ את הפונקציות xxx_url/xxx_path אובייקט ה app (מבלי להריץ את ה view). למשל:
      app.show_product_path(12) # \’/products/12\’

      שאלה של סגנון
      מה ההבדל בין הפונקציה show_product_url לפונקציה show_product_path? מתי יש להשתמש בכל אחת מהן?

      זה בעיקר עניין של סגנון:

      • show_product_url מייצרת Fully Qualified URL (כלומר: URL מלא ועצמאי), כפי שנדרש בתקן ה HTTP בפעולות redirect.
      • show_product_path, מייצרת URL רלטיבי שהדפדפן ידע להפוך אותו למלא בעת הצורך. ה URL קצר יותר ולכן נוח יותר לעבוד עם קבצי ה html שנוצרו. 
      אז מה אתם מעדיפים? להיות דקדקנים (url, לעבוד ע\”פ התקן) או לא לכתוב קצר (path, הדרך של ריילס)? עניין שלכם.
      אני בד\”כ מעדיף להיות דקדקן, אבל גם לפני ריילס עבדתי עם URLs יחסיים – וכך נראה לי שאמשיך.

      בכל מקרה, כדאי להכיר של helper functions יש עלות של ביצועים. בזמן ריצה הן מחפשות בטבלת ה routing מה שלוקח זמן. קריאה ל link_to עם controller ו action – היא אפילו יותר יקרה.

      Scoping

      scope הוא מנגנון שעוזר לארגן את ה routes בקובץ ה routes.rb, ולחסוך כמה שורות קוד על הדרך. יש לו הרבה מאוד וריאציות של קיצור – אציג כמה מהעיקריות שבהן.

        # original
      get \'drivers/new\' => \'drivers#new\', as: :driver_new
      get \'drivers/edit/:id\' => \'drivers#edit\', as: :driver_edit
      post \'drivers/reassign/:id\' => \'drivers#reassign\', as: :driver_reassign

      # scope - DRY a bit with controller (1)
      # alternatives: \"controller :drivers do ...\", \"scope :driver do\"
      scope controller: :drivers do
      get \'drivers/new\' => :new, as: :driver_new
      get \'drivers/edit/:id\' => :edit, as: :driver_edit
      post \'drivers/reassign/:id\' => :reassign, as: :driver_reassign
      end

      # scope - DRY a bit more with path (2)
      scope path: \'/drivers\', controller: :drivers do
      get \'new\' => :new, as: :driver_new
      get \'edit/:id\' => :edit, as: :driver_edit
      post \'reassign/:id\' => :reassign, as: :driver_reassign
      end

      # scope - DRY a bit further with default params (3)
      scope \'/drivers\', controller: :drivers, as: \'driver\' do
      get \'new\' => :new, as: \'new\'
      get \'edit/:id\' => :edit, as: \'edit\'
      post \'reassign/:id\' => :reassign, as: \'reassign\'
      end

      במקור היו לנו 3 routes שהיינו צריכים להקליד לכל אחד כמה וכמה תווים…

      1. הגדרנו, בעזרת הפקודה scope, לכולם controller אחיד, וכך קיצרנו את הגדרות ה controller#action לשם ה action בלבד. יש עוד 2 דרכים להגיע לתחביר המקוצר הזה (בהערה, השני יכול לבלבל).
      2. הגדרנו path, שמקצר את חלק ה relative URL ב route.
      3. הגדרנו as, שמהווה prefix ל as: של ה routes הספציפיים, והצבנו את ה path כארגומנט הראשון לפונקציה scope, מה שמאפשר לנו לוותר את המפתח path:.
      4. אפשר לוותר על כל המופעים של אותיות a ו e – וריילס ישלים אותם בעצמו על סמך מילון מוכן מראש. סתאאםם.
      אם אתם רואים את התוצאה הסופית וחושבים שניתן היה לקצר אותה יותר – אתם צודקים.
      עוד פרמטר פופולרי של scope הוא module: :abc שמניח ש:
      1. ה Controller שייך ל Module (שפת רובי) בשם Abc. כלומר Abc::SomeController.
      2. => ה Controller נמצא בתת תיקיה בשם abc/
      ישנה פונקציית קיצור בשם namespace שפועלת כך:
        namespace :drivers do
      # routes
      end

      # syntactic sugar of / equivalent to writing:
      scope path: \'/drivers\', module: :drivers, as: \'driver\' do
      # routes
      end

      הקיצור האולטימטיבי ליצירת routes

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

      כחלק מההתאמה של ריילס למודל ה REST, הוגדר ל Controller סט ה Actions הגנרי הבא:

      • index – הצגת רשימה של אובייקטים
      • create – יצירת אובייקט חדש (מתוך פעולת POST)
      • new – החזרת template ליצירת אובייקט חדש (ללא יצירת האובייקט בפועל). בד\”כ מדובר בהחזרת טופס וובי, \”יצירת אובייקט חדש\”, למשתמש.
      • show – הצגת הפרטים של אובייקט מסוים
      • update – עדכון אובייקט מסוים
      • edit – החזרת template לעדכון אובייקט חדש. בד\”כ מדובר בהחזרת טופס וובי \”עדכון\” אובייקט שבעקבותיו תגיע פעולת update.
      • destroy – מחיקת אובייקט מסוים

      הפקודה הבאה, תמפה סדרה של routes עבור שבע הפעולות:

      resources :products

      # syntactic sugar of / equivalent to writing:
      get \'products(.:format)\' => \'products#index\', as: :products # e.g. products_url()
      post \'products(.:format)\' => \'products#create\',
      get \'products/new(.:format)\' => \'products#new\', as: :new_product # e.g. new_product_url(12)
      get \'products/:id/edit(.:format)\' => \'products#edit\', as: :edit_product # e.g. edit_product_url(12)
      get \'products/:id(.:format)\' => \'products#show\', as: :product # e.g. product_url(12)
      patch \'products/:id(.:format)\' => \'products#update\',
      delete \'products/:id(.:format)\' => \'products#delete\'

      למיטב הבנתי, ה named routes נוצר רק לפעולות ה get, מכיוון שרק לפעולות אלו ניתן לעשות re-direct בפדפדפן.

      שימו לב שאם יש לנו route כמו הבא, שמופיע אחרי שורת ה resources:

      get \’products/poll\’ => \’products#poll\’

      לעולם לא יגיעו אליו. ריילס תתאים אותו ל products/:id שנוצר בעקבות פקודת ה resources. הדרך היחידה שיוכלו להגיע אליו הוא אם נציב את ה route הזה לפני פקודת ה resources. ההתאמה ל routes נעשית בסדר שבו הם מופיעים בקובץ.

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

      resources :products, only: [:index, :show, :edit]

      או השתמשו בפקודה ההופכית: except:.

      קינון routes

      קינון של routes מתבצע לרוב בין שני אובייקטים (REST resources) שיש בניהם קשר של שייכות. דוגמה שחוקה: כתבה, והערות לכתבה. מהות הקשר שמומלץ לנהל עבורו routes מקוננים הוא קשר \”חזק\” של הרכבה (composition), כאשר לאובייקט התלוי (למשל הערה) אין משמעות ללא אובייקט האב. למשל: כאשר מוחקים כתבה, טבעי שמוחקים את ההערות שלה. לא טבעי יהיה, בד\”כ, למחוק את אובייקט המחבר – וזו אינדיקציה טובה שלא כדאי לקנן את המחבר תחת כתבה, אפילו שיש ביניהם קשר.

      הנה דוגמה לקינון:

      resources :articles do
      resources :comments
      end

      # generates 7 routes for articles + routes such as:
      get \'articles/:article_id/comments\' => \'comments#show\'
      get \'articles/:article_id/comments/new\' => \'comments#new\'
      put \'articles/:article_id/comments/:id\' => \'comments#update\'

      … את הרשימה המלאה של ה routes הנוספים שנוצרים ניתן להסיק או לבדוק את הרשימה בתיעוד הרשמי.
      כמובן שה routes של article יפנו ל ArticlesController וה routes המקוננים יפנו ל CommentsController. הקינון, חוץ מזה שהוא נוח מבחינת מידול ה REST, מאפשר ל CommentsController לקבל גם את ה article_id:.

      עוד כלל הוא לעולם לא לעשות nesting עמוק (כלומר: 2

      הנה עוד 2 כלים נפוצים, member ו collection, המאפשרים להוסיף פעולות חדשות:

      resources :articles do
      member do
      get :preview
      end
      collection do
      get :search
      end
      end

      member מוסיף תחת ה resource עוד פעולה, אולי כזו שלא מוגדרת ב default, למשל preview. הפעולה היא כזו שמוגדרת על אובייקט יחיד – ולכן יש צורך במזהה. למשל, במקרה הנ\”ל ייווצר ה route הבא:

      /articles/:id/:preview

      collection הוא דומה מאוד, אבל מגדיר פעולה על סט ה resources, כזו שלא צריכה מזהה. בדוגמה הנ\”ל:

      /articles/search

      בדיקת ה routes בפועל

      כדי לבדוק שה routes נכונים, יש בריילס 2 כלים שימושיים:

      • מ command line, בעזרת הפקודה rake routes. ניתן לבדוק Controller ממוקד ע\”י שימוש בפרמטר, למשל: rake routes CONTROLLER=articles.
      • כאשר השרת רץ (rails s), ניתן לראות את רשימת ה routes תחת ה path הבא: rails/info/routes/

      סיכום

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

      כפי שאתם רואים אני עוד שקוע ריילס – ויש עוד כברת דרך….

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

      —–

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

      פיצול ה routes לקבצים שונים

      —–

      [א] ה route:

      get \’some_path/:a/:b/:c\’ => …
      יקבל ערכים זהים עבור 2 ה urls הבאים:

      /some_path/xx/yy/zz
      /some_path/xx?a=yy&b=zz

      הו-נתונים! – (AtomPub) חלק 2

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

      AtomPub

      מבוא לפאב האטום (AtomPub)
      בתחילה היה פורמט RSS – פורמט מבוסס XML לתיאור בלוגים. RSS זכה (וזוכה) לפופולאריות רבה בקרב אתרי חדשות ובלוגים.
      אולם, ל RSS יש כמה בעיות: התקן לא מפורט עד הסוף ומשאיר כמה שאלות פתוחות – דבר שיכול ליצור בעיות תאימות. למשל: התקן מציין שהתוכן של רשומה יכול להיות HTML או טקסט – אך לא מגדיר דרך לומר במה השתמשו בפועל. על הצרכן לעבור על הטקסט, לחפש תגי HTML ומהם להסיק מהו הפורמט. חובבני משהו.
      יתרה מכך, מפתח הפורמט מסר את הזכויות עליו (גרסה 2.0) לאוניבסיטת הארוורד וזו לא הואילה לפתוח את התקן או לשפר אותו בצורה מורגשת. מאז התקן “נעול”.

      סיבות אלו הספיקו ליצור צורך הולך וגובר בתקן חלופי ובעקבותיו הוגדר פורמט ה Atom: פורמט מוגדר היטב, בעל תקינה סטנדרטית של IETF (עוד גוף תקינה שעוסק באינטרנט). התקן זכה לאימוץ מתוקשר ע”י גולגל וכחלק מ AtomPub הוא אומץ ע”י מערכות תוכנה רבות.

      המודל הבסיסי של אטום הוא פשוט למדי, פיד המכיל מספר רשומות:

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

      הנה דוגמא אמיתית של קובץ Atom מינימלי (הכולל רשומה אחת):

      
      <feed xmlns="http://www.w3.org/2005/Atom">
      
        Example Feed 
        
        2003-12-13T18:30:02Z
         
          John Doe
         
        urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6
      
        <entry>
          Atom-Powered Robots Run Amok
          
          urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a
          2003-12-13T18:30:02Z
      Some text.
        </entry>
      
      </feed>
      אטום מחייב במינימום את השדות שיש בדוגמא זו (Author וכו). זוהי החלטה דיי הגיונית לפורמט של פיד חדשות/בלוג.
      הערה קטנה: בדוגמא למעלה הרשומה כוללת שדה Summary. אטום מחייב או Summary או Content (התוכן המלא).

      כאשר קובץ הפיד (סיומת .xml. או atom.) יושב על שרת ווב – כל שיש לעשות על מנת לקרוא את תוכנו (עבור Feed Reader כדוגמת Outlook או Google Reader) הוא לבצע קריאת Http get פשוטה לכתובת הפיד, לקבל את הקובץ שלמעלה ולהציג אותו בצורה יפה.

      Atompub

      AtomPub מוסיף מימד של עריכה לפיד, ע”פ עקרונות ה REST ומאפשר לבצע פעולות CRUD שונות:

      אם ארצה להוסיף רשומה לפיד, פשוט אשלח Http Post לכתובת הפיד, המכילה XML עם הרשומה להוספה וכל השדות המתאימים.
      תזכורת: פעולת POST ב REST משמעה: “שלח מידע למשאב קיים”. הפיד קיים ואנחנו שולחים לו עוד Entry והוא מוסיף אותו לפיד.

      להלן הפעולות המוגדרות בתקן (ניתן להרחיב):
      על פיד (ליתר דיוק Collection – עוד מעט אסביר) ניתן לבצע:
      GET כדי לקרוא אותו
      PUT על מנת להוסיף רשומה.
      על רשומה (ליתר דיוק Member – פריט) ניתן לבצע:
      GET (קריאת רשומה בודדת)
      PUT (עדכון רשומה)
      DELETE (מחיקת רשומה).
      על Service Document ניתן לבצע:
      GET.

      מדוע אנחנו מדברים על Collection ו Member ולא על פיד ורשומה? מהו ה Service Document??

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

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

      Service, או בעצם ה Service Document, הוא מסמך המתאר מספר אוספים (Collection) הקשורים לעולם זהה (Service). המסמך מחלק אותם לקבוצות הנקראות Workspaces – אין להם משמעות רבה.

      הנה המודל של AtomPub בצורה ויזואלית:

      עקרון של REST שאני רוצה להזכיר הוא Hypermedia Transformations. מה שעומד מאחורי השם המפחיד הזה הוא הרעיון לפיו בכל רשומה, (כלומר: בכל פריט. אדבר AtomPub) יש קישורים לפעולות האחרות האפשריות כגון האפשרות לעריכת פריט. הנה דוגמא:

        Some Report
       
        <link href=”http://host/products/some_report.atom“ rel=”edit”/>
        …

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

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

      מעבר לצרכים הבסיסיים (פעולות CRUD)
      AtomPub מכיל יכולות נוספות, אגע בעיקריות:
      Paging

      ברור שאוסף פריטים יכול להיות דיי ארוך ואנו לא תמיד נרצה לקבל (או לשלוח – בצד השרת) את כל הרשימה – לפני שברור שזקוקים לכולה. אולי היא ארוכה מאוד וכוללת אלפי פריטים? ב AtomPub יש מנגנון Paging (השם הרשמי הוא Partial Lists) שנשלט ע”י צד השרת – הוא מחליט כמה פריטים להעביר בכל Chunk. מסמך האטום שיחזור יכיל קישור עם rel של next האומר “אם אתה רוצה עוד – גש ללינק הבא”:

      למפתחי UI של מערכות סגורות הבחירה לתת לצד השרת להכתיב את גודל ה paging יכולה להראות קצת תמוהה – הרי ה UI יודע הכי טוב מה הוא רוצה. אולם, חשבו REST ועל החשיבות של דפים אחידים על מנת להשתמש ב caches (של HTTP) – יש פה פוטנציאל ממשי.
      השרת יכול גם להחליט אם הוא מותיר גם ללכת אחורה (קישור Pervious) או לקפוץ להתחלת או סוף האוסף. אם דיברנו על ההבדלים בין אוסף (collection) לפיד (של פורמט Atom) אז אוסף הוא הסט השלם והפיד הוא view או page שקיבלנו בכל פעם.

      Categories
      ב Atom (אופס, דילגנו על זה כבר) וגם ב AtomPub יש עניין של קטגוריות. אלו בעצם תגיות (tags) על הרשומות או הפריטים באוסף. יש גם “מסמך קטגוריות” שמגדיר את כל הקטגוריות (או תגיות) הקיימות במערכת. דיי בנאלי ולא כ”כ שווה להתעכב על זה לדעתי.

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

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

      עוד נקודה מעניינת היא שיכולת זו מעודדת לייצר פידים שונים המתארים אותם פריטים – עם שוני קטן שהוא הפריטים שבעריכה. מימושים רבים המבוססים על AtomPub נוטים להגדיר מספר מצומצם של שאילותות קבועות (פידים) עם URI קבועים אותם ניתן לתשאל. זו גישה שמתאימה ל REST ולניצולת טובה של ה caches אך היא קצת מגבילה את הלקוח ולעיתים דורשת ממנו “לבחור את פיד-הבסיס” ועליו לעשות עוד מעבר של “פילטור ידני”. זו לא גישה רעה – יש פה  tradeoff. בהמשך נראה ש ODATA דוגל בגישה אחרת.

      מדיה (Media Library Entries (MLE
      כפי שציינו Atom הוא פורמט מבוסס XML. מה קורה אם אני מעוניין לנהל מידע שלא יכול להיות מתואר בצורת XML? כגון תמונות, מסמכים (נאמר PDF) וכו’? כמובן שאפשר לעטוף את המידע הבינרי ב XML בעזרת בלוקים של CDATA – אך אז פעולת Http Get של פיד תגרום לקריאת כל התוכן הבינרי של כל הרשומות – כמות גדולה של נתונים.
      הפתרון של AtomPub הוא לייצר אוספים של רשומות מדיה (מידע בינרי) רשומות אלו נקראות Media Members.

      הפיד של רשומת המדיה יכלול רשומות XML המתארות את הפריט וכוללות URI שלו. מעין metadata. אם אתם זוכרים, פורמט atom מחייב מספר שדות חובה כגון “Author”. האם מעניין אותי לנהל את היוצר של התמונה? אם לא – האם נספק מסמך Atom לא תקני?

      ההחלטה של AtomPub היא להחזיר את שדות אלו למיטב יכולתו של השרת, כנראה על בסיס המידע שיש לו על הפיד, וסביר שקצת יפוברקו. רשומות ה Media Library Entries הן בעצם מצביע לקובץ המדיה. הרשומות יכללו לינק אחת לעריכת ה MLE – כלומר ה metadata (בצורת “rel=”edit) ו URI נפרד לעריכת קובץ ה מדיה עצמו (בצורת “rel=”media-edit)

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

      לילה טוב 🙂

      הו-נתונים! (Odata)

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

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

      בשרשרת פוסטים זו אדבר על ODATA (קיצור של Open Data) שנראה כיום התקן המבטיח להחלפת נתונים בין מערכות ברשת, המבוסס על עקרונות ה REST. התקן פשוט לשימוש פשוט, אך יש בו גם כמה מורכבויות. הוא מתאים בעיקר למצבים בהם הנתונים אותם אתם חושפים הם מורכבים ו/או יש לכם מספר צרכנים עם צרכים שונים. אם אתם זקוקים ל 3 שאילתות קבועות בין 2 מערכות – ODATA יהיה overkill בשבילכם. פשוט שלחו XML (או JSON) על גבי HTTP וטפלו בכמה שגיאות נפוצות.

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

      אתחיל ב Atom Publishing Protocol (או בקיצור AtomPub, נקרא גם APP – אך זהו שם מבלבל) – תקן שנוצר עבור פרסום נתונים של בלוגים או אתרי חדשות וזכה לאימוץ ע”י מערכות מתחומים שונים לחלוטין.

      גוגל, אחת המאמצות, ראתה שהתקן לא מספיק ושכל מוצר שלה (Calendar, Documents, Gmail) יצר כמה תוספות. היא אספה את התוספות השונות, סידרה אותן ויצרה את GDATA פרוטוקול מורחב המבוסס על AtomPub. לרוע המזל GDATA לא היה בשל כ”כ והיו בו מספר בעיות משמעותיות, אך עם הזמן גוגל מעדכנת ומשפרת את התקן. נדבר גם עליו.

      מייקרוסופט, אחת הפטרוניות המרכזיות של תקני ה WS-* (תקני ה Web Services, אשר REST בא להחליפו) ראתה את התקנים שלה נזנחים ע”י חלקים הולכים וגוברים של התעשייה ולא רצתה להשאר מחוץ לתמונה. היא יצרה תקן בשם ODATA עליו נדבר. היא הבינה שהיא הצטרפה לחגיגה קצת מאוחר. בצעד חריג, עבור מייקרוסופט, היא פתחה את התקן ושחררה ספריות בג’אווה, ג’אווהסקריפט, PHP ועוד המאפשרות להשתמש בו. יש גם דיבורים על מסירת התקן לגוף תקינה חיצוני ובלתי תלוי – אך זה עדיין לא קרה.

      כמובן שיש עוד שחקנים: יאהו מנסה לקדם תקן בשם DataRSS (ללא הצלחה רבה) וה W3C מנסה לקדם תקן בשם Linked Data המבוסס על פורמט בשם RDF – אך נכון להיום התקן שתופס הכי הרבה תאוצה הוא דווקא ODATA של מיקרוסופט והוא מאומץ גם ע”י חברות אינטרנט (נטפליקס, איביי) וגם ע”י ארגוני ענן (יבמ, סאפ).

      ODATA
      OData הוא קיצור ל Open Data Protocol. אם מחפשים עליו חומר כדאי לשים לב ש Open Data הוא מושג רעיוני של שיתוף מידע באינטרנט. חפשו ODATA או Open+Data+Protocol כדי לקבל תוצאות טובות יותר.

      OData פועל על גבי HTTP ומבוסס על עקרונות ה REST. בעצם הוא מבוסס על AtomPub שמבוסס גם על Atom וגם על REST. מבולבלים? הנה לנוחיותכם דיאגרמת Venn שתסביר את העניין:

      • POX הוא הרעיון של העברת נתונים על גבי HTTP בפורמט XML (כלשהו).
      • REST היא ארכיטקטורה מבוססת Resources המבוססת על POX, אך מציבה חוקים נוספים => כלומר, מקרה פרטי של POX. הסתייגות קטנה היא ש REST לא מחייב XML.
      • Atom הוא פורמט מבוסס XML לתיאור Web Feed – רשימת פריטי חדשות, פוסטים בבלוג וכו’. הפורמט המקביל והמוכר קצת יותר הוא RSS.
      • AtomPub הוא מימוש REST המבוסס על פורמט ה Atom שמאפשר לא רק לקרוא Web Feeds אלא גם להוסיף / לעדכן ולמחוק אותם. הוא מוסיף גילוי ועוד כמה יכולות.
      • ODATA (כמו GDATA) מבוסס על AtomPub (ומכאן גם על Atom) ומספק סט כלים עשיר יותר של כלים.

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

      ODATA לוקח את הדברים צעד אחד הלאה:
      • מרחיב מעט את המודל של אטום ומגדיר טיפוסים (דבר שמסייע לתקשורת עם צד השרת)
      • מאפשר להשתמש ב JSON במקום XML (פורמט רזה יותר)
      • מאפשר לבצע שאילתות מורכבות על הנתונים
      • מגדיר מודל אבטחה ו Authentication
      • עוזר לטפל במקביליות
      אציין שפרוטוקול GDATA חופף ל ODATA בהבטים רבים – אך הוא נראה פחות מגובש ומובנה.

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

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

      בפוסט ההמשך אדבר על AtomPub וכיצד אפשר להשתמש בו למימושי REST פשוטים