חיווט קומפננטות של JavaScript באמצעות דפוס Mediator

מחבר:
בתאריך:

הטעות הגדולה ביותר בפיתוח תוכנה הוא לגרום לרכיבים (מחלקות) לדבר אחד עם השני ישירות. כאשר רכיב A קורא למתודה ברכיב B, נוצרים "נישואין" שמונעים שינוי של אחד מבלי לשבור את השני. במקום זאת, אנו רוצים שרכיבים יקראו אל החלל (emit events) ויאפשרו למתווך (mediator) להקשיב לקריאה, ולהפעיל את המתודות ברכיבים השונים על פי צורך.

Mediator Pattern in JavaScript

 

0. הסקריפטים הנחוצים להדגמה

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

הקבצים הדרושים לפעולת האפליקציה הם:

demo
├── counter_display.js
├── event_emitter.js
├── game_api.js
├── game_controller.js
├── game_display.js
├── guess_form.js
├── index.html
└── main.js
1 directory, 8 files
  • כולם יושבים ביחד בתיקייה אחת.

 

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>The Guessing Game</title>
    <link
      href="/css/bootstrap.min.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <h4>Pick a number between 1 and 10</h4>
    <div id="status-text"></div>
    <div id="attempts-count"></div>
    <form action="" id="guess-form">
      <input type="number" />
      <button>Submit</button>
    </form>

    <script type="module" src="./main.js"></script>
  </body>
</html>

   

main.js

import { GuessForm } from "./guess_form.js";
import { GameDisplay } from "./game_display.js";
import { CounterDisplay } from "./counter_display.js";
import { GameAPI } from "./game_api.js";
import { GameController } from "./game_controller.js";

// main.js
const form = new GuessForm(document.querySelector("#guess-form"));
const display = new GameDisplay(document.querySelector("#status-text"));
const counter = new CounterDisplay(document.querySelector("#attempts-count"));
const api = new GameAPI();

// The Controller bridges the world
const game = new GameController(form, display, counter, api);

   

counter_display.js

export class CounterDisplay {
  constructor(el) {
    this.el = el;
  }

  update(count) {
    this.el.textContent = `Attempts: ${count}`;
  }
}
  • המחלקה מציגה הודעות בתוך האלמנט שמועבר לקונסטרקטור.

   

1. הבסיס: Event Emitter

ראשית, אנו זקוקים לדרך פשוטה לקרוא ולהקשיב, ולצורך זה נשתמש במחלקה EventEmitter:

event_emitter.js

export  class EventEmitter {
  constructor() { this.events = {}; }
  on(event, cb) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(cb);
  }
  emit(event, data) {
    if (this.events[event]) this.events[event].forEach(cb => cb(data));
  }
}

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

  • constructor: מאתחל אובייקט ריק (this.events) כדי לאחסן רשימות של פונקציות callback עבור שמות אירועים שונים.
  • on(event, cb): היא מתודת ההרשמה "subscribe". היא מאפשרת לרכיב לומר, "כאשר אירוע זה מתרחש, הפעל פונקציה זו."
  • emit(event, data): היא המתודה המשדרת "broadcast". היא מפעילה את כל הפונקציות הרשומות עבור אירוע ספציפי ומעבירה את הנתונים הרלוונטיים.

 

2. רכיבי התצוגה (ממשק המשתמש)

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

הטופס:

guess_form.js

import { EventEmitter } from "./event_emitter.js";

export class GuessForm extends EventEmitter {
  constructor(el) {
    super();
    this.input = el.querySelector('input');
    el.querySelector('button').onclick = () => {
      this.emit('guessSubmitted', this.input.value);
    };
  }
}
  • יורש את המחלקה EventEmitter דבר המאפשר לו להשתמש במתודות שלה כדי להכריז על המידע ברחבי האפליקציה.
  • באופן ספציפי, הוא מאזין לכפתור HTML וכשמשתמש מקליק על הכפתור, הוא עושה emit של האירוע "guessSubmitted" ביחד עם מידע נוסף.

 

התצוגה:

game_display.js

export class GameDisplay {
  constructor(el) { this.el = el; }
  render(message, color = 'black') {
    this.el.textContent = message;
    this.el.style.color = color;
  }
}
  • רכיב זה הוא פסיבי לגמרי. יש לו תפקיד אחד: לקבל מחרוזת טקסט וצבע ולהציג למשתמש כשקוראים לו.

 

3. צד השרת

המחלקה הבאה מדמה את צד השרת.

game_api.js

export class GameAPI {
  constructor() { this.secretNumber = Math.floor(Math.random() * 10) + 1; }

  async checkGuess(num) {
    // Simulate network delay
    return new Promise(resolve => {
      setTimeout(() => {
        const guess = parseInt(num);
        if (guess === this.secretNumber) resolve({ status: 'win', msg: 'Correct!' });
        else resolve({ status: 'fail', msg: guess > this.secretNumber ? 'Too high!' : 'Too low!' });
      }, 500);
    });
  }
}

כולל סימולציה לדימות:

  • יצירת "secretNumber" מספר אקראי בין 1 ל-10 בתחילת כל משחק.
  • בדיקה של הניחוש שאותו מזין המשתמש באמצעות פונקציה א-סינכרונית checkGuess(num).

 

4. הקונטרולר המתווך mediator

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

game_controller.js

import { EventEmitter } from "./event_emitter.js";

export class GameController {
  constructor(form, display, api) {
    this.form = form;
    this.display = display;
    this.api = api;

    this.wireEvents();
  }

  wireEvents() {
    this.form.on('guessSubmitted', async (value) => {
      this.display.render('Checking...', 'blue');
      
      const result = await this.api.checkGuess(value);
      
      const color = result.status === 'win' ? 'green' : 'red';
      this.display.render(result.msg, color);
    });
  }
}

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

  • constructor: מקבל אינסטנסים של Form, Display ו-API, ומאחסן אותם.

  • המתודה wireEvents() היא החלק הכי חשוב של דפוס ה-Mediator שמחבר את חלקי האפליקציה, ובכלל כך:

    1. מאזינה ל-"guessSubmitted" המגיע מהטופס.
    2. כשמגיע האירוע היא מורה לתצוגה להציג מסר "Checking..."
    3. שולחת את המידע ל-API ומחכה לתגובה.
    4. כאשר ה-API מגיב, היא מחליטה מה הצבע (ירוק או אדום) ומורה ל-Display להציג את התוצאה.

 

למה זה עדיף?

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

 

סיכום המסע

  • רכיבי UI פולטים אירועים.
  • API מספקים נתונים.
  • קונטרולר מתווך בין רכיבי צד השרת.

 

זה דפוס ה-mediator ועוד קצת על רגל אחת.

 

אולי גם זה יעניין אותך?

העברת פונקציות כארגומנטים לפונקציות ב-JS

מה זה? 8 תרגילים בהבנת המילה השמורה this ב-JavaScript

עובד שירות , service worker, שם צנוע לטכנולוגיה מדהימה

 

מדריכי JavaScript למתקדמים

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

 

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

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

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

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

השימוש שלך באתר מהווה ראייה להסכמתך עם הכללים והתקנות שנוסחו בהסכם תנאי השימוש.

הוסף תגובה חדשה

 

 

ענה על השאלה הפשוטה הבאה כתנאי להוספת תגובה:

מתי הוקמה המדינה?