2019-10-22

שירותים באנגולר


בפוסט הקודם (״שלום אנגולר 2״), דיברנו בעיקר על רכיבים (Components) באנגולר - ולא כ"כ על שירותים (services).
בפוסט הזה נדבר קצת על שירותים, ומעט על מה שמסביבם.


שירות היא מחלקה, הכוללת לוגיקה שעומדת בחלק או כל התנאים הבאים:
  • היא משותפת לכמה רכיבים - והשירות מאפשר שימוש-חוזר של הקוד.
  • היא כוללת Business Logic (המעט שיש בצד-הלקוח) או Integration Logic (קריאה לצד-השרת) - שני סוגי לוגיקה שאנו לא רוצים להכליל ברכיב.
  • החזקה של State לאורך זמן באפליקציה. רכיבים הם קצרי-חיים (עד ה navigation הבא) ושירותים חיים יותר (עד ה refresh הבא / פתיחה חדשה של האפליקציה).
במבט מהיר וביקורתי, ניתן לטעון ש"דחפו" לשירותים מספר תפקידים שונים שהמשותף להם: שהם לא מתאימים ל Component. באמת יש כמה Patterns וספריות - המחלקים את שלושת האחריויות שלמעלה לסוגים שונים של שירותים / שחקנים חדשים ושונים.

בגרסה הפשוטה, משתמשים בקונבנציות בשמות השירותים על מנת להדגיש תפקידים ספציפיים:
  • שירות שמכיל Business Logic בלבד (כאלו שיכולים וצריכים להיבדק לגמרי ב Unit-Tests) - ייקרא Helper Service או Utility Service. 
  • שירות שתפקידו העיקרי הוא לטעון ולהחזיק בזיכרון נתונים לטווח ארוך מאורך החיים של רכיבים - ייקרא Data Service.
  • רכיב אחר - ייקרא פשוט Service.



שירות פשוט


נעבור עכשיו לראות קצת קוד. נשתמש בשירות מאוד בסיסי שטוען הגדרה של מקצועות (מה שנקרא cob או class of business) מהשרת. השירות מינימליסטי - מסוג של Data Service.

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


  1. בשדה errorStatus נאחסן מחרוזת - אם הייתה שגיאה. תקשורת לשרת, בהגדרה, עלולה להסתיים בתקלות - ואל לנו להתעלם מהמצבים הללו.
  2. הבנאי שלנו מכיל שדה מסוג CobService - זה השירות שלנו..
    1. הגדרה של שדה בתוך חתימת הבנאי היא כתיבה מקוצרת ב TypeScript להגדרה של שדה, קבלת פרמטר בבנאי - והשמת ערך הפרמטר לשדה.
    2. מאיפה מגיע המופע של השירות? מי מפעיל את הבנאי ושולח את מופע השירות?
      אנגולר מרושתת ב Dependency Injection (בקיצור DI) שפועל כאן. כברירת-מחדל, המופע של השירות שאנחנו מקבלים הוא משותף לכל האפליקציה - כלומר Singleton.
  3. המתודה של השירות שבה אנו משתמשים נקראת ()$getCobMap . סימן ה$ בסוף שם של פונקציה או משתנה הוא קונבנציה לכך שהמשתנה הוא מטיפוס / או הפונקציה מחזירה טיפוס של Observable.
    1. לבינתיים נסתפק בידיעה ש Observable הוא כלי יסוד בספריה בשם RxJs (שאנגולר משתמשת בה) הדומה ל Promise, אך הוא יכול להחזיר תשובה כמה פעמים (בניגוד ל Promise שיעשה זאת רק פעם אחת). במקרה שלנו תהיה רק חזרה אחת - מכיוון שיש רק תשובה אחת לקריאת ה HTTP שהשירות מבצע.
  4. בכדי לשלוף נתונים מתוך Observable אנו משתמשים באופרטור subscribe. ה Observable מייצג Stream של ערכים (או תקלות) שיכולים להמשיך ולהגיע עם הזמן. כפרמטר אנו מעבירים אובייקט (להלן: ה Observer) עם כמה פונקציות:
    1. next -פונקציה שתטפל באיבר הבא.
    2. error - פונקציה שתטפל בשגיאה. כאן אנו מציבים ערך בשדה errorStatus. קיום של ערך בשדה מלמד שיש שגיאה בטעינת הנתונים - כפי שנראה מיד.
    3. את subscribe אפשר להפעיל בתחביר מקוצר יותר - שלא השתמשתי בו פה.
    4. הפעלה של Subscribe מחזירה אובייקט Subscription שאותו אנחנו שומרים - בכדי "לנקות".
  5. פונקציית עזר כללית על הרכיב - שמשמשת אותנו בתוך ה Template. 
    1. הייתה לי דוגמה אחרת לפונקציה כללית, אך נתקלתי במקרה קצה מוזר במנגנון ה Change Detection של אנגולר, וכך בעצם הפונקציה הזו גם פותרת בעיה - שקצת מורכב להסביר במסגרת הפוסט. למי שרוצה לצלול ולהעמיק.
  6. בעת הריסת הרכיב (בעיקר: navigation ל"מסך" אחר) עלינו לבצע פעולת Unsubscribe אחרת יהיה לנו memory leak ואירועים ימשיכו להישלח ל Subscription שלנו.
    1. כאשר יש כמה subscriptions באותו הרכיב, אזי מומלץ להשתמש באובייקט מרכזי (כמו subSink) לביטול הרישום - כך שלא נשכח לבצע את הפעולה.
    2. הצורך לזכור ולבטל רישום הוא אכן מטריד ולא רצוי. בהמשך הפוסט נראה כיצד כלי בשם ה AsyncPipe עוזר לנו להימנע מהצורך הזה - ברוב המקרים.
    3. כמובן שאנחנו מהרכיב שלנו מממש את הממשק OnDestory בכדי לבדוק שאין שגיאה בהגדרת חתימת הפונקציה (ואז היא לא תיקרא).


מכאן נמשיך ונציג את ה Template של הרכיב:


  1. אם קיים ערך ב errorStatus - אנחנו נציג אותו למשתמש. יש דרכים אלגנטיות יותר לנהל הודעות שגיאה - אבל זה מספיק טוב לצורך הפוסט.
  2. אנו לא רוצים להציג טבלה ריקה עם עדיין לא הגיעו הנתונים מהשרת (בכדי למנוע flickering) או אם יש מצב שגיאה. כל מבנה ה table לא יתווסף ל DOM על עוד אין לנו נתונים.
  3. את הנתונים עצמם אנו מציגים בטבלה, ולכן משתמשים ב ngFor* בכדי לעשות איטרציה על המפתחות של המפה, ואז מציגים את הערכים.
    1. בניגוד ללולאת for ב ES6 בה ניתן להשתמש גם ב in וגם ב of, ה directive של אנגולר ngFor* תומך רק ב of - מעבר על איטרטור / רשימה.

