דלג לתוכן הראשי
שכבת הפשטה ארכיטקטורת תוכנה בעברית
תקשורת בין אובייקטים

תבנית Mediator: אובייקט מתווך שמחליף רשת של תלויות הדדיות — מימוש מלא על טופס עם רכיבים תלויים

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

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

הכוונה

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

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

הבעיה: כל אחד מכיר את כולם

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

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

המשתתפים והמבנה

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

משתתף תפקיד בדוגמה שלנו
Mediator מגדיר ממשק לתקשורת עם העמיתים FormMediator
ConcreteMediator מממש התנהגות שיתופית על ידי תיאום העמיתים, ומכיר ומחזיק אותם ShippingForm
Colleague כל עמית מכיר את המתווך שלו, ומתקשר דרכו במקום עם עמיתים אחרים Widget, TextField, CheckBox, Button

מימוש מלא ב-Java

Java
// Mediator: colleagues report changes here instead of calling each other.
interface FormMediator {
    void changed(Widget source);
}

// Colleague base class: knows only its mediator.
abstract class Widget {
    private final FormMediator mediator;
    private boolean enabled = true;

    protected Widget(FormMediator mediator) { this.mediator = mediator; }

    void setEnabled(boolean enabled) { this.enabled = enabled; }

    boolean isEnabled() { return enabled; }

    protected void notifyChanged() { mediator.changed(this); }
}

// Concrete colleagues: generic widgets with no knowledge of each other.
class TextField extends Widget {
    private String text = "";

    TextField(FormMediator mediator) { super(mediator); }

    void type(String value) {
        text = value;
        notifyChanged();
    }

    String getText() { return text; }
}

class CheckBox extends Widget {
    private boolean checked = false;

    CheckBox(FormMediator mediator) { super(mediator); }

    void toggle() {
        checked = !checked;
        notifyChanged();
    }

    boolean isChecked() { return checked; }
}

class Button extends Widget {
    Button(FormMediator mediator) { super(mediator); }

    void click() {
        if (isEnabled()) {
            notifyChanged();
        }
    }
}

// Concrete mediator: the only place that knows the form's rules.
final class ShippingForm implements FormMediator {
    final TextField name = new TextField(this);
    final CheckBox otherAddress = new CheckBox(this);
    final TextField address = new TextField(this);
    final Button submit = new Button(this);

    ShippingForm() { update(); }

    @Override
    public void changed(Widget source) {
        if (source == submit) {
            String target = otherAddress.isChecked() ? " to " + address.getText() : " to home address";
            System.out.println("submitted: " + name.getText() + target);
            return;
        }
        update();
    }

    private void update() {
        address.setEnabled(otherAddress.isChecked());
        boolean hasName = !name.getText().isBlank();
        boolean addressOk = !otherAddress.isChecked() || !address.getText().isBlank();
        submit.setEnabled(hasName && addressOk);
    }

    String state() {
        return "address " + (address.isEnabled() ? "on" : "off")
            + ", submit " + (submit.isEnabled() ? "on" : "off");
    }
}

public class Main {
    public static void main(String[] args) {
        ShippingForm form = new ShippingForm();
        System.out.println("start:        " + form.state());
        form.name.type("Dana");
        System.out.println("name typed:   " + form.state());
        form.otherAddress.toggle();
        System.out.println("box checked:  " + form.state());
        form.address.type("12 Herzl St, Haifa");
        System.out.println("address set:  " + form.state());
        form.submit.click();
    }
}

הפלט:

פלט
start:        address off, submit off
name typed:   address off, submit on
box checked:  address on, submit off
address set:  address on, submit on
submitted: Dana to 12 Herzl St, Haifa

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

אותו מבנה בפייתון

Python
class Widget:
    def __init__(self, mediator):
        self.mediator, self.enabled = mediator, True

    def changed(self):
        self.mediator.changed(self)


class TextField(Widget):
    text = ""

    def type(self, value):
        self.text = value
        self.changed()


class CheckBox(Widget):
    checked = False

    def toggle(self):
        self.checked = not self.checked
        self.changed()


class ShippingForm:
    def __init__(self):
        self.name, self.box, self.address = TextField(self), CheckBox(self), TextField(self)
        self.submit_enabled = False
        self.changed(None)

    def changed(self, source):
        self.address.enabled = self.box.checked
        address_ok = not self.box.checked or bool(self.address.text.strip())
        self.submit_enabled = bool(self.name.text.strip()) and address_ok


form = ShippingForm()
form.name.type("Dana")
form.box.toggle()
print(form.address.enabled, form.submit_enabled)
form.address.type("12 Herzl St, Haifa")
print(form.address.enabled, form.submit_enabled)

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

איך עמיתים מודיעים למתווך

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

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

מחוץ לטפסים

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

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

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

השלכות

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

מתי לא להשתמש

  • שני רכיבים וכלל אחד. הפניה ישירה ברורה יותר מאובייקט מתווך שכל תפקידו להעביר הודעה אחת.
  • התקשורת חד־כיוונית. אם יש מקור אחד וכמה מאזינים שאינם מדברים בחזרה, Observer מספיק.
  • המטרה היא רק להסתיר תת־מערכת. זה תפקיד של Facade. ההבדל לפי הספר: Facade היא חד־כיוונית — היא פונה אל תת־המערכת, ותת־המערכת אינה מכירה אותה. Mediator היא דו־כיוונית — העמיתים מכירים את המתווך ומדברים איתו.

תבניות קשורות

Observer היא המנגנון הנפוץ להעברת הודעות מהעמיתים למתווך. Facade דומה במבט ראשון, אבל חד־כיוונית. Chain of Responsibility היא דרך אחרת לנתק שולח ממקבל, אבל שם הבקשה עוברת בין מטפלים ולא מתרכזת באובייקט אחד. בהקשר רחב יותר, Mediator היא יישום ישיר של הרעיונות שבמאמר על צימוד ולכידות: פחות קשרים בין רכיבים, ויותר לכידות בכל רכיב.

טעויות נפוצות

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

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

נכתב על ידי

רועי פרידמן : כותב

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

להמשך קריאה

לכל המאמרים בנושא

תקשורת בין אובייקטים

Observer Design Pattern: תבנית Observer למנוי לשינויים, אירועים וההבדל בינה לבין Pub/Sub

observer design pattern היא אחת מתבניות ההתנהגות החשובות ביותר בתכנון מונחה־עצמים: היא מאפשרת לאובייקט אחד להודיע לאובייקטים אחרים שמשהו השתנה, בלי להכיר את המחלקות הקונקרטיות שלהם ובלי לבנות תלות ישירה…

10 דק׳ קריאה

תקשורת בין אובייקטים

תבנית Chain of Responsibility: שרשרת מטפלים שבה כל חוליה מחליטה אם לטפל או להעביר — מימוש מלא

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

8 דק׳ קריאה