• בלוג
  • שיתוף קוד בין Stores בארכיטקטורת Flux

שיתוף קוד בין Stores בארכיטקטורת Flux

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

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

1. אז למה קשה לשתף קוד בין מנהלי מידע?

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

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

המתג ״פעיל״ מנוהל במנהל מידע שלישי שנקרא ListeningStore. כך נראה הקוד עבור מנהל מידע זה:

var actions = {
  setListenState: function(val) {
    Dispatcher.dispatch({ type: SET_LISTEN, payload: val });
  }
};

/******
* ListeningStore
*/
var ListeningStore = Object.assign({}, Store, {
  listening: true,

  setListening: function(val) {
    this.listening = val;
    this.trigger();
  }
});

ListeningStore.dispatchToken = Dispatcher.register(function(action) {
  switch(action.type) {

    case SET_LISTEN:
      ListeningStore.setListening(action.payload);
      break;
  }
});

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

UsersStore.dispatchToken = Dispatcher.register(function(action) {
  switch(action.type) {
    case NEW_MESSAGE:
      Dispatcher.waitFor([ListeningStore.dispatchToken]);
      if ( ! ListeningStore.listening ) return;

      if ( UsersStore.hasUser(action.payload.from) ) {
        UsersStore.addUser(action.payload.from);
      }
  }
});

ובאותו האופן ב MessagesStore:

MessagesStore.dispatchToken = Dispatcher.register(function(action) {
  switch(action.type) {

    case NEW_MESSAGE:
      Dispatcher.waitFor([ListeningStore.dispatchToken, UsersStore.dispatchToken]);
      if ( ! ListeningStore.listening ) return;

      MessagesStore.addMessage(action.payload.from, action.payload.text);
      break;
  }
});

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

2. אפשרות ראשונה: הגדרת פונקציה משותפת

האפשרות הקלה ביותר היא לקחת את הקוד המשותף לפונקציה ולהוסיף אותה למשל ל ListeningStore. כך זה יראה:

var ListeningStore = Object.assign({}, Store, {
  listening: false,

  setListening: function(val) {
    this.listening = val;
    this.trigger();
  },

  waitAndCheck: function() {
    Dispatcher.waitFor([ListeningStore.dispatchToken]);
    return this.listening;
  }
});

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

UsersStore.dispatchToken = Dispatcher.register(function(action) {
  switch(action.type) {
    case NEW_MESSAGE:
      if ( ! ListeningStore.waitAndCheck() ) { return; }

      if ( UsersStore.hasUser(action.payload.from) ) {
        UsersStore.addUser(action.payload.from);
      }
  }
});

מצד אחד הצלחנו לצמצם את הכפילות אך המצב לא אופטימלי: עדיין עלינו להוסיף את הקריאה לפונקציה המשותפת מכל מנהל מידע רלוונטי. בנוסף אין לנו מקום אחד לבדוק בו איזה מודולים יושפעו מכפתור Active ואם נרצה לחפש אותם נצטרך לבצע ״חיפוש בקבצים״; בעיה נוספת היא שכעת כשמסתכלים על קוד של UsersStore לא ברור ממבט ראשון שרכיב זה תלוי ב ListeningStore, והעסק הופך יותר מבלבל אם UsersStore יקבל תלות ברכיבים אחרים באמצעות קריאה מפורשת ל waitFor . מוזמנים לשחק עם הקוד המלא בקישור:
http://codepen.io/ynonp/pen/VevKyd

3. אפשרות שניה: חסימת הודעה באמצעות שינוי פונקציית Dispatch

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

{
  let next = Dispatcher.dispatch;
  Dispatcher.dispatch = function(action) {
    if ( action.type === SET_LISTEN ) {
      ListeningStore.setListening(action.payload);
    }

    if ( action.type === NEW_MESSAGE && ! ListeningStore.listening ) {
      // skip new messages when not listening
      return;
    }

    next.call(Dispatcher, action);
}

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

מוזמנים לשחק עם קוד הדוגמא בקישור:
http://codepen.io/ynonp/pen/OMyRrK

4. אפשרות שלישית: חסימת הודעה לנמען באמצעות שינוי פונקציית Dispatch

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

{
  let next = Dispatcher.dispatch;
  Dispatcher.dispatch = function(action) {
    if ( action.type === SET_LISTEN ) {
      ListeningStore.setListening(action.payload);
    }

    if ( action.type === NEW_MESSAGE && ! ListeningStore.listening ) {
      // skip new messages when not listening
      let old_callbacks = Object.assign({}, Dispatcher._callbacks);
      delete Dispatcher._callbacks[UsersStore.dispatchToken];
      delete Dispatcher._callbacks[MessagesStore.dispatchToken];

      next.call(Dispatcher, action);

      Dispatcher._callbacks = old_callbacks;
    } else {
      next.call(Dispatcher, action);
    }
  }
}

מוזמנים לראות את הקוד המלא בקישור:
http://codepen.io/ynonp/pen/ZQbpRB

5. סיכום

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

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