ניתן לראות את התוצאה של רינדור הרכיב שלנו:

מדהים!



אבל איפה השירות?!


נכון. עדיין לא ראינו את השירות עצמו. הנה הוא:


  1. Injectable@ הוא decorator שמסמן מחלקה המיועדת להזרקה במנגנון ה DI של אנגולר. מכיוון שאין ממש סיבה להזריק אובייקטים שהם לא שירותים (services) - אזי Injectible@ הוא סימן הזיהוי של שירותים בפרויקטי אנגולר.
    1. באפליקציית אנגולר יש היררכיה של Injectors:
      1. ה Injector הראשי נקרא root והוא שייך ל AppModule. הוא האב של כל ה injectors האחרים.
      2. לכל רכיב (component) יש Injector משלו, שהוא האב של ה injectors של רכיבים בנים.
      3. לכל מודול יש Injector שהוא אב לכל ה Injectors לרכיבים במודול - אך בן ל root injector.
    2. אנו רושמים את השירות שלנו ל Injector הראשי. כאשר מנהלים טעינה דינאמית של מודולים - נכון לשקול לרשום שירות למודולים ספציפיים.
  2. בבנאי אנו מבקשים Injection ל HttpClient - שירות סטנדרטי של אנגולר המספק יכולת לבצע קריאות HTTP.
  3. אנו משתמשים ב HttpClient בכדי לבצע קריאת GET. התשובה של קריאות מה HttpClient הן Observable - שהוא ה hook לחזרה של שהתשובה.
    כאשר אנחנו רוצים "לשבת על ה Stream ולהתערב בתוכן" אנו משתמשים בפונקציה pipe של ספריית RxJs. ל pipe אנו מספקים שורה של פונקציות שיפעלו על האיברים שעוברים ב stream. במקרה שלנו:
    1. אופרטור ה map (אופרטור = השם ב RxJs של פונקציה היכולה לפעול על Stream) פועל על כל איבר ב Stream ומבצע המרה שלו.
      1. Object.entities מחזיר רשימת Pairs מה properties של האובייקט. 
      2. אנחנו ממירים את ה Pairs מ [string, any] ל [number, string]
        1. ה Pairs במפה של TypeScript מיוצגים כ tuples (מבנה נתונים ב TypeScript הנראה כמו מערך עם טיפוס מוגדר לכל מקום ברשימה). 
        2. התחביר k+ הוא תחביר מקוצר ומקובל ב TypeScript להמרה של מחרוזת למספר. ההשמה ל string מתבצעת אוטומטית מתוך הגדרת ערך ההחזרה.
    2. אופרטור ה catchError פועל על כל שגיאה (Error) שעוברת ב Stream. לרוב מגדירים אותו בסוף ה pipe בכדי שיתפוס תקלות גם מכל הפונקציות שהוגדרו לפניו.
  4. הפונקציה שנשלחת ל catchError, מקבלת error ומחזירה Observable. יש את האופציה להחזיר Observable חדש שיחזיר ערכים מכל סוג. במקרה שלנו אנו זורקים את השגיאה מחדש, ולכן לא מחזירים Observable. אנו מצהירים על הפונקציה כמחזירה Observable של never - שזו הדרך לומר שלא יכול לחזור שם ערך. קונספט מאוד דומה ל Nothing בשפת קוטלין, למשל.
    1. אנו עושים איזו הבחנה מהירה אם זו תקלת צד-שרת או צד-לקוח וכותבים ללוג.
    2. ...ואז אנו זורקים שגיאה מחדש. throwError הוא אופרטור שיוצר Observable חדש ומיד זורק לתוכו את השגיאה שלי. אתם בוודאי יכולים לשים לב שזרקתי מחרוזת (string) כשגיאה ולא איזה "טיפוס Error גנרי". ב TypeScript אין טיפוס Error גנרי - ומי שמקבל את ה Error צריך להיות מודע לכך (או להסתכן בכשלון בזמן ריצה). האופרטור throwError מקבל any - שהוא הטיפוס הכללי ביותר TypeScript הרחב אפילו יותר מ object.
      ה Error הנפוץ ביותר באפליקציות אנגולר הוא כנראה ה HttpErrorResponse - בשל השימוש הרחב בקריאות HTTP. קל להתרגל למבנה שלו ולהניח שזה תמיד ה Error שיחזור - אבל זה לא תמיד נכון. 



שיפור העבודה הידנית


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

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

אנגולר מספקת מנגנון של "unsubscribe אוטומטי" בעזרת pipe בשם AsyncPipe. מכיוון שאנגולר שולטת במחזור החיים של הרכיב, היא יכולה לבצע unsubscribe אוטומטי כאשר הרכיב נהרס.
לצורך כך יש לשמור על הרכיב (כלומר: המחלקה ב TypeScript) מצביע ל Observable ולבצע את השליפה ("subscribe") בתוך ה Template בעזרת ה AysncPipe. בואו נראה איך זה קורה:


  1. כפי שציינתי, אנו שומרים מצביע ל Observable על הרכיב כשדה. 
    1. כאשר אנו מציינים את הטיפוס של המשתנה (זו לא חובה), TypeScript תדע לדרוש מאיתנו את המקרה בו חוזרת מחרוזת (השגיאה שאנו שולחים) מה Stream - ולא רק מפה.
  2. אנו עדיין רוצים לבצע טיפול מיוחד כאשר מגיעה שגיאה ב stream. הפעם אנחנו לא "יחידת הקצה" (קרי: ה subscriber. ה Template עכשיו הוא יחידת הקצה) - אנחנו רק "יושבים על ה stream" ומתערבים בעזרת אופרטור ה pipe.

והנה ה Template:


  1. ה asyncPipe פועל על Observable (או Promise), עושה לו subscribe ושולף ממנו ערך. כל ערך נוסף שישלח ב stream - יגרום לעדכון של ה markup בדפדפן.
    1. מכיוון שאנחנו רוצים להשתמש בערך שנשלף מה Observable במקום נוסף ב template - אנו שומרים את הערך למשתנה בשם cobs בעזרת המילה השמורה as בשפת ה Template של אנגולר.
      השימוש ב as הופך את הקוד לקריא יותר, וגם יעיל יותר מבחינת ביצועים - כי נמנעים מריבוי subscription לאותו observable.
  2. הנה השימוש הנוסף במשתנה שהצבנו בשלב 1.
  3. keyValuePipe מפרק לנו את המפה (פועל גם על אובייקט) לרשימה של Pairs (אובייקט מסוים), עליהם אנו יכולים לקרוא לשדות key ו value.

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

