יום שני, 29 בדצמבר 2014

ריילס, רובי on ריילס

ריילס, או בשמה המלא Ruby On Rails (קיצור מקובל: ROR) היא מסגרת פיתוח (Framework) פופולרית לבניית אפליקציות ווב.
ספציפית: אפליקציות עם ממשק וובי מצד אחד, ובסיס נתונים (בד"כ רלציוני, אבל לא רק) - בצד השני. 

מדוע ריילס?
ריילס הגיעה כתגובה ל JEE (בימיו הפחות יפים - תחילת דרכו) וכאלטרנטיבה פשוטה ומהירה לכתיבת אפליקציות ווב. באותם הימים, מרכז הכובד של JEE היה ה Enterprise JavaBeans (בקיצור EJB) שהיו כבדים ומסובכים. למשל, ה Guideline היה לכתוב לכל רכיב שמייצג נתונים 4 Interfaces נפרדים (רגיל, Home, Remote, ו LocalHome - אם אני זוכר נכון). ל EJB היו ה-מ-ו-ן קובצי קונפיגורציה XML לא-ידידותיים, ולמרות הכל - זה היה טרנד חם ורבים עברו לעבוד בו.

JEE הביא יתרונות רבים (סביבת multi-platform סטנדרטית, המתאימה ל Enterprise), אך במחיר גבוה. הוא כנראה היה טוב יותר מ Microsoft DNA או CORBA (כנראה...), אבל היו אנשים שחשבו שאפשר להשיג את הערך בעלות נמוכה יותר.
אחד מהם הוא Rod Johnson שיצר את Spring Framework, ה Framework שהפך הכל להרבה יותר פשוט (בעיקר הציג אלטרנטיבה פשוטה ל EJBs) - ומה שאח"כ הפך להיות הסטנדרט של JEE, בצורת EJB גרסה 3.0 (שהעתיקה הכל, כמעט, מ Spring + Hibernate).
חלוץ אחר הוא David Heinemeier Hansson (ידוע גם בקיצור DHH), בחור דני צעיר יחסית (יליד 79) שעבד בחברה בשם 37singals (היום נקראת basecamp) שפיתחה לעצמה Framework יעיל מאוד לפיתוח מערכות ווב בשפת רובי. ה Framework הזה יצא כ Open Source ונקרא Ruby on Rails. נראה שריילס, אגב, השפיעה באזורים מסוימים גם על Spring בגרסאותיה המאוחרות (אני לא מדבר רק על Spring Boot) - ומשם היא השפיעה גם על JEE. בכלל, ריילס הוא Framework רק השפעה, שניתן למצוא השפעות שלו במקומות רבים בעולם התוכנה.

מקור השם Rails הוא כזה: "במקום להתקדם באיטיות, כל אחד במסלול שלו, הצטרפו אלי: אני קובע את הדרך - אבל תוכלו לנוע במהירות". ריילס הוא יישום של אוסף של best practices (ש DHH האמין שהן מוצלחות, לא אוסף גנרי) - שמתאימים זה לזה, ומסביבם נכתב Framework שהופך את השימוש בהם למאוד קל ומאוד מהיר. ריילס הוא לא סיפור על פלורליזם - זהו סיפור על דעה מוצקה כיצד נכון לפתח אפליקציות ווב. אם אתם רוצים להיות יעילים בריילס, עליכם לקבל עליכם את "The Rails Way", או לפחות את רובה. אם תקבלו - תגלו את אחד ה Framework הפרודקטייבים הקיימים לפיתוח אפליקציות ווב. ריילס פרחה ביוחד בסביבות של סאטראט-אפים, שהדבר שהכי חשוב להם הוא לספק Value מהיר לשוק שאותו הם עוד לומדים.

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







התכונות העיקריות של ריילס


ההחלטה התכנונית המרכזית של ריילס, אם כן, היא "פריון (productivity) על פני גמישות".
שני עקרונות מרכזיים שנוהגים להזכיר שוב ושוב, בהקשר של ריילס, הם:
  • Convention over Configuration - במקום לומר היכן נמצא משהו (קובץ, משתנה, וכו') ומה שמו - בואו נגדיר כלל מוסכם (convention) היכן הוא אמור להימצא / איך הוא ייקרא - ונחסוך לנו את כל התקשורת הזו. הרעיון של CoC הוא לא חדש, ניתן למצוא אותו במייבן, למשל, שקצת קדמה לריילס. בכל זאת, ריילס היא זו שעשתה PR משמעותי מאוד לעקרון התכנוני הזה (ובמיוחד כ contra ל JEE) - והשפיעה בכך רבות על התעשייה.
  • Don't Repeat Yourself (בקיצור DRY) - גם זה רעיון שכבר מקובל עשרות שנים, אבל ריילס חרטה אותו על דגלה. לא לכתוב בקוד שום דבר פעמיים.

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

ריילס צמחה על גבי שפת רובי, קצת רחוק מהקהילה הטבעית של ג'אווה (ה Enterprises), ולכן ההשפעה שלה הייתה קצת יותר רחוקה. אל דאגה! JEE ננגחה שוב ושוב על היתירות של הקוד שהיא יצרה (בתרגיל הנדסי כ"כ מרשים): גם ע"י NET. (מי שזוכר את קרבות ה PetShop) וגם ע"י Spring - בניצוחו של רוד ג'ונסון (Rod Johnson), שגילה מעט מאוד עדינות כלפי החבר'ה של JEE. היום מצבה של JEE טוב בהרבה מאשר היה באותם הימים, אבל עדיין - פיתוח בריילס נחשב מהיר בהרבה.


עוד אלמנטים מרכזיים בריילס שכדאי לציין הם:


הרחבה לשפת רובי:

רובי, כתכונה של השפה מאפשרת (ומעודדת) שימוש ב metaprogramming בכדי להרחיב את השפה עצמה וליצור DSLs חדשים. יכולות אלו, הן אולי הסיבה המרכזית שריילס הצליחה להיות יותר אפקטיבית [א] מספריות ווב אחריות (ASP.NET MVC, Lavarel או !Play) - שהיו משוחררות גם הן מ"כבלי ארכיטקטורת ה Enterprise של JEE". פקודות שימושיות בכתיבה של אפליקציות ריילס (למשל הגדרות שונות על המודל) הן בעצם הרחבות של ריילס לשפת רובי. מתכנת ריילס ללא רקע משמעותי ברובי עשוי לא להיות מסוגל להבחין אלו תכונות בהן הוא משתמש שייכות לשפת רובי - ואלו שייכות לריילס (ואולי גם לא ממש אכפת לו).


אינטגרציה:

ריילס כוללת stack של טכנולוגיות שמחוברות טוב מאוד זו לזו, ומספקות חווית פיתוח אחידה end-to-end. החל מבסיס הנתונים (Active Records - כלי ה ORM של ריילס), ועד צד הלקוח (SASS, Haml, ו CoffeeScript [ה]) - הכל מרגיש מתאים, ודומה למדי. החוויה דומה, אולי, לזו של פיתוח בפלטפורמת NET. של מייקרוסופט - לה שליטה רחבה על כל ה Stack הטכנולוגי.

עד גרסה 2, ריילס הייתה gem [ב] אחד, שלא גמיש להחלפות. החל מגרסה 2 חלק מהרכיבים (Active Records, למשל) הופיעו כרכיבים הניתנים להחלפה, אם כי ההחלפה בפועל הייתה מאתגרת למדי. ובמשך הזמן ממש התפתחו plug-in APIs המאפשרים לעשות החלפות אלו בצורה פשוטה יותר ויותר.
שלא תבינו לא נכון: אם אתם הולכים לכתוב אפליקציה ראשונה בריילס, ורוצים כבר בשלב זה להחליף רכיבים כראות עיניכם - זה לא ממש כדאי. החלפת רכיבים היא עדיין פרקטיקה מתקדמת שמומלצת רק למי שמבין היטב את דרכי העבודה של ריילס. באפליקציות הראשונות - כדאי להיצמד לסטדרטי ולמקובל.

הגישה שרואה בריילס "אוסף של רכיבים" (מה שטכנית באמת נכון) - מהם ניתן להרכיב תצורה מותאמת-אישית, נקראת (גם) Hexagonal Rails (ע"ש Hexagonal Architecture, שדוגלת בהפרדת ה domain model משאר הרכיבים, ורואה בהם סוג של plugins שמתחברים מסביב אליו) - והיא פחות נפוצה, ואולי אף שנויה במחלוקת: האם ההשקעה, הלא פשוטה, "לאלף את ריילס" מתירה בסופו של דבר תוצר שמצדיק את ההשקעה הזו?





בדיקתיות:

תכונה אחרונה של ריילס שארצה לציין היא "בדיקתיות". יוצרי ריילס (ממש כמו יוצרי AngularJS, למשל) היו מודעים היטב לצורך בבדיקות - והם בנו תשתית שמאפשרת לבדוק את הקוד שרץ עליה בקלות יחסית. בדיקות יחידה ואינטגרציה הן חלק מובנה מריילס, ו flows הבדיקה מאופשרים היטב בכל הרמות , אם כי קצת יותר לכיוון ה integration tests וקצת פחות לכיוון ה isolated unit tests. כמות ספריות הבדיקה הזמינה לשפת רובי היא מרשימה (RSpec, MiniTest, Test::Unit, Cucumber, Factory_girl), ולכמה מהספריות הללו יש הרחבות מיוחדות לריילס (למשל Rspec_rails או Factory_girl_rails). בדיקות הוא לא רעיון זר בקהילת הרובי או ה RoR...

מעניין לציין שדווקא DHH הוא זה בשנה האחרונה העלה כמה דברי ביקורת על כתיבת בדיקות. בגלל ש DHH קידם מאוד את תרבות כתיבת הבדיקות (וגם בגלל שהוא הגורו הבלתי-מערוער בעולם של ריילס?) - דבריו זכו לתהודה רבה. הוא כתיב פוסט בשם Test-induced design damage, ואח"כ העביר סשן בשם "?Is TDD Dead". בסוף הפוסט תוכלו למצוא לינק לסדרת הסשנים המעניינת בה הפגישו אותו עם יוצר ה TDD (קנט בק) ומרטין פאוולר (ששימש כמגשר?). סדרה של 3 שעות - אך בהחלט מעניינת.

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



ריילס בחזית הטכנולוגיה

בעולם של Single Page Applications, בעולם בו חווית משתמש שהפכה מקובלת, שדורשת שקוד משמעותי ירוץ בדפדפן, ובעולם של noSQL - ריילס כבר זוהרת פחות. אפשר לעשות את כל הדברים האלו עם ריילס, אבל היא כבר לא יעילה כמו שהייתה בעבר, ומפתחי ריילס צריכים לפעמים להיות יבשושים ולכתוב קוד משעמם / קצת פחות יעיל.

אם אתם מחפשים את החלופה "העכשווית" לריילס שתוכננה במקור לקוד צד-לקוח משמעותי, ו MongoDB - אתם כנראה מחפשים את MeteorJS (עם כל היתרונות, וגם החסרונות שלה).

אכן נראה שחלק ממפתחי הריילס (כלומר: רובי + ריילס) עוזבים את ריילס ומחפשים Frameworks חדשים לעבוד איתם. הטיעון ששמעתי כבר כמה פעמים הוא "אני לא רואה הגיון לעשות MVC גם בשרת וגם בצד הלקוח" (ברור שלא! מוזר לשמוע בכלל ניסו כזה דבר). באופן טבעי, חלקם מחפשים את המוכר, ולא מפתיע למוצא frameworks שמנסים לספק את הצורך הזה. קשה לי שלא לציין את Sails - של node.js ו grails של שפת גרובי (groovy). האחרון הוא לא חדש - אבל קשה להתעלם מהצליל המוכר שהוא בחר לעצמו :)



הרכיבים העיקריים בריילס


ריילס בבסיסה היא ספריית MVC שמושפעת ישירות מה MVC (המקורי) של שפת smalltalk:

המודל - שומר את ה state של המערכת (בעזרת בסיס הנתונים, אם כי לפעמים רק בזכרון), אוכף "חוקים עסקיים" על נכונות ה data (למשל: סטודנט חייב להיות רשום לתוכנית לימודים כלשהי). הוא לא בהכרח המאמת היחידי של הנתונים, אלא ה gatekeeper לפני שהמידע נשמר / משותף עם חלקים אחרים במערכת. בניגוד ל MVC של Smalltalk הוא גם כולל חלק מהלוגיקה - ראו בהמשך את ההסבר על Active Records.
המודל, בגדול - הוא זמין לכלל האפליקציה, אין בו partitioning (או bounded context).

ה View - הוא הקוד שמייצר דפי HTML למשתמש, על בסיס מידע במודל. ריילס היא תשתית לכתיבת MVC בצד-השרת (כלומר: השרת מייצר HTML, ומוסיף JavaScript לאינטרקטיביות). בדומה למודל ה MVC הקלאסי ה View לא מקבל input מהמשתמש, ולא מנהל איתו שום סוג של אינטרקציה (לפחות לא בצד-השרת).

ה controller - אחראי לקבלת קלט מהמשתמש, טיפול בו, ותפעול (ניתן לומר: orchestration) של המערכת עד שהמשתמש מקבל בחזרה את התשובה שלו. דרך העבודה שלו היא לעדכן את המודל, ואז להזניק את ה View המתאים (הוא לא מעביר מידע ל view ישירות).

בריילס יש גם routers - שהם ממפים URLs, ומצבים - ל Views. לדוגמה: אם משתמש שהוא admin ניגש לדף מסוים - הוא יקבל View אחר ממשתמש רגיל. ה router עושה מה שבמערכות אחרות עושה סוג של "super controller". הוא דקלרטיבי, בעזרת סט של ״פקודות״ מיוחדות לעניין (מה שהופך אותו לפשוט יותר לכתיבה ותחזוקה) - וסה"כ הוא מהווה הרחבה מבורכת על מודל ה MVC ה"קלאסי".



רכיבים עיקריים של ריילס הם:

Active Records (בקיצור: AR)

כלי ה lightweight ORM של רובי, המנהל בפועל את הגישה לבסיס הנתונים של המודל. רובי מספקת גם כלים להגדרת טבלאות ברובי דקלרטיבי (ולא SQL, על הווריאנטים תלויי בסיס הנתונים הספציפי שלו) וכלים מובנים לביצוע migrations בין גרסאות שונות לסכמת בסיס הנתונים (מה שמקובל בג׳אווה לעשות עם flyway).

השם "Active Records" הוא בעצם של שם של דפוס-עיצוב (מופיע בספר PoEAA) אותו המודול מממש.

דפוס העיצוב "Active Record" מגדיר מחלקה בשפה (במקרה שלנו: רובי) שמתארת נתונים של שורה (להלן Record) בבסיס הנתונים, אבל מוסיפה עליהם גם את התנהגות ה domain הרלוונטית לאותם נתונים (להלן Active). למשל: validation, חישובים, derived fields, וכו'. הבהרה: יש מחלקה אחת לכל טבלה, ומייצרים מופעים שלה ע"פ הרשומות להם נזקקים באותו הרגע.

דפוס עיצוב זה מרשה לערבב בין מיפוי הנתונים בין בסיס הנתונים לזיכרון, עם לוגיקה עסקית - כל עוד הלוגיקה העסקית היא רק ברמה של הרשומה הבודדת (ולא "שייכת" ל scope רחב יותר). ערבוב זה, נמצא בוויכוח תמידי על הלגיטימיות שלו, מכיוון שהוא סותר כלל מקובל בארכיטקטורה הנפוצה ביותר למערכות מבוססות-נתונים (הרי היא Layered Architecture) - הפרדה בין Persistence ל Business Logic, או בצורת דפוס העיצוב "Data Mapper" אותה מממשים רוב כלי ה ORM המוכרים - הפרדה מוחלטת בין לוגיקה עסקית למיפוי הנתונים בין בסיס הנתונים לזיכרון [ו].

בריילס, יידרש מאמץ מיוחד בכדי לא-לעבוד עם Active Records, אז כדאי שתתנו לדפוס זה הזדמנות - גם אם כל חייכם "גדלתם" על Data Mappers ;-).

Active Records מאפשר לנו לכתוב אובייקט "פשוט" ברובי, לרשת מ ActiveRecord::Base ולקבל את פעולות ה CRUD הבסיסיות ע"י סיפוק הגדרות בסיסיות של מה שאנו רוצים (declarative programming).
לא! זה לא "בחינם", אין ארוחות-חינם בהנדסת תוכנה. אנו מוותרים על גמישות וקצת על ביצועים - בכדי לקבל את הנוחות הזו.

Active Records מכתיב כללים מוסכמים לשמות -  בכדי לצמצם את הצורך בהגדרות דומות, בכל פעם מחדש.
למשל: שם הטבלה בבסיס הנתונים צריך להתאים לשם המחלקה (בוריאציה מסוימת), וכן שמות ה primary key וה foreign_keys, ועוד. פרטים על כללי השמות - אספק בהמשך הפוסט. אפשר שתהיה חוסר התאמה בשמות - אבל אז צריך לכתוב קונפיגורציה לכך.

את שאר הרכיבים אזכיר בקצרה, לא כי הם פחות חשובים, אלא בגלל שנדרש ידע נוסף בריילס על מנת להבין אותם.
  • Action Pack - זהו הרכיב שמתפעל את ה MVC בריילס. כל פעולת משתמש בריילס נקראת "Action". הוא מורכב מ -3 תתי-מודולים:
    • Action Dispatch - אחראי ל routing.
    • Action Controller - מספק את מחלקת ה ActionController::Base ממנה יורשים כל ה controllers במערכת. 
    • Action View - אחראי לרינדור ה Views, בעזרת מנגנון ה ERB (קיצור של Embedded Ruby) - מנגנון שדומה מאוד ל JSP בג'אווה או ל PHP: סוג של template (בד"כ HTML, יכול להיות אחר) שבו מושתל קוד רובי המתאר את ההתנהגות הדינאמית.
  • Active Model - המשמש כ Facade של AR מול ה Action Pack.
  • Active Resource - המספק יכולות צריכה של שירותי REST חיצוניים. סוג של שירות אינטגרציה בין מערכות.
  • Active support - סט של utilities כלליים (שימושיים) שמגיע עם ריילס. המקבילה של Apache Commons - של מג'אווה.
  • Action Mailer - אשר תופס בתיעוד, משום מה, נתח מכובד. הוא בסה״כ שולח מיילים.
  • Active Job - לניהול Queues של jobs. הוא בעצם שכבת הפשטה למימושים קיימים (כגון Resque) ולא מימוש בפני עצמו. הוא חדש בריילס 4.2 .
  • Railties ("קשרי מסילה") - זהו ה gluing logic של כל רכיבי ה Active / Action למיניהם. ה Strategy של תפעול אפליקציות הריילס.
  • Rails - רכיב קטן למדי, שאחראי על אתחול המערכת - והכנסת המודולים האחרים לפעולה.

כל עבודת ה low-level ברמת ה HTTP הוא לא חלק מריילס. חלק זה יטופל ברמת ה Application Server - עליו נדבר בהמשך.


מבנה אפליקציית ריילס


כחלק מה Convention over Configuration, לאפליקציית ריילס יש מבנה תיקיות מוסכם-מראש. בכדי ליצור מבנה זה יש להקליד בשורת הפקודה:

$ rails new <app name> [optional: -d < sqlite3 | mysql | postgresql >]

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

מבנה אפליקציית ריילס חדשה

התיקיות המעניינות ביותר באפליקציית הריילס הן תיקיות ה app, config, ו lib. אם יש בדיקות טובות - הוסיפו לכך (במקום גבוה) את תיקיית ה tests (לפעמים תראו אותה בשם spec).

תיקיית ה bin כוללת את הסקריפטים לפעולות הבסיסיות של האפליקציה. אלו בעצם wrapper scripts (נקראים ברובי גם binstubs) שרק מכינים את סביבת הריצה ואז קוראים ל executable הנכון. לפני ריילס גרסה 4 קראו לתיקיה זו בשם "scripts".

קובץ ה Gemfile (מתחיל באות גדולה) הוא קובץ של כלי רובי בשם bundler, כלי שעוזר להגדיר, ואז להתקין - תלויות (gems [ב]) על סביבות חדשות בהן תרוץ האפליקציה.
ברגע שמריצים את הפקודה "bundle install", אז bundler יקרא את קובץ ה Gemfile, יבדוק מהן הספריות הנדרשות וגרסאותיהן, ויתקין את מה שלא נמצא - בגרסאות המתאימות. אם אתם מכירים את קובץ ה package.json של node.js - זה ממש אותו הדבר.

קובץ ה Gemfile שנוצר בעקבות הפקודה "rails new" כולל כבר רשימה של ספריות שימושיות: דרייבר לבסיס הנתונים שהגדרתם, כמה ספריות צד-לקוח, ספרייה ל generation של תיעוד, ספריה לעבודה עם JSON, וכו'.

Gemfile.lock הוא קובץ בו bundler מנהל לעצמו את הגרסאות שמותקנות בפועל. זהו קובץ שתרצו להכניס ל git repository שלכם - אך לא לשנות ידנית (אין קשר לקבצי lock של MS Office, שמטפלים ב concurrency).


קובץ ה rakefile הוא של כלי ה build של רובי שנקרא rake. ה build ברובי כולל בעיקר בדיקות, פענוח הסכמה, Linting, וטיפול בקצבי HTML ו CSS (למשל: minification).

קובץ ה rakefile של ריילס יהיה לרוב מינימלי, והדבר המרכזי שהוא יעשה הוא לטעון ולהריץ רשימה של rake tasks (שאתם תכתבו). קבצי ה task יושבים בתיקיה lib/tasks.

בריילס, עובדים לא מעט עם command line עבור scaffolding [ג]: ניהול בסיס הנתונים, ביצוע buid, הרצת האפליקציה, ועוד.

לפעמים הפקודות הללו לא עובדות כראוי, ו"טריק" מקובל הוא לקרוא ל  <bundle exec rake <rake params - מה שהופך את כל ה gems שמצוינים ב Gemfile לזמינים, אפילו אם הם לא ב path של מערכת ההפעלה.


התיקייה lib, ע"פ הגדרה אחת, כוללת קוד שהוא לא מודל, controller, router, או View.
התיקייה lib, ע"פ הגדרה נוספת, כוללת קוד שניתן לעשות שימוש חוזר בו בין הרכיבים השונים (view, model, controller).
מלבד כמה תתי ספריות נדרשות (למשל tasks) - יש לכם חופש כיצד לארגן אותה.

בגרסאות ישנות של ריילס, כל הקבצים שישבו ב lib נטענו כזמינים לכל הרכיבים במערכת. כלל זה שונה והיום עליכם להדיר במפורש באיזה lib רכיב ה app שלכם רצה להשתמש - בכדי להשתמש בו. ( או פשוט להוסיף ל config/application.rb את הפקודה config.autoload_paths += %W(#{Rails.root}/lib שתעשה מה שריילס עשתה בגרסאות ישנות).

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


שני קבצים חשובים שנטענים בעליה של ריילס הם config/environement.rb ו config/application.rb.
בנוסף תמצאו בתיקיה config/environemnts קובץ קונפיגורציה לכל סוג סביבה: development, production ו test - זה הבסיס למערכת ה staging של ריילס.
כשמפעילים את אפליקציית הריילס, ניתן לציין את הסביבה כפרמטר, למשל:

$ rails server - e development

בכדי לטעון קונפיגורציה לסביבת ה staging המתאימה.

זהו.

ייתכן ותתקלו בקבצים ריקים בשם ״keep.״. מטרתם היא למנוע מכלים מסוימים להתעלם מהתיקיות שנוצרו ע"י "rails new" - וניתן (ומומלץ) למחוק אותם כאשר התיקיה מתמלאה בתוכן. חבל לראות פרויקט ריילס בן כמה שנים - שעדיין שמר את הקבצים הללו.



החידושים שהגיעו עם ריילס 4, ששוחררה באמצע 2013. גרסה מג'ורית של ריילס משוחררת אחת לשלוש שנים, בערך. מקור.



ה Application Server


הפעלת האפליקציה נעשית ע"י פקודת "rails server", או בקיצור "rails s".
כל אפליקציית ריילס רצה על "שרת ווב משלה", בתוך תהליך של מערכת ההפעלה. ניהול של כמה אפליקציות על אותו שרת פיסי, מתרחשת ע"י הפעלה של כמה "תהליכי שרת", אולי מסוגים שונים ו/או גרסאות שונות.

ריילס מגיע כברירת מחדל עם שרת "רזה" בשם WEBrick. זהו שרת פשוט למדי - שמשמש בעיקר לצורכי פיתוח (ולא ל production). ב Production מקובל להשתמש ב Puma, Raptor או ב Unicorn.

Rack הוא שמה של הספציפיקציה של שרת ווב ברובי, וגם השם של ה Reference Implementation שלה.
Rack דומה בתפקידו ל Servlet API של ג'אווה, שבמקרה יש לו גם מימוש באותו השם (דמיינו ש Tomcat נקרא "Servlet"). החלק החשוב יותר של Rack הוא הספסיפיקציה. כנראה שהחבר'ה של רובי ראו מה קרה בעולם של פייטון, שבו לא היה תקן שכזה, וכל שרת ווב עושה את אותו הדבר - אבל מגדיר זאת אחרת.

סה"כ ה API ש Rack מציע הוא מאוד בסיסי: הוא עובד ברמת הפשטה מאוד נמוכה - ה HTTP Request-Response Lifecycle (בדומה ל CGI - אם אתם ותיקים מספיק בכדי לדעת מה זה).

מצד מימושי השרת, המימושים הנפוצים ביותר הם Rack (כמובן), Unicorn, או Mongrel.
ספסיפקציית ה Rack משתמש לא רק את RoR, אלא גם Frameworks אחרים כגון Sinatra, Ramaze ו Merb (שמוזג לתוך RoR בגרסה 3.0).

כמה מלים על Rack כ reference implementation:
מקור השם Rack הוא הכוננית בחדר השרתים שעליה מתקינים את השרתים / ציוד הרשת. יוצריו בחרו בשם זה בגלל מנגנון ה plugins שלו, שמאפשר למודולים צד שלישי (הנקראים middleware, דומים ל Servlet Filters בג'אווה) להרחיב אותו בקלות. הנה רשימה של מודולים זמינים.
מעניין להזכיר את Rack::MockRequest שמייצר mocks לאובייקטי request ו response (נחמד שזה מגיע מ"הספק"), ו Rack::Lint שעושה Linting על קוד המשתמש ב API של Rack ומוצא כמה common pitfalls.

קובץ הקונפיגורציה של Rack, הנקרא config.ru מכיל את ההגדרה של ה middlewares שאנו משתמשים בהם, ו metals.
Rails Metal Apps (בקיצור: metal, מלשון "bare-metal" - אני מניח) הן חתיכות קוד באפליקציית הריילס שלנו, שירוצו ישירות מעל ה API של Rack, ללא תוספות.

מדוע לעשות זאת? בגלל ביצועים, למשל: כדי לספק REST API שקוראים לו בקצב מהיר.

ה stack של ריילס שמריץ את ה controllers (ה Action Controller Stack) מציב תקורה משמעותית לכל request שמטופל. אם אנו רוצים לספק תשובה פשוטה, ולעשות זאת מהר - כדאי לעקוף אותו.

הערה: ברור שאם אנו זקוקים לביצועים ממש גבוהים - גם metal עדיין מציב מגבלה על מה שאפשר להשיג מהחומרה. שפת רובי היא לא concurrent (השתפרה, אבל עדיין לא ממש) ואין לה parallelism. כדי להגיע ל throughput או tps גבוה במיוחד - כדאי לשקול עבודה נקודתית עם node.js (עבור קוד "רעב" ל I/O) או עבודה ב Go (עבור קוד "רעב" ל CPU) [ד].

אחרונה חביבה, היא ספרייה בשם spring שעובדת עם rake (שהיא כבר חלק מובנה מריילס 4.1 ומעלה) שעושה preloading לשינויים באפליקציית הריילס מבלי לעשות restart לשרת (אלא על בסיס של fork של ה process - לא עובד ב Windows או ב JRuby). יכולת דומה קיימת באופן מובנה בשרת ה Unicorn.







ה Conventions של ריילס


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

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

שמות של טבלאות בבסיס הנתונים יהיו ב snake_case - כמו משתנים. טבלאות תמיד יקראו ע"פ צורת הרבים של האובייקט (plural): למשל People או Invoices ולא Person או Invoice.
למה? כדי שהקוד יהיה קרוב יותר לשפה הטבעית: "Select a Product from products". אני מניח שהבדל זה יכול גם להקל להבחין מהר בין מחלקה לטבלה בבסיס הנתונים.

שמות של קבצים (model, view, controller, וכו') יהיו גם הם ב snake_case - כפי שמקובל ברובי גם מחוץ לריילס.

נניח שאנו רוצים ליצור מודל בשם LineItem (שם מחלקה ברובי הוא CamelCase). כדי שהכל יעבוד, ללא קונפיגורציה נוספת, על שם הקובץ המכיל את המחלקה להיות line_item.rb (והיא תהיה בתיקייה app/models), ועל שם הטבלה בבסיס הנתונים להיות line_items.

"אני מבין איך עושים זאת עבור line_items, אבל איך *לעזאזל* ריילס תקשר עבורי בין people ל person?". שאלה טובה. הציצו ב utility של ActiveSupport שעושה זאת: inflections.rb. הוא מכיר יוצאי-דופן מוכרים כמו person-people או octopus-octupi, ואתם גם יכולים ללמד אותו כללים חדשים, למשל: selfie-selfieez.

ל controllers יש כללים נוספים:

אם יש לנו Controller ששם המחלקה שלו הוא InvoiceController, אז עליו להיות בקובץ בשם invoice_controller.rb שנמצא בתיקייה app/controllers. עד כאן - זה כמו המודלים.

בנוסף, ריילס תצפה שיהיה קובץ נוסף בשם invoice_helper.rb בתיקייה app/helpers שיכיל מחלקה בשם InvoiceHelper.

ריילס גם תצפה שה view templates של ה controller הזה יהיו בתיקיה בשם apps/views/invoice. כהתנהגות ברירת מחדל, ריילס תשתמש ב output של ה templates הללו ותכיל אותם בתוך layout template (לא דיברנו עדיין ממש על הפרטים של ה view...) בשם invoice.html.erb (או invoice.xml.erb - אם מישהו משתמש עדיין ב XML/XHTML) שנמצא בתיקייה app/views/layout.

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

אם יש לנו כמה controllers הקשורים זה לזה (למשל Admin screens), אנו יכולים להגדיר אותם בהיררכיה בתוך התיקיה app/controllers. אם הדפדפן ביקש URL בשל admin/user אז ריילס תחפש את ה controller בקובץ בשם user_controller.rb בתיקיה app/controllers/admin (כלומר: תת-התיקיה admin מקבצת את כל ה controllers בקבוצה).

בכדי למנוע התנגשות בין שני controllers בעלי אותו השם (נאמר user_conroller.rb בתיקיה app/controller/report), שם המחלקה יהיה namespaced ע"י מודול בשם של תת-התיקיה. במקרה שלנו: Admin::UserController.

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





---

קישורים נוספים:

The Rails Doctrine - הרעיונות התכנוניים / פילוסופיים מאחורי ריילס.

מה ההבדל בין RDoc ל Markdown

?Is TDD Dead

A Conversation with Badri Janakiraman about Hexagonal Rails

פוסט על הארכיטקטורה של ריילס




---


[א] זו לא אמת חד-משמעית, אך כך מקובל להאמין.

[ב] Gems, הן ספריות קוד של רובי אותן ניתן להתקין בקלות (<gem install <gem name $) מ repository משותף-לכל. דומה ל npm packages של node.s, ל eggs של פייטון, או Pears (אגסים) של PHP.

[ג] פירוש המילה: פיגומים. הוספת templates קטנים (controller חדש, מודל חדש) תוך כדי עבודה ע"י הפעלה ב command line של "... rails generate", או בקיצור: "... rails g". זו המקבילה הרזה ל Create New <...> Wizard ב Eclipse, למשל. פקודת rails generate דומה מאוד ל Yo - למי שמכיר, ובעצם הייתה עבורה מקור ההשראה.

[ד] יש כמובן את Scala, Elixier, Rust, ועוד. הצגתי את הבחירות הנפוצות של אנשי רובי.

[ה] טכנולוגיות אלו הן לא "של ריילס", אבל ריילס עושה בהן שימוש מקיף, ואלי גם השפיעה עליהן במידה:
  • SASS - שפת מטא ל CSS
  • Haml - תחביר מקוצר ל HTML 
  • CoffeeScript - שפת מטא לכתיבת ג'אווהסקריפט, בתחביר שדומה לשפת רובי.
כתבתי על שפות המטא של ג'אווהסקיפט ו CCS בעבר.

[ו] Data Mapper, כמו Hibernate - מבודד את ה Domain מבסיס הנתונים, בכדי להפוך את ה Domain Logic לבלתי תלוי, קל לבדיקה (Unit Testing), ולאפשר שינויים בבסיס הנתונים במנותק מה Domain וליהפך. אמנם במקרים רבים הצוות משתמש ב Data Mapper כי זה "Best Practice" למרות שהכוונה הברורה היא שבסיס הנתונים יהיו מיפוי מדויק של המודל - או ליהפך (הכוונה: תמיד אובייקט = טבלה, בלי שום רמת הפשטה). כמובן שרמת ההפשטה האפשרית בפועל מעל בסיס הנתונים היא מסוימת - ולא "אינסופית". כדאי להזכיר ש Data Mappers נולדו בסביבת ה Enterprise, בה לעתים רבות לאפליקציה לא הייתה בלעדיות על ה Database schema.

יום שני, 22 בדצמבר 2014

פוסט חנוכה: הגיגי-שפה

מה יותר "חד": Bleeding Edge או Cutting Edge?
מה ההבדל בין Accurate ו Precise (יש הבדל משמעותי) או בין Edge case ו Corner case?

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



סופגנייה מול Donuts - מה בעצם ההבדל?


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

ניסוי ותהייה או ניסוי וטעייה?

אני שנים חשבתי שמדובר בתהייה... מלשון לתהות, עד שהסבירו לי שמדובר ב"ניסוי וטעייה" - מלשון טעות: "נסה, וקבל פידבק שלילי על כישלונות".
האמת היא ש"טעייה" היא בחירה קונקרטית של מישהו באקדמיה ללשון עברית, אבל יש כאלו שעדיין מעדיפים תרגום הביטוי "Trial and Error" ל"ניסוי ותהייה" או אפילו ל"ניסוי ותעייה" (לתעות - ללכת לאיבוד). הכל עניין של טעם.


עיקר ותפל או עיקר וטפל?

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

ובכן טפל הוא "משני" והבחנה בין "עיקר וטפל" היא המקבילה לביטוי האנגלי "Primary and Secondary", כלומר: הבחנה בין הכי חשוב, לשני הכי חשוב (הבחנה שהיא לא-פשוטה), ולא כפי שניתן לחשוב בטעות שזו הבחנה בין "נהדר למטופש" - משהו שנשמע בסיסי או קל.


(שימו לב שעל כמה מההבדלים להלן - כתבתי בפוסטים לאורך השנים)


Parameter או Argument

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


Accurate vs. Precise

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


בגדול: Accuracy הוא הכיוון הנכון, Precision הוא הדיוק בפרטים.



מקור הביטוי Pros and Cons

מה זה "Pro" ומה זה "Con"? האם אלו מלים? האם ניתן להשתמש בהן בצורת יחיד? מה משמעותן?
ובכן, המקור הוא ביטוי בלטינית בשם "pro and contra" שמשמעו: "בעד ונגד", ובפועל משמש בימנו כ"יתרונות וחסרונות". contra קוצר ל con, ומכיוון שיש יותר מייתרון / חסרון יחיד - משתמשים בהם בצורת רבים: pros and cons.


Transparent vs. Opaque

לפעמים רואים מלים אלו כנרדפות ומתארות שקיפות ("זה שקוף לך!"), אבל בפועל אלו הפכים:
Transparent - משמע שקוף.
Opaque - משמע אטום.

כשאנו משתמשים בהכמסה בקוד בכדי להסתיר פרטי מימוש אנו נוהגים לומר: "זה transparent - זה שקוף לך!", למרות שבפועל היה נכון לומר: "זה opaque - זה אטום לך! אתה יכול לא-לדעת".


General vs. Generic

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

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


Corner Case vs. Edge Case

האם יש באמת הבדל? האם אני צריך להסיק מסקנות אישית - כשאני מריץ בגוגל כאלו שאילתות?!

ובכן - יש הבדל מוגדר היטב:

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

Corner Case (איני יודע אם יש מינוח עברי מקובל) - הוא תופעה פתולוגית, שמתרחשת רק כאשר רצף של מקרים, לא נפוצים מתרחש. באופן ויזאולי ניתן לחשוב עליו כהרכבה של כמה edge cases שמתרחשים בו-זמנית (ואכן פינה היא נקודת חיבור של כמה קצוות). זה בעצם מה שנתקלים בו כאשר משחררים תוכנה מורכבת ללקוחות מורכבים.



מה יותר חד: "bleeding edge" או "cutting edge"?

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

באופן מעט מפתיע - דווקא יש הגדרה ברורה:

Cutting Edge (ביטוי נרדף: "Leading Edge") מציין את הפיתוח הכי מתוחכם ומתקדם בתחום. אין לו הקשר לסיכון או לחוסר בשלות.

Bleeding Edge הוא לאו דווקא קשור ל"קצה החד", אלא לקצה שממנו אפשר להחתך בשימוש: טכנולוגיה לא בשלה הטומנת בחובה סיכון גבוה.

ההבדל הוא בעצם בין "הטוב ביותר" ל"חדש ביותר" (מקור).


מהיכן מגיע המונח "באג"?

(לצעירים שביננו שעדיין לא שמעו זאת כבר חמש פעמים)

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

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

הנה צילום המחברת של גרייס (שנלקח מתוך הביוגרפיה שלה):




UK מול "בריטניה הגדולה"

נכון, זה לא ממש לא קשור לתחום התוכנה, אבל נתקלתי בהבחנה הזו כחלק מהעבודה - תוך כדי קריאה של איזה תקן.
מה שמשך את תשומת לבי היא הערה בתקן שאמרה בערך כך: "אמנם אנו משתמשים בקיצור GB - אבל הכוונה היא ל United Kingdom ולא ל Great Britian, זו טעות היסטורית שלא ניתן לשנות".

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

מקור



כמה הגיות שאנו נוהגים להגות לא נכון (תזכורת):

מה שעלול לבלבל את השומעים הלא-ישראלים שלנו במידה...

Digital נשמע כמו דיג'יטל, ולא דיגיטל.

Medium נשמע כמו מידיום (midi) ולא מדיום (med). מדיום הוא מדיום-תקשורת.

Char הוא קיצור של Character, ולכן הגיוני יותר להגות אותו כ"קר" ולא כ"צ'ר" (למרות ששמעתי גם אמריקאים אומרים "צ'ר").

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

Data הוא דייטה (dey) ולא דטה (da).

Micro (כמו במיקרו-שירותים) הוא מייקרו (mai) ולא מיקרו (mi)


סיכום


אני מקווה שניהנתם.
אתם יותר ממוזמנים להגיב ולהוסיף את ההגיגים שלכם.

וחג שמח!




יום שני, 15 בדצמבר 2014

דפוסי ארכיטקטורה: מיקרו-שירותים (Micro-Services Architecture)


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

מיקרו-שירותים הוא "הטרנד החם" בנושא הארכיטקטורה בשנים האחרונות, ונראה שהוא כבר עכשיו הדיח את ארכיטקטורת השכבות ממקומה והפך ל "ארכיטקטורת ברירת המחדל למערכות ווב" (או מה שאוהבים לקרוא לו היום Twelve-Factor App)

יש בזה הגיון רב: בסאפ יצא לי לראות מערכות בהן Layered Architecture התאימה בצורה נהדרת לאתגרים והדרישות. מדובר במערכות עסקיות, גדולות ומורכבות בהן ה UI הוא שיקוף של הסכמה בבסיס הנתונים.
במערכות מודרניות יותר, בהן ה UI הוא עשיר יותר ובעצם בסיס הנתונים הוא שיקוף של חווית השימוש שתוכננה, חלק מההנחות של ארכיטקטורת השכבות (למשל: "ה UI משתנה ללא שינויים ב Business Logic ו/או ה Persistence") - פשוט לא משקפות את המציאות.

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

חשוב לזכור: דפוס ארכיטקטורה הוא כמו "מרק מוכן בשקית": סביר להניח שווריאציות על הדפוס שמותאמות אליכם אישית יוכלו להיות טובות יותר מ"הצמדות לדפוס כפי שתועד, ללא עוררין". כמו כן חשוב לזכור שלמרות שזהו דפוס פופולרי במיוחד בימים אלו - זהו לא הפתרון לכל בעיה. עדיין יש מערכות, עם UI מודרני (וכו') - שדפוסי ארכיטקטורה אחרים (גם Layered Architecture) מתאימים להם יותר. חשוב לזהות את סט הבעיות הייחודי למערכת שלכם - ולהתאים את הארכיטקטורה שעונה בצורה הטובה ביותר עליהם, ולא דווקא את הארכיטקטורה הנפוצה / פופולרית / "נחשבת" ביותר.






הסבירו לי נא, בדקה, מה "הקטע" של מיקרו-שירותים?


אנחנו עוד נחזור ונעמיק בדברים, אבל אנסה קודם להסביר בקצרה את עיקרי-הדברים.

הגורם הדוחף העיקרי לארכיטקטורה של מיקרו-שירותים הוא כנראה הנושא של Continuous Deployment: כיצד אנו מרכיבים את המערכת שלנו כך שנוכל לעשות deploy לגרסה חדשה 10 פעמים ביום - ומשם מאיצים את הקצב עוד ועוד?
ארכיטקטורה של שכבות בכלל לא מסייעת לעניין זה, אלא אם אתם הולכים לעשות deploy כל פעם ל Layer בודד אחר.

בפועל, רוב היכולות של מערכות הווב המודרניות מרכיבות גם UI, גם לוגיקה, וגם Persistence - כך שכל deploy דורש את כל שכבות המערכת.

מיקרו-שירותים הוא תהליך פירוק המערכת (מודולריזציה) לרכיבים (נקראים "שירותים"). ה"חיתוך" הוא לא אופקי (כמו במודל השכבות - ע"פ רמת הפשטה), אלא אנכי (ע"פ הפונקציה). כמו כן מחלקים את המערכת ליותר חלקים. תכונות השירותים הן:
  • ניתן לעשות לכל שירות deploy באופן בלתי-תלוי באחרים.
    לאמזון יש עשרות אלפי מיקרו-שירותים, והיא עושה deploy כל 10 שניות (בממוצע). איך ייתכן? היא פשוט עושה deploy כל פעם לחתיכה קטנה אחרת של קוד.
    בדרך כלל מיקרו-שירותים מורצים כתהליכים נפרדים של מערכת ההפעלה, המתקשרים זה עם זה על גבי HTTP / REST.
  • ניתן לפתח כל שירות באופן בלתי תלוי: CI, בדיקות, וסביבת העבודה היא בלתי-תלויה. אולי גם שפת התכנות.
    תכונה זו היא מרכזית מאוד, ומטרתה לשפר את ה scalability של הפיתוח: כמה מפתחים בצורה יעילה במקביל על הקוד. ההנחה כאן שהיא שיותר קל לפתח הרבה רכיבים קטנים - מאשר רכיב אחד גדול.
  • לכל שירות יש Persistence בלתי-תלוי.
    תנאי זה בא לשרת את שני העקרונות הקודמים - והוא לא מטרה בפני עצמה. Persistence היא תלות בין רכיבים במערכת, אם כי פחות מורגשת. אם שני שירותים ישתמשו באותן טבלאות בבסיס הנתונים - יהיה לכם הרבה יותר קשה להגיע ל deployment בלתי תלוי. גם אי-התלות בסביבת הפיתוח תשתפר מתנאי זה.
  • מיקרו-שירותים הם קטנים.
    "כמה קטנים?" - זהו דיון חם עליו אפשר לכתוב פוסט בפני עצמו. הנה כמה מדדים מקובלים (ולא-מתואמים) לגודל המומלץ למיקרו-שירותים. למרות שהם שונים - כולם מרמזים על "קטן למדי":
    • מספיק קטן כך שאדם אחד יכול להכיר היטב את כל הקוד של השירות ("כזה שיכנס כולו לראש שלי" - הגדיר ג'יימס לואיס, חלוץ בתחום ה MSA)
    • מספיק קטן שצוות אחד (שניתן להאכיל בעזרת 2 פיצות אמריקאיות גדולות) - יכול לפתח ולתחזק אותו.
    • כמה מאות שורות של קוד. 
    • מספיק קטן שלא יהיה קשה לארגון "לזרוק" אותו - ולכתוב אותו מחדש.
      מספרים שדף הבית של אמזון מפעיל כ 100-150 שירותים שונים (תלוי במקרה המדויק) בכדי לשרת כניסה של משתמש.
  • שירות עושה "דבר אחד בלבד" - Single Responsibility Principle.
    כמובן שגם "מתפעל את הלוגיסטיקה של וולמארט" הוא דבר אחד בלבד - ולא דבר קטן בכלל!
    זהו כמובן כלל קונספטואלי, שלא ניתן לאמוד אותו "מתמטית" - ושיהיה עליכם ליצור קונצנזוס ארגוני לגביו. כלל זה בא לשרת ולסייע לכם לקיים את כל התנאים הנ"ל.


היתרונות העיקריים של ארכיטקטורת המיקרו-שירותים הם:
  • ארכיטקטורה שתומכת ב Continuous Deployment. שניה! האם CD הוא מטרה או אמצעי? ובכן... גם וגם. CD משרת מטרות עסקיות (feedback מהיר ולמידה מהירה) והוא מנבא מוצלח של ההצלחה העסקית של הארגון (מקור).
  • Scalability של הפיתוח, הרבה מפתחים יכולים לעשות הרבה שינויים במקביל - ולפחד פחות.
    הנחה סמויה - יש לכם לא רק את הארכיטקטורה של מיקרו-שירותים, אלא גם מערך מקיף של בדיקות אוטומטיות שהכרחיות לקיום של CD סביר.
  • High Availability - קריסה של שירות אחד (או חמישה) - לא משביתים את המערכת שלכם, אם הם לא קריטיים ל flows המרכזיים של המערכת. שירות קרס בצורה בלתי צפויה? יש memory leak? מכיוון שכל שירות רץ כתהליך נפרד של מערכת ההפעלה - הוא עשוי לא להשפיע על השירותים האחרים. קריסה של קוד ב Layered Architecture / Monolith - סביר יותר שתגרום להשבתה.
  • אפשור / קידום שכתוב הדרגתי של המערכת.
    אם למשל, החלטתם לעבור מרובי לסקאלה (עוד מופע של "Ruby doesn't scale") או מ Framework אחד ל Framework שני - המשמעות בארכיטקטורה Layered היא דרמטית: עצירה ארוכה בכדי לאפשר שינוי שכזה.
    מצד שני, זה הרבה יותר פשוט בארכיטקטורת מיקרו-שירותים: ניתן להתחיל להעביר שירותים ל Stack החדש בזה אחר זה - וחלק מהם לא להעביר לעולם. רק שימו לב שאתם לא נגררים לתפעול של stacks רבים רק כי "אפשר", או מחוסר תשומת לב - זו טעות קלאסית.
    תכונה זו של הארכיטקטורה היא שימושית במיוחד כאשר אתם חיים בעולם שבו המערכת שלכם נמצאת ב Refactoring תמידי (כי העסק דינאמי ומבנה השירות הישן כבר לא עושה את העבודה אחרי שנה-שנתיים...). 
  • Scalability של ה production - מכיוון שאתם יכולים "לשכפל" ו/או לבצע אופטימיזציות על שירות X מבלי להתמודד עם שכפול או השפעות הביצועים של שירות Y שמתקשה להשתכפל. בעיות Scalability מטופלות בשיטה "פרה, פרה".
    נושא זה הוא גם, בד בבד, חולשה של מיקרו-שירותים, נסביר מיד.


באופן טבעי, יש לארכיטקטורת מיקרו-השירותים גם כמה חולשות:
  • קושי ב monitoring ושחזור בעיות ב production. כאשר יש לכם עשרות (שלא לדבר על מאות) שירותים שונים - קשה יותר לשחזר בעיות ולנתח. למשל: לאסוף ולסנכרן לוגים של שירותים שונים, במיוחד אם כמה שירותים התעדכנו לגרסאות שונות מאז. צפו השקעה בניהול והפצה של ה session id בין כל הקריאות. מיקרו שירותים "מפרקים את התמונה" להרבה תמונות קטנות, וכדי להבין מה קורה במערכת - יש להשקיע ב "להרכיב את התמונה בחזרה".
  • Scalability של ה production, ויעילות בכלל - מכיוון ששירותים שיכלו עד עכשיו לתקשר בזיכרון, מתקשרים כעת על גבי HTTP  - לכל קריאה נוסף overhead מסוים. מכיוון שיש הרבה מיקרו-שירותים, שיגרמו להרבה קריאות וה overhead הזה ילך ויגבר.
    פעמים רבות, ה overhead הזה מחייב מעבר ל I/O אסינכרוני - מה שמסבך את הקוד.
  • Refracting יכול להיות מורכב - אם הוא חוצה-שירותים.
  • Operational Complexity - בעוד monitoring הוא האתגר הראשון, הוא לא האחרון. תפעול של שירות בודד אולי הוא קל יותר, אך לתפעל מערכת של מאות שירותים (גרסאות, היכן רצים, כיצד לשדרג) - היא משימה לא קלה לכל הדעות. אומרים שאימוץ של מיקרו-שירותים מעביר מורכבות מהפיתוח ל Operations. סביר להניח שאימוץ ארכיטקטורה של מיקרו-שירותים תאלץ את גוף ה Operations שלכם "לעלות מדרגה" מבחינת היכולות שלו. אם הוא לא יצליח - זה הולך להיות כואב מאוד...

באופן מעט מפתיע, המונח Microservice Envy נוסף כמשהו שכדאי להיזהר ממנו, בדו"ח של Technology Radar Jan2015 של חברת Thoghtworks. למה מפתיע? כי Micro-Services הוא משהו שמדברים עליו קצת יותר משנה, אז הגענו מהר למדי למצב בו מתריעים משימוש יתר / שימוש ללא הבנה מספיקה. הנה הציטוט המדוייק:

We remain convinced that microservices can offer significant advantages to organizations, in terms of improving team autonomy and faster frequency of change. The additional complexity that comes from distributed systems requires an additional level of maturity and investment. We are concerned that some teams are rushing in to adopting microservices without understanding the changes to development, test, and operations that are required to do them well. Our general advice remains simple. Avoid microservice envy and start with one or two services before rushing headlong into developing more, to allow your teams time to adjust and understand the right level of granularity.





אז מה ההבדל בין מיקרו-שרותים ל SOA?


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

הקושי הבולט שאני נתקל בו בניסיון לענות על השאלה הזו, היא שבעוד שאנו מסכימים (כנראה) מהם מיקרו-שירותים - מי בכלל מסכים בעולם על ההגדרה של SOA?!

הדרך היחידה בה אני יכול לענות על השאלה הזו, היא להניח כל פעם על פרשנות שונה של "SOA" - ואז לנסות ולענות.


אם SOA עבורכם הם SOAP ותקני ה *-WS
אז התשובה היא פשוטה: אתם יכולים להשתמש ב *-WS גם במיקרו שירותים, אבל סביר שתקורת הפיתוח ותקורת הביצועים - יהרגו אתכם.
אחת מהנחות היסוד של *-WS היא "coarse grained services" - וזה היהפך המוחלט בהנחת היסוד של מיקרו-שירותים שהם קטנים. חוץ מזה - אין ספק שיש גם הרבה עקרונות משותפים.


אם SOA עבורכם היא ניהול ("mix and match") של שירותים בדמות ESB או CMDB
ESB (קיצור של Enterprise Service Bus) הוא הרעיון בו ניתן יהיה לעשות שינויים במערכת לא בקוד, אלא בקונפיגורציה - וכנראה ע"י power business users. יהיו הרבה שירותים שעושים פעולות טכניות (חישוב עלות טיסה, רישום הזמנה, וכו') - אך כל flow בעל משמעות יורכב בעצם ב ESB ע"י חיווט של השירותים הנתונים בקונפיגורציות שונות.

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

לגבי CMDB (קיצור של Configuration Management DB) - עדיין אין לי תמונה ברורה לגביהם. לי אישית זה נראה כמו "ניסיון שני" לעשות ESB, כאשר העקרונות מעט שונים (בשאיפה: לקחים) ואין שום התייחסות רשמית לקשר / לדמיון ל ESB - אולי בכדי לא להבריח משתמשים.

לצורך הדיון שלנו - זה לא משנה: ESB ו CMDB מדברים על "שירותים טיפשים - חיווט חכם" בעוד מיקרו-שירותים מדברים בבירור על "שירותים חכמים - חיווט טיפש". ייתכן ובמיקרו-שירותים תנהלו קונפיגורציה מרכזית היכן נמצא כל שירות (routing דינאמי ע"פ זמינות נוכחית, אולי גם קרבה גיאוגרפית) - אבל בזה בערך זה יסתיים. ה Flows העסקיים מוגדרים בקוד ע"י "שירותים ברמת הפשטה גבוהה" שקוראים לשירותים ב"רמת הפשטה נמוכה". אין פה עניינים של קונפיגורציה.


אם SOA עבורכם הוא מידול של המערכת לשירותים stateless ופונקציונליים
כלומר: אתם מאמינים שניתן לחלק את המערכת לפונקציות ("רכיב לכל משימה") - ולא דווקא ע"פ חלוקה OO. את השירותים עצמם - ניתן כמובן לפתח ב OO. כמו כן - אי-תלות מובנה בין השירותים.

במקרה כזה, ההבדל בין SOA למיקרו-שירותים הוא סמנטי, כלומר: יש להתאמץ מעט בכדי למצוא אותו. לא בכדי קוראים אנשי המיקרו-שירותים לפרשנות כזו של SOA בשם "SOA done right" (דוגמה, במסגרת הכתומה).

ההבדלים שניתן למצוא (כי חיפשנו) הם:
  • CD כסביבת הפעולה
  • אי-תלות ב Persistence כתנאי מרכזי (ולא רק כ"אפשר עם, אפשר בלי")
  • הקפדה על גודל קטן, והשיח על "זריקת שירותים - וכתיבתם מחדש, כתהליך מכוון".
לא הבדלים שקשה לגשר עליהם - אם אתם כבר מאמינים בפרשנות הזו של SOA.




האנטומיה של מיקרו-שירות



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


עד עכשיו סיפקתי סקירה high level של הנושא - אך יש עוד פרטים רבים שהייתי רוצה לגעת בהם.
למשל: כיצד מתחילים? כיצד "מפרקים" מערכת למיקרו-שירותים (להזכיר: MSA = Micro-Services Architecture)?

כלל מנחה יסודי הוא שהחלוקה לשירותים היא פונקציונלית ועסקית. כלומר: החלוקה לשירותים היא ע"פ פונקציה עסקית (חישוב עלות טיסה, ביצוע הזמנה, ביטול הזמנה, וכו'). כל שירות כולל את כל המרכיבים הטכניים הנדרשים לביצוע המשימה: UI, לוגיקה עסקית, ו Persistence.

על השירות להיות self-contained: להכיל את ה deployment script העצמאי משלו, את הפרוייקט ב IDE משלו, את הבדיקות שלו, את התלויות שלו (ספריות צד-שלישי, בגרסאותיהן השונות) - וכו'.
מבחן טוב לעצמאות הזו היא מפתח חדש שמקנפג סביבה רק עבור השירות הזה, פותח רק אותו ב IDE - ומצליח לעשות שינויים ולהעביר אותם ל production מבלי להסתבך. השאיפה היא ליצור מערכת גדולה - מתוך הרבה רכיבים קטנים ופשוטים, ושפיתוח של כל רכיב קטן שכזה - יהיה פשוט כמו כתיבה של רכיב פשוט עצמאי (נאמר: תרגיל בקורס באוניברסיטה).

השלכה משמעותית של רעיון זה, הוא שאת השירות צריכה לכתוב קבוצת אנשים שיכולה לבצע את כל המטלות הנדרשות מהם (UI, Operations, DB, ולוגיקה עסקית). ע"פ Conway's law, אם בארגון שלכם יש "צוותי UI", "צוותי DB", ו"צוותי server" (ולא "צוותים פונקציונליים") - אזי סביר שהיישום של השירותים אצלכם יהיה: שירותי UI, שירותי DB, ושירותי Server - שזה נחשב מידול לא טוב של מיקרו-שירותים. ייתכן ויהיה עליכם לשנות את המבנה הארגוני בכדי להגיע ל MSA מוצלח.

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

הנה כמה תשובות לדילמות נפוצות. שימו לב שהן רק guideline - מכיוון שהן נענות "למקרה הממוצע", ולא לכל מקרה באשר הוא.


   האם על כל שירות להיות פרויקט עצמאי ב IDE?
כדאי שכן.
מצד אחד נראה ש"לתחזק סביבה פעם אחת" זה פשוט יותר, מצד שני אם אתם נוגעים רק ב 5% מהקוד במערכת, ועליכם לתחזק את הסביבות הנדרשות לקוד זה - בד"כ התחזוקה זו תגזול פחות זמן. למה לכם "להיתקע" עכשיו כמה שעות בגלל migration שעשו ברכיב שלא קשור אליכם ויש בו בעיות - רק בכדי לגרום ל build של הרכיב שלכם לפעול?!


   האם על כל שירות להיות מנוהל ב Git Repository עצמאי?
זה עניין של טעם. למשל: גוגל מנהלים 90% מהשירותים שלהם ב repository יחיד. Git repository לכל שירות - גם הוא סביר בעיני.


   מה עושים עם ספריות שכל השירותים צריכים? למשל Logging או אבטחה?
מה שאתם עושים עם כל ספריית Open Source שאתם משתמשים בה הרבה - הוסיפו אותה כ dependency לכל השירותים שזקוקים לה. זה אמנם לא שירות - אבל לא כל הקוד חייב להתמפות לשירותים, יכולים להיות גם "סתם רכיבים לשימוש חוזר". הרגישו נוח לייצר וריאציה שלכם על דפוס הארכיטקטורה.
אפשר לדמיין מיקרו-שירות שעובר ל production כתא חי: יש את פנים התא שהוא ייחודי, ועוד קצת infrastructure שיש לשלוח עם כל תא ל production בכדי לקיים אותו. וכן - יהיו הרבה instances של השירותים הללו חיים (מה שמקשה על ניטור ו root cause analysis).

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


   מה??? יהיו לי במערכת גרסאות שונות של 3rd Parties? יהיה עלי להתמודד גם עם באגים של SpringFW 3.0.1 וגם עם אלו של SpringFW 4.1.2?
ובכן - היכולת להשתמש בגרסאות שונות של ספריות הוא דווקא יתרון גדול של MSA. כשיש מערכת גדולה ועותק יחיד של הספריה - אנחנו נאלצים לרוב "להיתקע" עם גרסה ישנה לאורך זמן, או לצאת ל"מסע צלב" בכדי לשדרג אותה. הפחד מ"ריבוי באגים" הוא לא תמיד כ"כ ריאלי. בכל מקרה - ברור שיש תקורה בשימוש במספר רב של גרסאות של אותה הספרייה - הייתי ממליץ לנהל "רשימת גרסאות מאושרת" שניתן יהיה לבחור רק ממנה במוצר. לאזן בין גמישות לשליטה.

יש מצבים בהם יש בעיה טכנית להשתמש בגרסאות שונות של אותה הספרייה (למשל: כאשר השירותים הם לא תהליכים שונים של מערכת ההפעלה אלא למשל wars ב JEE application server, או שימוש ב jQuery כאשר UI של שירותים שונים מוצג זה לצד זה על אותו דף של דפדפן. מה עושים? מתפשרים, או מחליפים טכנולוגיה - כמו תמיד.


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

הייתי מסכם זאת כך: RDBMS (בסיסי נתונים רלציוניים) הם בעלי התאמה גבוהה לארכיטקטורת Layers, אך בעלי התאמה פחות טובה - ל MSA. אם אתם עובדים עם MSA (או רק CD) - הבעיות יהיו קטנות יותר אם תעבדו עם בסיסי נתונים שהם schema-less כמו K/V DB או Document DB.
האמת שהיכולות המתקדמות של RDBMS עודדו הפרות משמעותיות של עקרונות ארכיטקטוניים חשובים (הכמסה, שבירת ה Layering שהוגדר, וכו'). איך קיבלנו זאת כל השנים? - זה פשוט עבד.


   איך מנהלים Transactions אם הנתונים שלנו מבוזרים לטבלאות שונות?
פשוט לא עושים. לא מגיעים ל Internet Scale כשעובדים עם ACID. נוקטים מדיניות של Eventually Consistent.


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


   כיצד שירותים מתקשרים זה עם זה? כיצד הם יודעים על קיומו אחד של השני?
בדרך כלל לכל שירות יש URL וניתן להשתמש ב DNS ו Load Balancer בכדי לנהל כמה עותקים - ולמצוא עותק זמין. הכי נפוץ הוא להשתמש בתקשורת REST, הרי - למה להמציא את הגלגל מחדש. אפשר גם על גבי TCP/UDP, אולי RPC או Thrift - הכל ע"פ הצרכים שלכם. בד"כ יהיו לכם גרסאות שונות של ה APIs של השירות, וכמה גרסאות של API שיחיו במקביל - עד שתוכלו באמת להשבית אותם.

דפוס נפוץ בארגון APIs של מיקרו-שירותים הוא ה API Gateway (מסמך: כיצד נטפליקס עושים זאת): שירות נוסף שכל תפקידו לקבל בקשה מה Client (ב URL אחד וב HTTP request אחד) - ולקרוא לכמה שירותים, להרכיב את התוצאות, ולהחזיר הכל באותה הקריאה. שירות זה הוא סוג של Facade שיסתיר מ clients חלק מהשינויים במבנה השירותים (פיצול, שינויי API, וכו')

תקשורת על גבי HTTP היא סינכרונית, ובהחלט אפשר למצוא שירותים שמשתמשים ב Messaging (למשל AMQP) בכדי לתקשר בין השירותים. אפשר לראות פעמים רבות שימוש בדפוס הארכיטקטורה CQRS בהקשר זה כאשר מפעילים שירות שלא משנים את ה persistence בצורה סינכרונית, אך שירותים שיגררו "כתיבות" בעזרת הודעות א-סינכרוניות.

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

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


   האם יש טעם להשתמש ב MSA, אם אנו לא חותרים ל CD?
זו שאלה טובה. אי חתירה ל CD אמור להדליק לכם תמרור אזהרה כאינדיקטור לכך שאולי MSA היא לא מה שאתם מחפשים. אולי כן, ואולי לא.


   האם אין תשתית כזאת, שתפתור לי את כל הבעיות שנובעות מ MSA - ותשאיר אותי עם כל התהילה, והיכולת להתמקד בכתיבת הקוד העסקי?
יש כל-מיני ניסיונות. יש את סנסה ל node.js, את rodent לרובי, או DropWizard של Yammer לעולם ה JVM. משום מה כולם עוסקים בפיתוח של המיקרו-שירותים (החלק הקל) ולא בתפעול שלהם (החלק היותר מורכב).
בכל מקרה... אם אתם קוראים את הבלוג - אתם אמורים לדעת מה דעתי האישית בנושא.


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






Bounded Contexts


רעיון שחוזר על עצמו שוב ושוב בהקשר ל MSA הוא רעיון ה Bounded Context, רעיון של מתודולוגיית ה Domain-Driven Design (בקיצור: DDD) של אריק אוונס. הרעיון מזהה שה Domain Model שלנו (תיאור האובייקטים בעולם והקשרים ביניהם. לא בתוכנה - ב"ביזנס") הוא לא אבסולוטי - אלא תלוי הקשר.

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

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

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

בכל מודל תחום (BC) יש רק את אוסף האובייקטים והתכונות שרלוונטיים לעולם הזה. המתכנתים ואנשי הביזנס מתקשרים על עולם מושגים שהם יכולים להכיל ולשלוט בו. הגבלנו בכוונה את "טווח הראיה" שלהם לטווח ניתן לשליטה.

מה קורה כאשר אותו אובייקט (נאמר הזמנה) קיים באופן שונה במודלים שונים? למשל "הזמנה" היא בעלת תכונות מסוימות ב BC של אנשי המכירות, ובעלת תכונות שונות ב BC של הנהלת חשבונות?

יש לכך כמה פתרונות: אפשר לנהל Shared Context שמכיל את ההזמנה ומשותף לשני העולמות, ניתן לעשות "המרה" של אובייקט מ BC אחד ל BC שני, אפשר שה BC יראה רק view - אבל בבסיס הנתונים (שכבה נמוכה יותר) יהיה מודל שהוא aggregation של כל התכונות מכל המודלים, וכו' וכו'.

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






בדיקות


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

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

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

השימוש ב MSA פותח כל מיני שאלות על Testing שלא היינו צריכים להתמודד איתן קודם לכן. 
טובי קלמסון ניסה לסכם את כל אפשרויות הבדיקה במצגת דיי מקיפה. קשה לי לתמצת את מה שהוא אמר - כי הוא חוזר על הרבה רעיונות מוכרים ומנסה להתאים אותם לסביבה של MSA:
  • נסו להריץ כמה שיותר בדיקות בזיכרון / עם mocks - שירוצו מהר.
  • כתבו יותר בדיקות יחידה ואינטגרציה ופחות בדיקות End-to-End שקשה לתחזק ("פירמידת הבדיקות").
  • בדקו לוגיקה בעזרת בדיקות יחידה, וקוד יותר "משעמם" עם בדיקות אינטגרציה או בדיקות-רכיב (בעיה: יש פרשנויות שונות לכמעט כל סוג בדיקה שנציין).
גישה אחרת (אציין: מקובלת) היא להשקיע פחות בבדיקות אינטגרציה - ויותר בניטור המערכת והיכולת לבצע root cause analysis מהיר. במקום prevention (בדיקות) - להתמקד בטיפול הבעיה ברגע שהיא צצה. יש הגיון כלכלי ברור לגישה זו, אם כי לוקח זמן להגיע לרמה "מספיק טובה" של ניטור והתאוששות בכדי שנסמוך על המערכת הזו להחליף את תפקידם של בדיקות האינטגרציה.

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



סיכום 


סקרנו את הבסיס של דפוס הארכיטקטורה של MSA - דפוס פופולרי במיוחד בשנה האחרונה. התחלנו בשאלות הבוערות - וצללנו אח"כ לפרטים.

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

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

החלטנו לעבור למיקרו שירותים - אבל הייתה הסתייגות רבה מניהול תהליכים נפרדים המתקשרים ב REST, בעיקר בגלל שזה קוד רב "ומשעמם" שיש להוסיף למערכת. המערכת כיום (כמו רוב מערכות ה Layered Architecture) מלאה בקשרים רוחביים בתוך ה Layers שקשה מאוד לנתק. באמת התרנו לעצמנו לקרוא מכל מקום ב Layer לכל פונקציה אחרת ב Layer - מה שגרם לריבוי תלויות. כמובן שגם ברמת ה DTO בעיות רבות נפתרו בעזרת "joins" - מה שקשר את הפונקציות השונות של המערכת גם בשכבה זו. ניתוח תלויות ראשוני נראה קצת מבהיל במחשבה שכל תלות הופכת לקריאת HTTP.

לכן...החלטנו לוותר על התכונה של "isolation by process" של MSA - ויצרנו וריאציה קצת חדשה: כל שירות מקבל פרויקט נפרד (pom.xml במייבן משלו) עם build משלו, בדיקות משלו, וכו' - והוא נארז לתוך קובץ jar. (המערכת כתובה בשפת ג'אווה + UI בשפת ג'אווהסקריפט). את כל השירותים אנו אח"כ אורזים לתוך קובץ war. גדול - ואז עושים deploy.

כיצד אם כן עושים deploy לשירות בודד?
שומרים תמיד את קובץ ה war. האחרון שעבר deploy, מעדכנים רק את קבצי ה jar. של השירות הרלוונטי (הוא עצמו + תלויות ישירות) - ועושים deploy מחדש.

איך מוודאים isolation בין השירותים?
הגדרנו שבכל שירות יהיה package מיוחד בג'אווה בשם facade המתאר את הממשק החיצוני של השירות - ורק לו שירותים אחרים יכולים לקרוא. אנו מתכוונים לאכוף התנהגות זו בעזרת JDepend שירוץ בעת ה build ויפיל אותו בעת חריגות.
בעת build של כל שירות - ה jar. שנוצר מתעדכן ב maven repository של הארגון, ושירותים אחרים שתלויים בו פשוט מגדירים תלות ל jar הזה ומקבלים גרסה עדכנית בעת ה build שלהם.

ל facade יכולים לקרוא שירותים אחרים או שכבה שנמצאת בתוך השירות (בשם webaccess) - במידה והשירות גם זמין על גבי HTTP.

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

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

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



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





-----

לינקים מעניינים

תיאור של חברת SoundCloud על האתגרים במעבר מאפליקציה אחת גדולה ("Monolith") לארכיטקטורה של מיקרו שירותים. מומלץ!
חלק א': http://goo.gl/1ciqbS, חלק ב': http://goo.gl/31Gmy7, חלק ג': http://goo.gl/0aeq3u.

פוסט ידוע, ומצוטט רבות, של ג'יימס לואיס ומרטין פאוולר על מיקרו-שירותים: http://martinfowler.com/articles/microservices.html.
גם שווה קריאה. הם עוסקים בכמה היבטים בהם לא נגעתי בפוסט.

הקלטה של סשן מ Buraco 2012 של Fred George על מיקור-שירותים מיקרו-שירותים: https://www.youtube.com/watch?v=2rKEveL55TY

יש גם מצגת של ג'יימס לואיס בנושא (איך לא?): http://www.infoq.com/presentations/Micro-Services

מצגת מעניינת: http://www.slideshare.net/fuglylogic/microservices-26369481

פרק בפודקאסט "Software Engineering Radio" שעוסק במיקרו-שירותים: http://www.se-radio.net/2014/10/episode-213-james-lewis-on-microservices/




יום שישי, 12 בדצמבר 2014

סדרה: רובי (Ruby) למפתחי ג'אווה ותיקים

לאחרונה אני עובר ל Stack טכנולוגי של: רובי, ריילס, ו AWS (יש גם עניין של מובייל, אבל שם אני יחסית מעודכן).
כחלק המדרך שאני עובר אני כותב בבלוג על הנושאים הללו. באופן דומה למעבר שעשיתי במעבר לג'אווהסקריפט - אני מנסה לכתוב את הדברים מנקודת המבט שאני הייתי זקוק לה: מתכנת וותיק, שמכיר (נאמר) ג'אווה ו Object Oriented היטב - אך "נוחת" בעולם לא מוכר של רובי.

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

אני מרגיש פספוס קטן שרק עכשיו, אחרי כ 12 שנים בתעשיה, אני נחשף לראשונה באמת להבנה מהם רובי וריילס. פתאום אני מזהה נקודות השפעה שונות שלהן שראיתי שהכרתי כבר שנים - אך לא זיהיתי את מקור ההשפעה. נכון: ריילס נמצאת בדעיכה, מתוך כך שהמשקל של ה UI עובר לצד הלקוח, וכדי לכתוב REST APIs ברובי מספיק כנראה לקחת את Sinatra ו Active Records (כך אומרים). לא צריך את כל "המשקל" של ריילס. ההחלשות של ריילס משפיעה גם על רובי, ופתאום יותר מגניב מ"לעשות רובי" זה "לעשות node.js" [א].

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


---





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







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







RVM - כלי לניהול סביבות (וגרסאות רובי)
כאשר אתם מתחילים לכתוב רובי קצת יותר ברצינות - זהו כלי שכדאי להכיר.


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




ריילס, רובי on ריילס
שפת רובי איננה שפת רובי ללא ה framework הכ"כ-מפורסם שלה לפיתוח אפליקציות ווב: Ruby on Rails. בפוסט זה נספק סקירת high level על ריילס.






-----

[א] אפילו מאטצ' (שם החיבה של Matsumoto Yukihiro - ממציא רובי) עובד (במקביל) על שפה חדשה - Streem.



-----

לינקים מעניינים

Ruby Weekly - דרך טובה להתעדכן על החדש ברובי / ריילס.



יום חמישי, 11 בדצמבר 2014

כנס הארכיטקטורה הראשון של IASA ו ILTAM

עדכון (11 בדצמבר, 2014)

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


------


שבוע שעבר, בימים שני ושלישי התקיים "כנס הארכיטקטים הראשון" של IASA ואילתם.

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

ל IASA יש גם קבוצה ב LinkedIn  (דורשת הצטרפות) - בה חברים רבים מהארכיטקטים שאני מכיר. אם אתם ארכיטקטים - אני ממליץ לשקול להצטרף (אני חושב שלא יאשרו לכם להצטרף אם לא כתוב בקורות החיים שלכם שאתם ממש ארכיטקטים באופן רשמי).

את ILTAM, אני אודה - אני לא ממש מכיר לעומק. זו גם קהילה מקצועית, רחבה יותר, המורכבת בעיקר מחברות הייטק גדולות ומבוססות (לא רק חברות תוכנה נטו: אלתא, 3M, וצה"ל למשל - הם חברים) במטרה לקדם ולשתף ידע.

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

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


אני העברתי הרצאה על "חמשת תחומי המומחיות של הארכיטקט" - שבעיקר הייתה מבוססת על הפוסט שפירסמתי באותו השם - כאשר מעט פיתחתי כמה מהרעיונות.

בחלק השני השתתפתי בפאנל מקצועי (עם ד"ר עירית הדר מאונ' חיפה, עצמון הד-טוב VP R&D בפונטיס, וניר סלע - מנהל פיתוח התוכנה ברפאל) שדן במיומנויות הנדרשות בכדי להיות ארכיטקט, או אולי - ארכיטקט "מוצלח". פעם ראשונה שאני משתתף בכזה פאנל - ואני מקווה שהצלחתי לתרום לדיון ולעניין.

מקור

אני אספר בקצרה על שני sessions שהיו לי מעניינים במיוחד:

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

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

רוב התוצאות שאציג להלהן הן מסקר בו נשאלו מאה ומשהו ארכיטקטים על העבודה שלהם: מה הם עושים מול מה שהם חושבים שהם צריכים לעשות. כל התוצאות הן בסקאלה של 1 עד 5 (כלומר: 3.5 ממוצע) - כאשר המספר המדויק לא ברשותי - אני מספק רק "קריאה" שלי מגרף מסוג Bar Chart. למשל:
  • ארכיטקטים הם חלק מצוות הפיתוח (בערך 3.6) בעוד הם חושבים שהם צריכים להיות פחות חלק ממנו (בערך 3.4).
  • ארכיטקטים מובילים את תהליך פיתוח התוכנה (3.1 = נטיה ל"לא"), אבל הם מאמינים שהם צריכים לעשות זאת (3.9)
  • הם גם משתתפים במידה רבה בישיבות תכנון (3.9) - אבל מאמינים שצריכים להיות שם הרבה יותר (בערך 4.35).
  • בגדול ארכיטקטים מאמינים שהם צריכים להיות:
    • שותפים משמעותיים יותר בפיתוח המקצועיות ומתודולוגיות הפיתוח בארגון.
    • שותפים משמעותיים יותר בהגדרת המוצר / חקר השוק.
    • שותפים משמעותיים יותר בגיוס עובדים.
  • מה הם חושבים שהם צריכים לעשות פחות?
    • קידוד: עושים לא-הרבה (3.1) - אבל רוצים לעשות פחות (3)
    • אחריות על באגים / תחזוקת-קוד: עושים מעט (2.6) - אבל רוצים לעשות משמעותית פחות (בערך 2.3).
ניתן לקרוא את התוצאות ולומר: "ברור! כמו כולם הם רוצים להשפיע יותר, לעשות יותר עבודה כיפית - ופחות עבודה משעממת.". אין לי מדד להשוואה למפתחי תוכנה - אבל אני מניח שהיינו רואים מגמות דומות.

בכל זאת, שימו לב שהשאלה לא הייתה "מה הייתם רוצים לעשות", אלא "מה אתם חושבים שאתם צריכים לעשות (should do)". אני מעריך (היפותזה) שמפתחים ותיקים לא היו חושבים שהם צריכים "להנחות את הארכיטקטורה של כל פעילויות התכנון" ("Provide architectural guidelines for all software design activities" - ההדגשה שלי) - עושים: 4.1, חושבים שצריכים: 4.37.

חשבו על הטיעון הלוגי הבא (שרץ כנראה במוחו של כל ארכיטקט תוכנה, בשלב כזה או אחר):
"אם אני ארכיטקט" וגם "למוצר יש ארכיטקטורה" => "אני צריך להגדיר את הארכיטקטורה".

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

אני חושב שהייתי מעדיף שיקראו לארכיטקט Principal Engineer או System Engineer וכו'...

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


Wix - להגיע ל n מליון משתמשים (n = כרגע 50 וקצת)
טוב... רציתי לגעת גם במצגת של יואב מ Wix - אבל גם רציתי לכתוב פוסט קצרצר.
אתם יכלים למצוא ביוטיוב את ההרצאה - בגרסה מעט יותר מוקדמת שלה.

כמה נקודות מעניינות שעלו:
  • אם Wix הייתה "עושה ארכיטקטורה נכונה מהתחלה" - היא לא הייתה שורדת את השנים הראשונות.
  • רכיבים במערכת צריכים להיכתב לתקופה קצובה - ואז להיכתב מחדש (טיעון מקובל בעולם ה Micro-Services). כתבו קוד שיהיה קל-להחלפה.
  • לא זקוקים לבסיס נתונים NoSQL בכדי לעשות NoSQL. עברתי, בזמנו, חוויה דומה - וכתבתי על כך פוסט.
  • כל מפתח מחזיק הרשאות ל production servers - אנו מעסיקים רק את מי שאנו סומכים עליו (אני מתרגם זאת לעצמי: אנו לא סומכים על אף-אחד, בנינו תשתית מתאוששת-עצמית - ולכן אנו יכולים "לסמוך על כולם"). זו גישה דיי מקובלת בעולם ה CD.
  • ההעדפה של Wix ל Managed Data Center (דוגמת Rackspace - אני לא יודע עם מי באמת הם עובדים) על פני AWS - שם כמות המשתנים הבלתי ידועים / נשלטים (למשל: מה החומרה שלי, latency) - היא קטנה יותר.
  • הערה: פרופיל השימוש ב Wix הוא דיי חריג בעולם ה Web (המון המון תוכן סטאטי) - ולכן אני מציע לשקלל עובדה זו בכל רעיונות Scalability שאתם לוקחים מהם.
כמו שאמרתי, Wix היא במיינסטרים של עולם ה Start-up Web ו CD.



סיכום


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

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


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




יום ראשון, 7 בדצמבר 2014

תכנות מונחה-עצמים בשפת רובי

שפת רובי היא שפת Object-Oriented (בקיצור: OO).
כמו כל שפה, היא עושה זאת בדרך משלה, שהיא קצת שונה משפות אחרות. אני כותב את הפוסט הבא לקהל מפתחי ג'אווה שמכיר OO היטב. אינני מנסה להסביר עקרונות של OO ואניח שאתם מכירים, למשל, את ההבדל בין extends ל implements.

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

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




נפתח בהתמצאות בסיסית:

המבנים הבסיסיים של ה OO ברובי הם:
  • מחלקה (class)
  • אובייקט (object)
  • מודול (module) - אפשר לחשוב עליו כ"מודול הרחבה" שניתן "לחבר" אותו למחלקה בכדי להרחיב את היכולות שלה.
  • מחלקה יחידנית (singleton class) - לפעמים נקראת גם metaclass, משמשת להשגת התנהגויות מסויימות שנדבר עליהן בהמשך.
  • מבנה (struct) - סוג של "תחליף זול" ליצירת מחלקות פשוטות בקלות.

ברובי אין interfaces, ואין abstract classes.


בואו נשים לב לעוד כמה תכונות והבדלים בין המבנים שיכולים לעזור בהתמצאות:
  • ניתן לייצר מופעים (instances) ממחלקות וממבנים בלבד.
  • מודול ומחלקה יחידנית הם דומים מאוד למחלקה - אבל יש עליהם כמה מגבלות (הברורה: אין מתודה new ליצירת מופעים).
  • כל המבנים הנ"ל הם בעצם גם אובייקטים לכל דבר: הם instances של מחלקה כלשהי, יש להם self, וכו' - לא שונה כ"כ מג'אווה, למען האמת.

הערה: מכיוון ש"כל המבנים הם אובייקטים", אשתמש במונח "מופע" (instance) בכדי לתאר אובייקט שהוא מופע של מחלקה (class), כלומר: אובייקט "פשוט".




נראות במחלקה


מה העיקרון החשוב ביותר ב OO? - הכמסה!
בואו נראה איך הכמסה עובדת ברובי.

כברירת מחדל ברובי:
  • כל משתני המחלקה (@@) או משתני המופע (@) - הם private
  • כל המתודות - הן public
  • מקרה מיוחד היא המתודה initialize (הבנאי) שהוא private: המתודה new של המחלקה BasicClass (ממנה כל מחלקות הרובי יורשות) היא public והיא קוראת לבנאי של המחלקה שלנו.

יש שוני בין ההגדרות של private ו protected בין ג'אווה (או ++C) לבין רובי.

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

למשל:
class MyClass

  @other_object = MyClass.new

  def foo
    say_it               # specified no-one - okay!
    self.say_it          # specified 'self' - no go.
    @other_object.say_it # specified 'other_object' - no go.
  end

  private

  def say_it
    puts 'yay!'
  end

end

x = MyClass.new
x.say_it # NoMethodError
x.foo # yay!, then NoMethodErrors
המילה השמורה private בגוף המחלקה מגדירה שכל מתודה שהוגדרה מקטע זה ואילך תהיה private (דומה ל ++C). ניתן בהמשך להגדיר מקטע protected ואז public או private בחזרה, וכו'

למתכנת ג'אווה מנוסה, הקוד הבא אמור להעלות שאלה:
"ניתן לראות בקלות ש x.say_it היא קריאה למתודה פרטית. למה ה IDE (נניח RubyMine) צועק על בעיות אחרות - אך לא על זו ?!"
התשובה היא בגלל שניתן לשנות את נראות המתודות בזמן ריצה - ולכן לא ניתן להחליט בבבירור שזוהי שגיאה.

ניתן לקרוא ל "MyClass.protected :foo" בזמן ריצה בכדי לשנות את המתודה (מתוך המחלקה) ל protected.
לא נהוג באמת לשנות את הנראות בזמן ריצה, אבל כן נהוג להשתמש במתודה הנ"ל בכדי להגדיר שמית מתודות ואת הנראות שלהן - במקום לנהל את הנראות במקטעים.

מה המשמעות אם כן של protected?

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



מחלקות ומופעים - כיצד הם מיוצגים?


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



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

מתודות המופע (instance methods)
מתודות המשותפות לכל המחלקות המופעים של המחלקה MyClass. אלו בעצם המתודות הרגילות של המחלקה, כמו המתודה foo בדוגמת הקוד למעלה.


מתודות המחלקה (class methods)
הן מתודות שזמינות להפעלה מתוך reference למחלקה עצמה. הן דומות למתודות סטטיות (static method) בג'אווה.
שימו לב שבתיעוד מקובל לסמן מתודות מופע ב# ומתודות מחלקה ב::
class MyClass
  def self.my_class_method
    puts 'first!'
  end

  def MyClass.second_class_method
    puts 'second!'
  end

  class << self
    def MyClass.third_class_method
      puts 'third!'
    end
  end
end

x = MyClass.new

MyClass.my_class_method # first!
MyClass.second_class_method # second!
MyClass.third_class_method # third!

x.my_class_method # error !@#$!
x.class.my_class_method # first!
הנה דוגמת קוד בה הגדרנו, בשלושה אופנים שונים, מתודות מחלקה.
הדרך השלישית נראית מעט מוזרה. מה שעשינו הוא קודם כל להחליף scope ל scope של ה singleton class של האובייקט MyClass (שהוא גם אובייקט לכל דבר) - ושם הגדרנו את המתודה. זאת מכיוון שמתודות מחלקה מוגדרות בעצם על ה singleton class של אובייקט המחלקה. מבלבל משהו.

מתוך המופע x אינני יכול לקרוא למתודות המחלקה של MyClass (למרות שאני מופע של MyClass), אלא רק בעזרת התייחסות (reference) למחלקה עצמה (למשל: x.class).


משתני מחלקה וקבועים
את משתני המחלקה אנו מכירים מהפוסט הראשון - הם מתחילים ב @@ והם דומים ל static fields בג'אווה. ניתן לגשת אליהם מתוך מתודות של המופע.

נזכיר שגם משתני מופע (@) וגם משתני מחלקה (@@) הם private members. ניתן לגשת אליהם מתוך מתודות של המחלקה / מופע - אך לא מתוך קריאה ל x.@some_variable (מדוע? מכיוון שציינו את מקבל ההודעה, כמובן!)
class MyClass
  @@y = 4
  Z = @@y

  def foo
    puts @@y
    @@y += 1
  end
end

MyClass.new.foo  # first instance -> 4
MyClass.new.foo  # second instance -> 5
# MyClass.new.@@y = syntax error

x = MyClass.new
puts MyClass::Z # 4
puts x::Z # error!
אופיים של קבועים (Z - במקרה שלנו) נקבע ע"י כך ששמם שמתחיל באות גדולה. שימו לב שמחלקות ומודולים - הם (אובייקטים) קבועים בשפת רובי.


מתודות ישירות / יחידניות (singleton method)
בשונה מג'אווה ניתן להגדיר מתודות על מופע בודד - ולא על המחלקה.
class MyClass
  def foo
    puts 'yay!'
  end
end

y = MyClass.new

def y.goo
  puts 'woo'
end

x = MyClass.new

y.goo # woo
puts y.singleton_class.method_defined? :goo # true

puts x.singleton_class.method_defined? :goo # false
x.goo # NoMethodError
כלומר: המתודה goo קיימת רק על המופע y - ולא על שאר המופעים במחלקה.
אם אתם זוכרים את הכללים שלמעלה - מופע (אובייקט) מכיל רק state ולא מתודות. מתודות יושבות על מחלקות או מודולים. כיצד זה מסתדר עם מה הקוד שזה עתה ראינו?

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

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


המחלקה היחידנית של MyClass היא לא חלק מהיררכיית ההורשה של המופע x (היררכית הורשה היא עניין אישי ברובי) - ולכן אם מתודת המחלקה נקראת ישירות מתוך המופע (x.some_class_method) - המופע לא ימצא אותה.

הנה דרך נוספת להגדיר singleton method על המחלקה x
x = MyClass.new
y = MyClass.new

class << x
  def poo
    puts 'ok'
  end
end

puts x.poo # ok
puts y.poo # NoMethodError
הביטוי class << x גורם לנו להכנס ל scope של מהחלקה היחידנית. פשוט.
בכדי שלא יהיה משמעם, קיים הבדל בין שתי דרכי ההגדרה של מתודה על מחלקה יחידנית - הבדל שקשור לנראות של קבועים על המחלקה היחידנית. בצורת ההגדרה הקודמת  הפונקציה לא "תראה" את הקבועים שעל המחלקה היחידנית, ואילו בצורת ההגדרה הנוכחית - הם יהיו זמינים מחלקה. למה? מדוע? - לא חפרתי מספיק בכדי להבין...

הערה אחרונה בנושא: הגדרת נראות private למתודת מחלקה (class method) נעשית בצורה מעט שונה: לא ע"י שימוש ב private שאנו משתמשים עבור מתודות מופע, אלא ע"י שימוש ב private_class_method. שימוש מקובל ב directive הזה הוא להחביא את המתודה new:: בכדי להגדיר דפוס עיצוב של Singleton (של GOF).
ה directives של הנראות (private, public, protected) הן בעצם פונקציות של המחלקה Module המשפיעות על המטא-מודל של האובייקטים ברובי. מכיוון שמתודות מחלקה לא נמצאות באמת על המחלקה (אלא על המחלקה היחידנית של המחלקה) - זקוקים למנגנון מעט שונה בכדי לשנות את הנראות שלהם מבלי לסבך. מבלי לסבך את המפשן של רובי, התכוונתי.


זוכרים שהזכרנו בתחילת הקטע את המטאפורה של המחלקה כ"שק של תכונות"?
בואו נראה התנהגות זו בפעולה:
class MyClass
  def foo
    puts 'aleph'
  end

  def foo
    puts 'beth'
  end
end

x = MyClass.new
x.foo # beth
הגדרנו מתודה בשם foo, ואז הגדרנו אותה שוב.
התוצאה? דריסה של רישום המתודה הראשון ברישום השנה - וזה מה שנשאר.

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

הנה דוגמה נוספת:
class MyClass
  def foo
    puts 'aleph'
  end
end

x = MyClass.new

class MyClass
  def goo
    puts 'beth'
  end
end

y = MyClass.new
x.foo # aleph
x.goo # beth
y.foo # aleph
y.goo # beth
הגדרנו את המחלקה MyClass ואז הגדרנו אותה שוב?
הפעולה הזו נקראת ברובי "re-opening a class". הגדרה נוספת של מחלקה לא "דורסת" את רישום המחלקה הקיים, אלא גורמת לרישום חדש או נוסף של כל ה members לאותו "שק" של מתודות שנקרא MyClass.

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

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

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

האם יש יכולת דומה בשפות אחרות?
בג'אווהסקריפט ניתן לעשות אותו הדבר - שינוי של ה prototype של האובייקט (= בערך מחלקה ברובי) מכל מקום במערכת, וזה נחשבת פרקטיקה לא טובה.
ב #C יש Partial Classes שזה סוג של פיצול מחלקה לכמה קבצים שונים - אבל השימוש הנפוץ הוא שחלק אחד נוצר מ code generation והשני - מתוחזק בצורה ידנית, והם יושבים במבנה הפרוייקט זה לצד זה.

בקיצור:ע"פ כל קריטריטיון שאני מכיר, כדאי להמנע מלהשתמש ביכולת ה"פתיחה מחדש של המחלקה" - ולשמור על יכולת ההתמצאות (orientation) וההבנה הקלה של המערכת. שימוש אחד שיכול להיות סביר להרחבת מופעים והוספת singleton_methods היא unit-testing ו mocking. עדיף קוד שלא דורש זאת - אך יש כמה מצבים שבהם אני מעריך שהייתי שמח להשתמש ביכולות הללו בבדיקות.



מודולים


מודול ברובי הוא מבנה שמקבץ מתודות, שבניגוד למחלקה - אי אפשר לייצר ממנו מופע (instance).
איך משתמשים במתודות הללו? "מחברים" את המודול למחלקה (או כמה מחלקות) - והן יקבלו את הפונקציונליות הנוספת. מודולים הם בעצם, מה שנקרא בשפות אחרות mixin.
module NicePrinter
  def putz(msg)
    puts '[' + msg + ']'
  end
end

class MyClass
  include NicePrinter

  def fooz
    putz 'yay!'
  end
end

MyClass.new.fooz # [yay!]
ניתן לשלב מודול בכמה מחלקות, ולבצע include לכמה מודולים באותה המחלקה (קירוב של "הורשה מרובה").

דרך נוספת לשלב מודול במחלקה הוא בעזרת extends
module NicePrinter
  A = 1
  def putz(msg)
    puts '[' + msg + ']'
  end

  module NiceSpacer
      B = 2
      def add_spaces(str)
        str.scan(/./).join(' ')
      end
  end
end

class MyClass
  include NicePrinter
  extend NicePrinter::NiceSpacer

  def fooz
    putz MyClass.add_spaces('yay!')
  end
end

x = MyClass.new

x.fooz # [y a y !]
puts MyClass::A # 1
puts MyClass.singleton_class::B # 2
puts MyClass::NiceSpacer::B # 2
בדוגמה הזו עשינו include כמו בדוגמה הקודמת, וגם עשינו extend ל מודול המקונן NiceSpacer (ניתן לעשות extends לכל מודול - פשוט רציתי להראות קינון של מודולים "על הדרך").

למה השתמשנו ב" ::" ולא פשוט בנקודה? כי :: הוא ה resolution directive - דרכו מגיעים לקבועי-מחלקה. מודול (מכיוון ששמו מתחיל / חייב להתחיל באות גדולה) הוא קבוע,  - ועל כן זו הדרך הנכונה לגשת אליו. שימוש בנקודה היה זורק Exception.

extend, בניגוד ל include, מוסיף את המתודות שעל המודול להיות class methods. להזכיר: include מוסיף את המתודות שעל המודול להיות instance methods. כנ"ל לגבי קבועים.

שימו לב שהקבוע B נוסף לנו פעמיים:
  • בפעם הראשונה (כרונולוגית) - כחלק מה include: כאשר עשינו include נוספף המודול NicePrinter וכל המודולים המקוננים שלו (במקרה שלנו: NiceSpacer).
  • בפעם השניה פעם על המחלקה היחידנית של MyClass - כאשר השתמשנו ב extends
 ברגע שמוסיפים מודול מקונן - אז גם המודול הפנימי נוסף תחת ה namespace המקונן שלו. בדוגמה למעלה בעצם הוספנו את NiceSpacer פעמיים: פעם כמודול מקונן על המחלקה MyClass, ופעם ישירות על המחלקה היחידנית של MyClass. אני מקווה שקריאת ההסבר פעמיים תספיק בכדי לקלוט את העניין... אם לא - פשוט פתחו irb ונסו קצת בעצמכם.

האם יש למודולים עוד תכונות ויכולות? - כן.

ניתן להגדיר צורות שונות של תלויות בין מודולים כך שחיבור של אחד למחלקה (ע"י include או exclude) בעצם יחבר גם מודולים אחרים, או סתם "יפתח" (re-open) את המחלקה שמוסיפה את המודול ויבצע בה שינויים.

יכולות אלו שימושיות מאוד לבניית DSL - אבל כדאי מאוד להיזהר בהן בכתיבת "תוכנה בסיסית". לכו תבינו שמתודה שלכם מתנהגת אחרת כי מודול שנוסף, גרר מודול אחר - שעושה שינוי ב members של המחלקה...
התחכמות (cleverness) ב Metraprogramming של רובי היא סגולה ללילות לבנים ללא שינה. ראו הוזהרתם!

מודולים משמשים גם כ namespace (כמו ב ++C או #C), ניתן לאגד בתוכם מחלקות, פונקציות, וקבועים - תחת שם שלא יתנגש עם מחלקות, פונקציות, וקבועים אחרים:
module MyModule
  class MyOtherClass
    def goo
      puts 'wow!'
    end
  end
end

class MyClass
  include MyModule
end

x = MyModule::MyOtherClass.new
x.goo # wow!

y = MyClass::MyOtherClass.new
y.goo # wow!
האם ניתן לעשות include ו/או extend גם למודולים כאלו שמכילים מחלקות? - בוודאי. חשבו על Module ומחלקה כ hash ("שק תכונות") שניתן להרכיב אותם (עם כמה מגבלות) אחד על השני.

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



מתודות ו"שליחת הודעות"


בשונה משפות כמו ג'אווה / ++C בהם מתייחסים לx.foo כ "method invocation", ברובי (כמו ב Smalltalk או Objective-C) מתייחסים להפעלת מתודות כשליחת הודעות.

למשל:
class MyClass
  def foo
    puts 'yay!'
  end
end

x = MyClass.new

x.foo # 'yay!'
x.send :foo # 'yay!'
קראנו ל foo ב 2 אופנים:
  • x.foo היא רמת ההפשטה הגבוהה, "כאילו" foo היא תכונה של המופע x.
  • x.send :foo היא האופן בו הדברים קורים בפועל ברובי - שליחת הודעה בשם foo ל x.

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

הנה כמה הבדלים עקרוניים:
  • ניתן לשלוח כל הודעה (בעזרת המתודה send) שראינו למעלה. זהו בעצם syntactic sugar למה שמתרחש באמת.
  • ניתן להענות לכל הודעה, גם ל"הפעלת" מתודה שלא הוגדרה מראש במחלקה - ע"י מימוש המתודה method_missing במחלקה.
  • בעזרת method_missing ניתן לעכב הודעות, להתעלם מהודעות, וכו'.
כאשר קוראים למתודה שולחים הודעה למופע (אובייקט), מחפשים את מתודה בשם שנשלח בהודעה על האובייקט, ואם מתודה כזו לא נמצאה - מתחילים לחפש, בצורה רקרוסיבית, במעלה שרשרת ההורשה של המחלקה עד שמגיעים ל BasicObject (האב הקדמון ביותר).

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

הכלי של method_missing מאפשר כח רב ברובי - להענות למתודות שלא הוגדרו מראש על ידי המחלקה.
מימוש ברירת המחדל של method_missing הוא לזרוק exception, והוא נמצא בתוך module בשם kernel ש"מחובר" למחלקה Object.

האם שתי דרכי הפעולה זהות?
לא. אם תקחו את הדוגמה למעלה ותהפכו את foo למתודה private - תראו שדרך ההפעלה הראשונה (בעזרת נקודה) - נכשלת, אבל הדרך השניה (send) מצליחה. מדוע?

אכיפת ה visibility ברובי נעשית כאשר משתמשים במנגנון הנקודה, ו send פשוט עוקפת את המנגנון הזה. send היא בעצם מתודה public של המחלקה Object (הבת הישירה של BasicObject) - שתפעיל את send_internal של המפרשן של רובי.
כלומר: ע"י send ניתן לקרוא, מכל מקום בקוד, למתודות private של מחלקות אחרות. מה עוצר מבעדנו לכתוב קוד שקורא ל private members של מחלקות אחרות ללא מגבלה? - משמעת עצמית בלבד.






היררכיות ההורשה ברובי


הנושא הזה הוא בעצם הנושא ממנו התחלתיאת הפוסט, כאשר הוספתי עוד ועוד חומר רקע (חשוב בפני עצמו) בכדי שאוכל לדון בנושא בחופשיות.
  • ברובי יש מנגנון של הורשה (inheritance) בין מחלקות - וזו הורשה יחידה.
  • ברובי, כפי שראינו, יש מנגנון של ה Modules שהוא מנגנון של mixins.
  • ראינו שיש גם מחלקות יחידניות (singleton classes) שהן אלמנט טכני, "מאחורי הקלעים", אבל הידיעה אודותיהן עוזר להבין כמה מההתנהגויות של רובי - שאחרת היה קשה להבין.

הנה דוגמה פשוטה של הורשה:
class Person
  def say_my_name
    puts 'john smith'
  end
end

class Employee < Person

end

Employee.new.say_my_name # john smith
אין פה שום דבר מפתיע.

בואו נסבך מעט. האם אתם יכולים להסביר את התוצאה הבאה?
module SongsWeLike
  def say_my_name
    puts "destiny's child"
  end
end

class Person
  include SongsWeLike
end

class Employee < Person

end

Employee.new.say_my_name # destiny's child

puts Employee # Employee
puts Employee.superclass # Person
puts Employee.superclass.superclass # Object
מצד אחד, התוצאה אינטואטיבית. זה מה שהיינו מצפים שתהיה ההתנהגות.
מצד שני, קשה להסביר אותה - בהתבסס על "טיול על עץ ההיררכיה", מכיוון שהמודול הוא לא חלק מההיררכיה - נכון?

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

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

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

הנה האופן שבו נראית ההיררכיה באמת:


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



super


מה קורה כאשר אנו רוצים להרחיב מחלקת-אב, אך עדיין להשתמש בחלק מהפונקציונליות שלה?
בדומה לג'אווה (שיש את this), יש ברובי מילה שמורה בשם super.
class MySuperClass
  def foo(num)
    puts 'super ' + num.to_s
  end
end

class MyClass < MySuperClass
  def foo(n)
    puts 'duper:'
    super
    super n + 1
  end
end

MyClass.new.foo 7 # duper:, super 7, super 8
בניגוד לג'אווה אין חובה ש super תהיה הקריאה הראשונה במתודה, ואין מניעה להשתמש בה רק פעם אחת.
אם לא נציין פרמטרים - רובי באופן אוטומטי תשלח את כל הפרמטרים של הפונקציה שלנו - לפנוקציית ה super. אם מתודת ה super מצפה למספר שונה של פרמטרים - נקבל שגיאת ArgumentError.

הערך של הפרמטרים שיעברו לסופר, הוא הערך הנוכחי שלהם - ולא הערך שהתקבל בהפעלת הפונקציה. כלומר: אם קיבלתי פרמטר, שיניתי אותו, ואז קראתי לסופר - סופר יקבל את ערך הפרמטר לאחר השינוי:
class MySuperClass
  def foo(num)
    puts 'super ' + num.to_s
  end
end

class MyClass < MySuperClass
  def foo(n)
    n = 0
    super
    super n + 1
  end
end

MyClass.new.foo 7 # super 0, super 1
אני מניח שהשיקול התכנוני מאחורי התנהגות זו הוא צמצום הגודל של ה local table (המקבילה ברובי ל Activation Frame של ++C) - תחת ההנחה שמדובר במקרה קצה שלא יבלבל הרבה מפתחי רובי.

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

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

האם סיימנו עם ההפתעות? - חס וחלילה!

ניתן לחשוב בתמימות ש super תמיד קורא למתודה של ה superclass עם אותו השם. זה הגיוני, אבל אם אתם זוכרים ההיררכיה יכולה להכיל מחלקות יחידניות - מה שעלול להוביל להתנהגות לא צפוייה ו/או לא רצויה.
במקום זאת, בקריאה ל super רובי תפעיל את אלגוריתם החיפוש אחר מתודה עם השם של המתודה ממנה היא הופעלה - החל מה superclass של המחלקה הנוכחית ומעלה. כלומר: תעלה לאורך שרשרת ההורשה עד שהיא מוצאת מתודה בשם של המתודה ממנה היא נקראה. אם היא הגיעה ל BasicObject ולא מצאה מתודה בשם הצפוי - היא או תתחיל לחפש (שוב - החל מה superclass שלנו) מתודה בשם method_missing.

מצד אחד - זה הגיוני, מצד שני - עלול להפתיע.

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

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



ריבוי-צורות (polymorphism) ברובי


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

ובכן... כבר ציינו בתחילת הפוסט שבשפת רובי אין מבנים המקבילים ל interface או abstract class בג'אווה. כיצד אם כן ניתן להשיג ריבוי-צורות בלעדיהם??

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

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

הנה ה"דפוס" המקובל ליישום ריבוי-צורות ברובי:
class AbstractHerald
  def announce
    raise NotImplementedError, 'You must implement the announce() method'
  end
end

class OptimisticHerald < AbstractHerald
  def announce
    puts 'life is good!'
  end
end

class PessimisticHerald < AbstractHerald
  def announce
    puts '... life sucks!! :('
  end
end

x = OptimisticHerald.new
puts x.is_a? AbstractHerald # true
puts x.is_a? Hash # false
האכיפה על כך שנממש את המתודה announce - נמצאת ב runtime (מוזר בג'אווה, טיפוסי ברובי).
בעזרת ?is_a - אנו יכולים לבדוק אם מופע שביידנו יורש ממחלקה מסויימת.

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

מה עם ריבוי ממשקים?
נדמיין לרגע מחלקה בג'אווה בשם User שניתן לגשת אליה דרך הממשק UserInfo לפעולו ת של קריאת נתונים, ודרך הממשק UserMaintainance - לפעולות עדכון על פרטי המשתמש (היא מממשת את שני הממשקים). המחלקות השונות במערכת מוגבלות לעשות על User רק את מה שהממשק שבידן מאפשר להן - הגבלה שנאכפת ע"י השפה.

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

כיצד עושים זאת? המתודה ?respond_to בודקת האם מחלקה (וההיררכיה שלה, המודולים שמחוברים אליה, וכו') יודעים לטפל במתודה (ע"פ השם שלה - לא ע"פ מספר הפרמטרים!).
אם השתמשתם ב method_missing יהיה עליכם לדרוס את המתודה ?respond_to_missing ולהצהיר על אלו מתודות אתם מגיבים - בכדי ש ?respond_to תמשיך לספק תשובה נכונה (מזכיר את equals ו hash בג'אווה).
puts x.respond_to? :announce # true
puts x.respond_to? :quit_job # false
הבדיקה מתבצעת ברמת המתודה ולא ברמת המחלקה, מכיוון מתודה למופע ישירות (כפי שראינו קודם) - גם אם שרשרת המחלקות והמודולים שהוא מממש לא מכילות מתודה שכזו.

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

בפועל, סביר שתשתמשו ב ?respond_to בנקודות בהן:
  • הקוד שלכם נכשל בהן בעבר, והחלטתם "לחזק" (hardening) אותן.
    כלומר - הסיבה לתקלה הייתה מורכבת, ולא טעות הקלדה "טפשית".
  • בנקודות שהסיכון לתקלה שכזו הוא בהחלט סביר, בהנחה שאתם מתכנתים זהירים / קפדנים.

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

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



Structs


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

אתם אולי מכירים structs מ ++C או #C - שם "הקטע" שלהם היא רציפות בזכרון / יכולת שמירה על ה stack של ה thread. ברובי אין כאלו דברים - והשיקולים הם שיקולים של מהירות פיתוח ותחזוקה. Struct נמצא על הרצף בין Hash (מבנה הנתונים מטיפוס Dictionary - ללא מתודות או קבועים, או הורשה) למחלקות. הוא קצת יותר מ Hash וקצת פחות ממחלקה.

Struct הוא בעצם generator של מחלקה פשוטה, שכוללת כמה משתני מופע - ו getters / setter למשתנים הללו. היא חוסכת, מצד אחר, הקלדה של מחלקות משעממות, ומצד שני מספקת מבנה מעט יותר יציב / מוגדר-היטב מ Hash. היתרונות העקריים הם:
  • יש הגדרה ברורה לסכימה של ה struct (לאלו פרמטרים מצפים).
  • ניתן לגשת לפרמטרים הללו בצורה יותר אלגנטית: במקום [data[:currency משתמשים ב data.currency.
כאשר משתמשים ב Hash (שזו דרך מהירה מאוד, ושימושית מאוד בהמון מקרים) לתאר מבנה נתונים - אין מקום ידוע בו מוגדרת הסכמה: איזו שדות קיימים? כיצד בדיוק הם נקראים? הסכמה, פעמים רבות, היא צירוף כל המפתחות בהם עשו שימוש איפשהו בקוד.
class MyClass
  MyMessage = Struct.new(:source, :target, :text)

  def foo
    some_condition = false
    MyMessage.new('a', 'b', 'c') unless some_condition
  end

  def goo
    MyMessage.new 'a', 'b'  # text will be nil
  end

end

msg = MyClass.new.foo
puts msg.text, msg.source # c, a

קצת חבל שה IDE בו אני משתמש, RubyMine, לא מספק auto-complete ממשי ל Structs.

אם אתם רוצים להוסיף איזו מתודה או שתיים ל struct - אפשר.
Struct::new יכולה לקבל גם בלוק (של מתודות קבועים) ותמיד אפשר "לפתוח את ה Struct להרחבה". כל עוד זה נעשה בצמוד ליצירה שלו - אני מאשר. :)

הנה אופן השימוש בבלוק:
MyMessage = Struct.new(:source, :target, :text) do
  def encryptText
    self.text = self.text.gsub(/./, '*')
  end
end

msg = MyMessage.new 'me' , 'you', 'a secret'
msg.encryptText
puts msg.text # ********





סיכום


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

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


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





------

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

http://rubymonk.com/learning/books מקור טוב ללמידת רובי לעומק



------

[א] אם אתם רוצים לראות את שרשרת ההיררכיה של מחלקה - פשוט הפעילו עליה את המתודה ancestors.

[ב] מפתחי ג'אווה, ענו לעצמכם בכנות: איזה אחוז משגיאות הקוד המשמעותיות הקומפיילר "תופס"? ואיזה אחוז הוא לא?