הגם שהכתיבה של async עדיפה, חשוב להבין מה קורה מאחורי הקלעים - ולכן הלכתי בדרך הארוכה, ופתחתי בעבודה עם subscribe.


ארגון שירותים


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

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

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

הדפוס של Container Presenter הוא מרכזי וחשוב - אבל לפעמים הוא מדגיש את הסרבול במבנה שירותים שטוח.
"מסך" (קרי Container component) מורכב, עשוי להזדקק לנתונים מ 10 או 20 שירותים. אם הנתונים צריכים לעבור טרנספורמציה וחיבור (סוג של "join" אפליקטיבי) - אזי מדובר בלא מעט קוד באותו רכיב Container.

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

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

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

דרך התמודדות אחרת היא לפנות ל Framework לניהול State שמציב פרדיגמה סדורה לגבי ניהול State באפליקצית אנגולר - נושא שהופך למבלבל, ככל שהאפליקציה גדלה ונעשית מורכבת. אפשר לציין את 4 ה Frameworks הבאים, כמקובלים ביותר בשוק:
  •  Akita - פריימוורק פשוט יחסית לניהול State, המושפע מ Domain Modeling של OO (למשל: Model/Entity ו Repository), ויכול לעבוד באותה המידה עם אנגולר, Vue, או React.
  • NgRx - פרימוורק מקיף למדי לניהול state, ומאוד פופולארי בעולם של אנגולר. הוא מבוסס על עקרונות של תכנות פונקציונלי ומושפע מאוד מ Redux של React. לי אישית, הוא מזכיר קצת Enterprise Java בתפקידים הרבים שהוא מגדיר, וכללי-התקשורת המסודרים ביניהם. NgRx מכוון לאפליקציות סופר גדולות ומורכבות. 
    • נראה שרוב המשתמשים ב NgRx הם followers של כותבי-אפליקציות גדולות ומורכבות, ועבור הרוב הגדול של המשתמשים, NgRx הוא פשוט  overkill. לא פעם יוצרי NgRx יוצאים בקריאה "אתם לא צריכים NgRx" - ולא סתם. 
  • Ngxs הוא "חיקוי" NgRx רזה יותר, המשתמש בכלים של TypeScript בכדי ליתר הרבה מה boilerplate code הנדרש ב NgRx (ויש הרבה כזה). Ngxs מגדישה את הדפוס של CQRS.
  • יוצרי NgRx יצאו בעצמם עם הפסטה פשוטה יותר של NgRx/Store בשם NgRx/data, המאפשרת לעבוד עם המנוע של NgRx/Store אך ללא רבים מהסיבוכים וה boilerplate code של NgRx/Store. ע"פ הגדרתם זו "כניסה רכה ל NgRx/Store - עם אופציה לצלול לעומק ברגע שיהיה צורך". ממבט מהיר - נראה שההפשטה שהם יצרו דומה למודל של Akita. יש עוצמה רבה בפשטות.

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

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

הסרה / החלפה של Framework רזה היא לרוב עבודה קלה. עד שנבין ש Framework גדול הוא overkill עבורנו (עקומת הלמידה בד"כ היא תלולה) - ההסרה יכולה להיות קשה מספיק כך שתדחה במשך שנים.



זה נכתב עוד על Flux, תשתית פשוטה בהרבה מ Redux. מקור



מתי בעצם צריך State Management?



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

פייסבוק הציגו את Redux במידה רבה לפתור בעיה של race condition בעדכון נתונים באפליקציה. למשל: Server push מול עדכונים של המשתמש. הגישה הפונקציונלית מתאימה מאוד לשמירת עקביות ותמונה אחידה בנתונים (Immutable state משותף, שזוכה לעדכונים מנקודה יחידה במערכת). לא לכל האפליקציות יש את הבעיה הזו - ולכן לא כולן צריכות לאמץ Redux/NgRx - מכיוון שיש תקורה גבוהה באימוץ המודל הזה [א].

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


בגדול אפשר לסווג את ה State של האפליקציה לאחד משלושה סוגים עיקריים
  • Session State - פרטים על המשתמשים וה Login, פרטי מעקב (sessionId/contextId), ומצב האפליקציה (האם כבר בחרתי xyz או שעדיין לא, ולכן אזור שלם באפליקציה חסום בפני).
  • Entity State - אובייקטים עסקיים שהאפליקציה טענה, בכדי להציג / לעדכן / לשתף בלוגיקה כלשהי.
  • Local State - פרטים מקומיים ל "מסך" או "חלק מסך" (רכיב) מסוים - שאין להם משמעות מעבר לאותו ה"מסך". למשל: מה הבחירה שלי ב Dropdown מסוים. אם אעבור לחלק אחר באפליקציה ואחזור - כנראה שלא אצפה שהבחירה שלי ב dropdown תישמר (אלא תחזור לערך ברירת-המחדל).

דבר ראשון שחשוב לציין הוא ש Local State הוא חשוב ושימושי.
למשל: אני רוצה לשמור משתנה מקומי לרכיב כמו ה errorStatus בדוגמה למעלה, או אינדיקציה שעלי להציג אנימציה "...Loading".

אפשר לשמור את ה state הזה ב store מרכזי, לשתף ולפעפע אותו ברחבי האפליקציה - אבל חבל.
זה לא נחוץ, זו תקורת פיתוח, וזה בהחלט מסבך את האפליקציה.

כל State שיכול להיות Local - עדיף שיהיה כזה. מבחינת שימושיות זה יהיה לרוב יותר מבלבל אם תזכרו עבור המשתמש כל selection על כל תא במסך שעשה. זה טוב לשכוח, מדי פעם.


Session State - אין לכם הרבה ברירה לגביו. אתם צריכים לשמור אותו ולשתף אותו בכל רחבי האפליקציה.
ברוב הפעמים ה session state הוא דיי קטן, ואפשר ליצור SessionService שיתחזק וינהל אותו.


השאלה הגדולה היא בעיקר לגבי ה Entity State שעלול להיות:
  1. גדול - מחלקות (classes) רבות, ו/או מופעים (objects) רבים. 
    • כאשר יש יותר אובייקטים ממה שסביר לשמור בו-זמנית בזיכרון האפליקציה - מבחינת צריכת-זיכרון. 
  2. משותף לכלל האפליקציה. אם מסך אחד אומר שללקוח יש יתרה של $400, ומסך $350 - זה באג. לא מעניינת אותי מדיניות ה caches ופעפוע הנתונים.
  3. להתעדכן בצורה תכופה. הנתונים בשרת משתנים בטווח של דקות או אולי המשתמש מבצע עדכונים רבים - כי זה אופן השימוש באפליקציה.

ככל ששלושת הצירים הללו מתקדמים יותר - כך סביר יותר שנזדקק לפתרון מורכב יותר מקבוצה של שירותים ו/או FacadeService / BehaviorSubjects.

אני מניח גם שאפשר לומר, באופן כללי, שציר 3 (עדכונים תכופים) גורר מורכבות גבוהה יתר מציר 2 (שיתוף רחב), שגורר מורכבות גבוהה יותר מציר 1 בלבד (גודל / כמות אובייקטים).

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

הסכנות שיש לשים לב אליהן הן בעיקר:
  • האם אפשר להגיע למצב של אי-עקביות של הנתונים? מה הסיכוי שנחזיק את אותם הנתונים כפול במערכת - והם לא יהיו זהים?
    • לעתים, רק גודל של האפליקציה יכול לגרום למצב הזה, כי יד ימין #6 לא יכולה בדיוק לדעת מה יד שמאל #24 - עושה. למשל: נוצרים שני שירותים במערכת שמביאים מהשרת ומנהלים - את אותם הנתונים.
  • מה מפת הקשרים בין החלקים באפליקציה שלנו? 
    • הרבה קשרים בין רכיבים לשירותים - זה לרוב בסדר, ומקסימום Facade Services יכולים לצמצם את כמות הקשרים. 
    • הרבה קשרים בין שירותים הם גם בסדר - אם יש להם היגיון ברור וסדור לאורך האפליקציה (למשל: ל FacadeServices יש הרבה קשרים לשירותים בסיסיים יותר).
    • כאשר הקשרים בין השירותים הופכים להיות דו-כיוניים / צורך במעגלים - זה המקום לחשוב על ניהול מורכב יותר של State.
  • Race conditions בעדכון הנתונים - כאשר התופעה רחבה, קשה מאוד לניהול בקשרים פשוטים בין שירותים.
    • גם: אפליקציות מובייל כאשר ייתכן latency גבוה / לא עקבי בתגובה לקריאות HTTP ו/או ניתוקים אפשריים.

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

שתי תכונות שנוטים לייחס לפתרונות לניהול State בצורה מוגזמת, וכנראה לא נכונה הן Testability טוב יותר (כנראה ההיפך הוא הנכון) וביצועים טובים יותר (Caches בהחלט עוזרים - אבל אפשר להשתמש ב caches גם ללא פתרון ניהול state).


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



סיכום



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

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

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



----


קישורים שימושיים:

השוואה בין Akita, NgRx, ו Ngxs - פרימוורקים לניהול State באנגולר (או בכלל).

-----

[א] You Might Not Need Redux - פוסט מהיוצר של רידאקס.


2019-10-05

TCR, או: מתי TDD כבר יוכרז כמיושן?


TCR היא טכניקת תכנות חדשה מבית היוצר של Kent Beck.

אם זה לא הרשים אתכם עד כה, אני אזכיר שקנט בק הוא הבחור שהיה שותף להמצאה של ה Wiki והבאת CRC Cards (שיטת Design) לקהל הרחב. הוא גם המציא את (Extreme Programming (XP וגרם למהפכה בעולם התוכנה. ביחד עם Erich Gamma (מה GOF) הוא כתב את JUnit ועוד גרסאות בשפות שונות. לבסוף הוא המציא את TDD - שגם השאירה חותם גדול על התעשייה.

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

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

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




רקע


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

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

המצב של גדילה כ"כ גדולה (יותר מ 50% בשנה) נקרא Hyper-growth או Blitzscaling, ואין לו פתרון פשוט.

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

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


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

התנגשות (merge או גרוע הרבה יותר: build שבור) - הוא נזק סביבתי חמור. כאשר ה build שבור:
א. מספר לא חסום של מפתחים - חסום לעבוד לפרק זמן.
ב. ייתכן וחלק מהמפתחים שכפלו ל branch שלהם את התקלה ואז:
     ב1 - היא עלולה לגזול להם זמן נוסף באיתור ותיקון התקלה בעצמם.
     ב 2 - היא יכולה, בצורה מסוימת, לחזור ל master מחדש כחלק מתהליך ה merge חזרה.

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


תרשים של קנט בק על ההבדלים בין TDD ל TCR


ל TDD - המתודולוגיה הקודמת של קנט, שעוזרת לשמור על קוד בשליטה וניתן-לשינוי בכל רגע, יש כשל עמוק כאשר מדברים על Scale: מתודולוגית ה TDD לא מחייבת אנשים ל commits קטנים. מתכנת יכול לכתוב 20 בדיקות, ואז לעבוד עוד יום או יומיים או שלושה עד שכולם יהיו ירוקים. הוא כבר לא יעמוד בקריטריון הרשמי של Continuous Integration [א] - אבל לכאורה הוא עדיין עושה TDD.

קנט בא לתקן את מתודולגיית ה TDD לעבוד ב Hyperscale וקרא לה Test && Commit:
הרזולוציה המחויבת מעכשיו היא של בדיקה בודדת:
  • בצע שינוי קטן של קוד.
  • כתוב בדיקה שמאמתת את השינוי.
  • הרץ את הבדיקה, ואם היא עברה - עשה merge ל master.

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

השם של השיטה מתוך הסקריפט הבא, שאמור לרוץ בסוף כל cycle קצר שכזה:

test && git commit -am working

הרעיון של "Living Integration" במקום "Continuous Integration" (שאולי עכשיו נכון יותר לקרוא לו Daily integration - הוא אטי הרבה יותר מדי מ"הנדרש") - נקרא ע"י קנט והצוות שלו בשם "Limbo", המצב האינסופי של העולם הבא.

יש קונצנזוס רחב בתעשייה, כבר כשני-עשורים לפחות, שעבודה ב batches קטנים ככל האפשר - היא דרך ליעל תהליכי עבודה ולהימנע מהתברברויות יקרות. הלימבו אמור להיות המצב התאורטי הגבוה ביותר של batches הקטנים ביותר והאינטגרציה הרציפה ביותר האפשרית. רעיונות שעלו בצוות היו "בואו נכתוב סקריפט שעושה push ל master כל פעם שקובץ נשמר" או "בואו נאסור על commit לכלול יותר מ 25 שורות שהשתנו" (שני רעיונות קיצוניים למדי, ותאורטיים - לפחות כיום).

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

קנט לא אהב את הרעיון, ולכן לפי דבריו "הוא היה חייב ללכת לנסות אותו!". בעצם נראה שקנט אהב את הרעיון - ומשם המתודולוגיה הפכה ל: (Test && (Commit || Revert - מכאן שמה הוא TCR.

מעכשיו המתודולוגיה עובדת כך:
  1. בצע שינוי קטן של קוד.
  2. כתוב בדיקה שמאמתת את השינוי.
  3. הבדיקות עברו? - בצע commit (ושלח את הקוד ל master)
    הבדיקות נכשלו? בצע revert --hard לקוד - והתחל מהתחלה.

כל הכנסה של קוד צריכה לעבור את כל הבדיקות, יש ב TCR איזו הנחה סמויה שהמערכת מכוסה לגמרי בבדיקות-יחידה, שיכולות לרוץ בדקות בודדות - לכל היותר.


הרעיון של TCR הוא די קיצוני: למחוק את הקוד אם לא הצלחנו בפעם הראשונה?

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


סקריפט שימושי להריץ בזמן שאתם עובדים על ה master branch. כולם צריכים לעבוד על ה master branch - כמובן!


שימוש מעשי



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

בספר "Extreme Programming Explained" קנט בק הסביר שב XP הוא עורך בעצם ניסוי. הוא לוקח כל מיני פרקטיקות טובות שכרגע נמצאות על רמה 2 או 3 - לרמה 10, לראות מה קורה.

"אם Code Review הוא טוב - עשו Code Review כל הזמן!" - כך נוצרה הפרקטיקה של Pair Programming, למשל - כחלק מ XP.

XP לא ממש הצליחה כשיטה, אבל הפרקטיקות שלה, שהחזירו אותן לרמה 3 או 4 - עבדו מצוין, והן מוטמעות היום עמוק בתעשייה. 

גם TDD - לא ממש התממש לפי החזון. אומרים שרוב מי שעושה TDD - לא באמת מבין את המתודולוגיה, וגם מי שמבין אותה - לא עושה אותה "By the Book".

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

ברור שהרעיונות של TCR הם אבסורדים ולא מעשיים! זהו קנט בק, וטוב שיש מישהו כמוהו - שמעלה אותם.

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

בואו נראה כמה יישומים מעשיים שמדוברים עכשיו:


לימוד / אימון


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

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

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



LDR הוא סוג של TCR. בחיי.



"30 דקות ל commit"

את הגרסה הזו אני ניסיתי בחודשים האחרונים:
  1. התחל לעבוד על קוד. 
    1. חתור למצב בו יש מצב יציב שעובר commit מהר ככלל האפשר: 5 עד 10 דקות.
    2. עדיף מאוד עם בדיקות - אבל לא כל commit שלי באמת נבדק בטסט חדש.
  2. אם הגעת ל 30 דקות בלי commit - בצע revert לקוד והתחל מחדש.

בגרסה הזו מתמקדים ב commit מהיר כל 5-10 דקות (מקסימום: 30 דקות) - ולא ב merge מהיר ל master, מה שעוזר "לדלג" מעל קושי גדול ביישום ריאלי של TCR. ה merge עדיין מכיל מספר commits, ועובר תהליך qualification ארוך ומורכב (יחסית לדרישות של TCR).

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

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

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

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


It’s 2019. Blocking, asynchronous code reviews are the dominant method for collaboratively developing software. If I draw one certain conclusion from my experiments with TCR and Limbo, it’s that blocking, asynchronous code reviews are not the only effective workflow for collaboration. While I don’t know if TCR and/or Limbo will be the future, I think that something different  is coming -- Kent Beck


TCR /w Async Merge

תהליכי ה Integration בחברות לא מאפשרים כיום לעשות merge כל כמה דקות. מי שאינו קנט בק חי בדרך כלל עם Suite של בדיקות מסוגים שונים שלוקח להם לפחות 10 דקות לרוץ, ולעתים גם כחצי שעה או שעה (זה הקצה הקצת פחות טוב...).
ויש גם את ה Code Review שיכול להסתיים רק אחרי כמה שעות. גם היסטוריה ב Git תהיה בלאגן אם כל מפתח יכניס commit כל 10 דקות. המערכות שלנו לא מוכנות כרגע באמת ל commit ל master כל 10 דקות, ע"י מאסות של מפתחים.

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

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

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

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


הנה קישור למאמר של עוד כמה וריאציות שבחור בשם תומאס מציע: BTCR, Relaxed TCR וכאלו.
אני חושב שהמסקנה מהניסיונות הללו היא ש:
  • יש משהו עוצמתי ב TCR שגורם לאנשים להמשיך ולנסות אותו. אני לא זוכר כזו דבקות מסביב ל Implementation Patterns.
  • הדרכים העיקריות ליישום שימושי של TCR עדיין לא נמצאו.
אני אישית חושב ש "30 דקות ל commit" היא האופציה המעשית ביותר. לי היא עובדת כשינוי גישה מרענן.


סיכום



  • מה אתם חושבים על TCR?
  • האם ניסיתם? האם היו תוצאות טובות?


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


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




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


---

[א] ע"פ ההגדרה: כל מפתח עושה merge ל trunk/master כל יום.



2019-10-01

שלום, אנגולר 2 (או 8+)


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


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

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


למה דווקא עכשיו?

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

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

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



פתרון מקיף - כבר אמרנו?




מהי אפליקציית אנגולר?


אפליקציית אנגולר היא סט של Components ו Services מאורגנים לתוך מודולים של אנגולר:


  • Components הם רכיבים ויזואליים, שרואים על המסך.
  • Services הם שירותים לוגיים המשרתים את ה Components. שירותים בסיסיים המשותפים לכל האפליקציה - יושיבם במודול הראשי שנקרא ה "App Module".

שימו לב שיש הבדל בין Angular Module לבין ES6 Module. בדומה ל ES6 modules גם Angular Modules מאגדים קוד בעל מאפיינים דומים - אך יש להם סמנטיקות נוספות. לאורך הפוסט כאשר אתייחס ל"מודולים" הכוונה היא ל Modules של אנגולר.

ה Components מאורגנים בהיררכיה, וכל אחד הוא View ("מסך") או חלק מהמסך:


בתוך ה App Module, נמצא ה component הראשי, שנקרא ה Root Component.

מכיוון שאנגולר מרכיב לרוב Single Page Applications (קרי SPA), ה Root Component יהיה לרוב המסגרת לדף הסטנדרטי, וכל "תוכן אפשרי של מסך" יהיה Component נוסף - שגם הוא יורכב מ Sub-Components נוספים.
  • הפרקטיקה היא ש Components מכילים כמות מוגבלת של קוד. אם Component הוא גדול ומורכב - הרי שיש לפרק אותו לכמה Components (בדומה ל microservices)
  • Components הם reusable, ורבים מהם יופיעו שוב ושוב - במסכים שונים של האפליקציה. 
    • אפשר לחשוב על Component כתגית HTML חדשה עבור האפליקציה, למשל <customers-table> שאותה ניתן לשלב בכמה מסכים שונים. ייתכן וארצה ליצור רכיב בשם <customers-table-and-details> שמציג את הטבלה, וכמה שדות עם פרטים נוספים עבור הלקוח שנבחר. גם רכיב כזה יכול להיות שימושי בכמה מסכים שונים.

דוגמה פשוטה לחלוקת מסך ל Components. מתוך מדריך של Angual Augury.

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


מודולים

את כלל הקוד באפליקציה מחלקים למודולים, בד"כ ע"פ הקריטריונים הבאים:
  • מסך מורכב או קבוצת מסכים המטפלים בנושא מסוים - הם מודול. לרוב נקרא: Feature Module. למשל: מסכי הגדרת לקוח - הם בסיס איתן למודול עצמאי.
  • פיצ'ר לוגי במערכת - יכול להיות מודול. למשל Data Export, לא כולל הרבה UI (אולי רכיב בודד) אך כולל הרבה לוגיקה או עבודה עם מערכות חיצוניות - מצדיק מודול. ייתכן וניתן לעשות Data Export לדברים שונים ממסכים שונים במערכת. זה גם Feature Module - אך מסוג מעט אחר.
  • Shared Modules הכוללים רכיבים ו/או שירותים שזקוקים להם בכמה חלקים של האפליקציה. לעתים Shared Modules הם משותפים בין כמה אפליקציות.
    • הרבה פעמים מקובל ליצור באפליקציה מודול משותף בשם core - על מנת שה AppModule יישאר קטן ככל האפשר. ה AppModule מייבא אותו ומחצין (export) אותו לכל שאר המודולים באפליקציה.

כל מודול יכול להשתמש ברכיבים ו Services שלו, באלו של ה AppModule (הציבוריים), ובאלו (הציבוריים) של מודול שהוא מייבא (import).

מודולים יכולים להיטען בצורה דינמית, וזה שיקול נוסף בחלוקה למודולים: מודולים הם יחידת הטעינה הדינאמית של אנגולר. למשל: אם יש לי אפליקציה עם 100 "מסכים", אף בשימוש טיפוסי משתמש ייגש רק לכ 10 "מסכים" - חבל מאוד לטעון את כל הקוד בהתחלה. כאשר יש אפליקציה SPA גדולות (שזה בעצם ה use-case העיקרי של Angular) - טעינה נבונה של מודולים עשויה לשפר מאוד את הביצועים.



TypeScript או ג'אווהסקריפט?


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

TypeScript היא שפה מודרנית ומתוכננת-היטב, בניגוד גמור ל JavaScript המקורית. 
  • שמה של השפה נובע ממערכת ה Types שלה, שמקל לבדוק את נכונות הקוד - אבל מאפשר גם ל IDEs לעבוד בצורה עמוקה יותר עם הקוד: מציאת שגיאות אפשריות, Refactoring ו Navigation ברמה ששפה דינמית וללא טיפוסים כמו JavaScript - לא מאפשרת.
  • TypeScript היא SuperSet של שפת ג'אווהסקריפט, כלומר: ג'אווהסקריפט היא מקרה פרטי של TypeScript (בערך). לדוגמה: אם ניקח קובץ js. ונשה את שמו ל ts. לרוב נצטרך להוסיף הגדרה של טיפוסים - אך משם הוא יעבוד.
  • הדפדפן לא מכיר TypeScript, ולכן הקוד מתורגם (Transpiled) לשפת ג'אווהסקריפט.
    • אם תראו את קוד הג'אווהסקריפט שנוצר מ Typescript - הוא יהיה דומה מאוד לקוד ה Typescript שכתבתם.
  • TypeScript מכילה את רוב היכולות של ES6 - אך לא את כולן. למשל: אין תמיכה ב Proxy (כלי דיי מתקדם, ולא לשימוש יומיומי). עם הזמן TypeScript מוסיפה עוד אלמנטים מ ES6, וגם אלמנטים ייחודיים משלה.


TypeScript, כפי שאמרנו, היא יישום של ES6 עם כמה תוספות, בעיקר:
  • Static Typing - למשתנים יש טיפוסים, והקומפיילר (ליתר דיוק: טרנספיילר) בודק את נכונות השימוש בהם עוד לפני שהקוד רץ.
  • Interfaces שלא קיימים בג'אווהסקריפט - הם תוספת משמעותית. למי שעבד בג'אווה - אין צורך להסביר את החשיבות.
  • Class Properties - קיימים ב ES6, אך בצורה לא מפורשת. ב TypeScript ניתן להגדיר properties על מחלקה בצורה מפורשת (או בתחביר מקוצר בתוך ה constructor) - מה שמאוד נוח ושימושי.
  • נראות - members במחלקה של TypeScript יכולים להיות גם private. ב ES6 נראות היא בעיקר ברמת המודולים.

לשימוש ב TypeScript יש כמה מחירים [א] (בעיקר: צורך בתהליך build, ניהול הטיפוסים עשוי להיות overhead מורגש) - אך סה"כ, מדובר בשפה מוצלחת למדי: גם לדעת "מומחים", וגם ע"פ מידת האימוץ שלה בתעשייה.



מה ההבדל בין Angular ל AngularJS?


האם זה לא אותו הדבר? (תשובה: לא.)
  • Angular החל כפרויקט פנימי בגוגל ב 2009, וב 2012 שוחרר לכלל התעשייה כפרויקט קוד פתוח.
  • ב 2014 הצוות של Angular החליט לבצע שינויים דרמטיים לספריה, ובעצם החל לכתוב אותה מחדש, תהליך שיארוך כשנתיים. היו הרבה דרמות וויכוחים מסביב למהלך, אך כיום אפשר לומר ש AngularJS היא Legacy - ובעצם רק Angular היא רלוונטית בראיה קדימה. 
  • שם הספרייה החדשה היה "AngularJS 2.0" ובעצם מאז שמטו את ה JS מהשם (חשבו: TypeScript) והגרסה התקדמה. מאז השם הוא פשוט "Angular", למשל Angular 8.

בעוד AngularJS בנוי מסביב למבנה MVC (קרי: Model-View-Controller), מה שהיה מקובל באותם הימים, Angular היא ספריה שבנויה מסביב ל Component - ממש כמו ReactJS או Vue - וזה המודל הפופולארי כיום.

אפשר לציין ספריה בשם Aurelia שהיא, קונספטואלית, ממשיכת דרכה של AngularJS ומודל ה *MV. חלק ממאוכזבי אי-המשך הדרך של AngularJS - פנו לעבוד בה. עדיין, נתח השוק שלה קטן משמעותית משלושת הספריות המרכזיות (React, Vue, Angular).

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

יש הרבה דמיון בין הגרסאות שיכול לבלבל אותנו ולגרום לנו לחשוב שמה שאנו קוראים רלוונטי לאנגולר (החדש). למשל: ב AngularJS יש directive נפוץ בשם ng-if, אך ב Angular שינו לו את השם ל ngIf, ויש להשתמש ב directives עם סימן *. חבל לבזבז זמן ולדבג קוד של AngularJS בתוך אפליקציית Angular...



רכיבים (Components)


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

בואו נצלול ונראה כיצד רכיב כזה בנוי:





  • נתחיל ב Metadata, הנכתב כ decorator [ב] על המחלקה. זהו בעצם המידע שהופך מחלקת TypeScript רגילה - ל Component, ומחבר עליה את כל החלקים.
    • selector - ה HTML Tag שמייצג את הרכיב. כל הוספה שלו ל Markup - יוסיף את הרכיב (יש לדאוג ל imports מתאימים).
    • template (אלמנט שני חשוב ברכיב) - ה HTML Markup של הרכיב, הוא חלק משמעותי ברכיב. בד"כ ינוהל כקובץ נפרד, אם כי גם ניתן לכתוב אותו כ inline בתוך ה template.
      • ה Template הוא לא ה HTML הסופי שיוצג, והוא עשוי להכיל directives (אלמנט נוסף של אנגולר) ורכיבים אחרים (ע״י ציון ה Selector שלהם) המשפיעים על ה Markup הסופי. למשל ngIf directive יסיר את האלמנט שעליו הוא יושב, אם הביטוי המשויך אליו אינו חיובי.
        ייתכן ובמידה והחתול של שרדינגר יריץ את הקוד - הכותרת ״Hello World!״ לא תוצג.

    • style - ה S)CSS) של הרכיב. אפשר גם לכתוב inline, כאן אנו רואים קישור לקובץ.
      • הסוגריים המורבעים הם מכיוון שזה מערך. ניתן להחיל על הרכיב כמה קבצי Style (אמצעי ל reuse/שיתוף של CSS).
      • מי שמכיר את SMACSS - ה guidelines לכתיבת CSS, בוודאי ינטה לתת namespaces ל styles של הרכיב, בכדי שלא ישפיעו בטעות על רכיבים אחרים. אל דאגה! Angular עושה זאת בצורה אוטומטית עבורנו - ואפשר לשמור על ה markup מינימלי יותר.


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


Lifecycle Events

איזה סוג של קוד כותבים במחלקה של רכיב? כבר אמרנו: Presentation Logic, בעיקר.

מה מניע את הקוד הזה? מאיפה הוא מתחיל לרוץ? - מאירועים בחיי הרכיב, או מ Events (נדבר על Data-Binding בהמשך). הנה אותו הקוד עם מימוש של כמה מתודות נפוצות של ה Lifecycle של הרכיב:


  • הוספתי את ה Lifecycle events הנפוצים ביותר. את הרשימה המלאה של Lifecycle Hooks של רכיב אנגולר ניתן למצוא כאן.
  • שימו לב שלכל אירוע. המחלקה מממשת מנשק מתאים (OnInit וכו'). מכיוון שלממשקים אין ייצוג ב ES6 (הם קיימים רק ב TypeScript) אנגולר לא באמת מסתמכת עליהם בזמן ריצה. הסיבה להוסיף אותם היא לכדי לוודא שאין שגיאת כתיב בשם הפונקציה. הקומפיילר לא יתלונן אם אגדיר פונקציה בשם ngOnDestory - אבל היא לעולם לא תרוץ, ומציאת הבעיה עשויה לארוך זמן.
  • כמה דגשים ספציפיים לגבי האירועים:
    • ההבחנה החשובה הראשונה היא בין הבנאי ו ngOnInit. יש להרחיק מהבנאי פעולות ארכות / שעשויות להשתבש (כמו הבאת נתונים מהשרת) - ולשים אותם ב OnInit. שימוש-יתר בבנאי היא נפוצה בקרב מפתחים החדשים לאנגולר.
    • כשנעסוק בנושא ה Data Binding, נראה היכן ngOnChanges מקבלת תפקיד מרכזי.
    • כדי לקבל הודעות מה services של אנגולר - אנו צריכים להירשם (subscribe) אליהם. זה קוד שלרוב יקרה ב ngOnInit. כדי להימנע מ memory leak עלינו לעשות Unsubscribe ב ngOnDestory - פעולה חשובה שפעמים רבות שוכחים ממנה!
      • הדבר נכון אפילו יותר לשימוש ב NGRX (מימוש דפוס ה "Redux" באנגולר) ו RxJS (ספריה פופולרית בשימוש באנגולר לתכנות ראקטיבי) - שם רישום לאירועים הוא דרך העבודה הסטנדרטית.


רכיבים מקוננים


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

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

ורוד: התפקיד של כל קובץ ברכיב, סגול: הטווח של כל רכיב, תכלת: הטווח של כל מודול.

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

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

 $ ng generate component MyComponent

גישת ה generation (או Scaffolding, כפי ש Ruby On Rails הגדירה אותה) מונעת בעיות רבות של copy-paste ועוזרת לשמור על מבנה אחיד בפרויקט. עשו מאמץ קל - והתרגלו לעבוד ב cli להוספת רכיבים חדשים לפרויקט.

בואו נראה קצת קוד, של App Component המכיל בתוכו רכיב פשוט.

הנה הקוד של ה AppComponent:


הוא מכיל בתוכו תגית שמתאימה ל selector של MyComponent:


יכולנו, באותה המידה, לכלול את MyComponent מספר פעמים.

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


ה AppComponent לרוב יכיל רק Layout עם רכיבים אחדים: רכיבי מסגרת (header, footer, side-panel, וכו') ורכיב אחד ל"מסך"/תוכן - שישתנה תוך כדי עבודה באפליקציה (נושא של Routing - שלא אכסה בפוסט).

גם בתוך הרכיבים המתארים "מסכים", נוהגים לבנות את הרכיב העליון כרכיב רזה מבחינת UI, המכיל מספר רכיבים שמשמשים לפרזנטציה. דפוס מקובל באנגולר הוא שהרכיב העוטף (להלן: Container Component) מכיל Layout ואת הגישה לנתונים - והוא מורכב מרכיבים קטנים הנקראים Presentation Components - שמייצרים את עיקר ה Markup. רכיב ה Container אחראי להביא את הנתונים (דרך ה services, מצד השרת) - ומעביר אותם, ע"פ הצורף, לרכיבי ה Presentation - שרק עוסקים בתצוגה.

היתרונות שב Container Components:
  • גישה לנתונים נעשית במקום אחד - ברכיב ה Container. קל מצוא את הקוד (LIFT). ייתכן וכך גם ניתן לגשת שרת המרוחק - פחות פעמים.
  • רכיבי ה Presentation פשוטים ורק עסוקים בהצגת ה State (להלן Presentation Logic).
    • קל לכתוב להם בדיקות-יחידה (מה שנקרא גם Component-Test: לבדוק את ה Markup שנוצר). אין צורך ב Mocks כי כל ה Integration Logic נמצא ברכיב ה Container. 
יש חיסרון בגישה הזו, בכך שרכיבי ה Presentation הם לא עצמאיים ולא יכולים לרוץ בלי Container. למשל: הם לא Routable - כלומר: אי אפשר לשמור Bookmark למצבים ספציפיים שהם מייצגים, בלי עבודת נוספת ברמת ה routing. לא עניין גדול - לפי דעתי.



Data Binding


נושא אחרון שארצה לסקור, בזריזות, הוא נושא ה Data-Binding, היכולת לקשר בין תכונות (properties) של המחלקה ל Template בצורה ״אוטומטית״.

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


באנגולר, יש ארבע צורות של Data-Binding:




נסקור אותן (בקצרה) בזו אחר זו.



  • Interpolation היא הצורה הבסיסית ביותר: אנו כותבים ביטוי בתוך סוגריים מסולסלים כפולים (מה שנקרא Mustache Style) בתוך ה Template Markup, בזמן ריצה הביטוי מחושב ותוצאתו נוספת ל HTML הסופי.
    • מה שאולי קצת מיוחד, הוא שיש לנו גישה לשדות והפונקציות של המחלקה של הרכיב. במקרה הזה yourName ו ()addNumbers הם שדה ופונקציה במחלקה, בהתאמה.



  • Property Binding היא צורה מתקדמת יותר המאפשרת לקשור property של אלמנט HTML לשדה במחלקה של הרכיב. למשל, image URL. 
    • תחביר: את ה property של ה HTML Element לו אנו רוצים לעשות binding אנו עוטפים בסוגריים מרובעים (להלן: box), והערך שמוצב הוא expression שיעבור evaluation בזמן ה binding. במקרה שלנו זהו שם של שדה על מחלקת הרכיב (יכול להיות גם פונקציה, או כל ביטוי ב ג'אווהסקריפט).
    • יכולנו לכתוב את אותו הביטוי כ interpolation:
      <""=img src={{imgUrl}} alt>
    • התוצאה של חישוב ביטוי ב interpolation - תמיד תהיה מחרוזת. אם יש property מטיפוס אחר (למשל: disabled הוא property מטיפוס Boolean) - עלינו להשתמש ב Property Binding. מחרוזת שאינה ריקה ב js - תחשוב כ true. כלומר: גם "false" היא true. אופס!
    • ה Property Binding מאפשר לנו לגשת לתכונות מקוננות, מה שמאוד קשה לעשות עם interpolation. למשל, לקבוע רוחב של תמונה:
      < "img [src]="imgUrl" [style.width.px]="imageWidth>
      • התכונה הזו נקראת "Style binding", אבל יש גם class binding או attribute binding ועוד...

  • Event Binding היא היכולת להגיב ל Browser Event בתוך המחלקה של הרכיב. במקום שנכתוב inline javaScript בתוך ה Template (פרקטיקה רעה לתחזוקה) - אנו נעטוף את שם האירוע שאנו רוצים לטפל בו בסוגריים עגולים, ואז נציב לתוכו ביטוי שיהווה את ה handler באירוע. בד"כ: הפעלה של פונקציה.


  • לעתים אנו רוצים גם להציג תכונה של המחלקה ב UI, וגם לתת ל UI לשנות את הערך - ולעדכן את המחלקה. היכולת הזו מתאפשרת בקלות בעזרת Two-Way-Data-Binding (בקיצור: 2WDB) יכולת שעוד זכורה לטובה מ AngularJS (אם כי היא גם זכורה לרעה, בשל שימוש יתר).
    • בקיצור: אפשר לקצר 2 ביטויי bindings שנראים כך:
[value]="view.value" (valueChange)="view.value = $event"
    • לביטוי המקוצר הבא: "value)]="view.value)].
      האופרטור נקרא "Banana in a box" בכדי להזכיר שהסוגריים העגולים הם פנימיים לסוגריים המרובעים.
    • השימוש הנפוץ של 2WDB הוא בטפסים (Forms), כאן בא לשימוש Angular Forms (מודול אופציונלי של Angular) שניתן להוסיף לאפליקציה, ואז להשתמש ב directive בשם ngModel המקל את העבודה עם 2WDB וטפסים. בשימוש ב ngModel יש לזכור להוסיף את FormsModule ל import של ה NgModule שלנו.
    • בדוגמה למעלה, חיברנו את 2 השדות name ו age של מחלקת הרכיב לשדות input.
      • כל שינוי ב UI - יעדכן את ערך השדה באובייקט (ויקפוץ את האירוע: ngOnChnages)
      • שינוי בערך השדה מתוך הקוד - יעדכן את ערך השדה ב UI.
    • בדוגמת הקוד למעלה גם שילבתי pipe, רכיב באנגולר שבצע formatting ל output של שדות.
      ה pipes הם יכולת משלימה ל data binding מכיוון שיש פער בין האופן שבו אנו רוצים להחזיק את הנתונים במחלקה - ובאופן שאנו רוצים להציג אותם ב UI. למשל: formatting של Date. ישנם כמה pipes מובנים באנגולר, ואתם יכולים לכתוב custom pipes משלכם - לשימוש חוזר ואחיד באפליקציה.
      • ה Titlecase pipe הופך את המחרוזת ל lowerCase מלבד אות ראשונה בכל מילה שתהיה Capital. מה שלעתים נקרא גם humanize.

כל דוגמאות הקוד שלי לגבי data binding נלקחו מהפוסט הזה שמכסה בצורה תמציתית (אך רחבה יותר ממה שאני כיסיתי) את נושא ה Data Binding.



סיכום


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

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


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


----

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

השוואה בין אנגולר ל React. מה ההבדל? מה הדמיון?

----

[א] why you might NOT want to use TypeScript - חלק א', חלק ב'.

[ב] Decorators ב TypeScript הם כמו Attributes ב #C או Annotations בג׳אווה: דרך להגדיר metadata על רכיב בשפה. במידה ועובדים עם ES5 - יש workaround אחר.