6.8. Mediator

  • EN: Mediator

  • PL: Mediator

  • Type: object

The Mediator design pattern is a behavioral design pattern that reduces coupling between classes by making them communicate indirectly, through a mediator object. The mediator object handles and coordinates communication between different classes.

In Python, we can implement the Mediator pattern using classes. Here's a simple example:

First, we define a Mediator class that will coordinate communication between Colleague objects:

>>> class Mediator:
...     def notify(self, sender, event):
...         pass

Then, we define a Colleague class that communicates with other Colleague objects through the Mediator:

>>> class Colleague:
...     def __init__(self, mediator):
...         self._mediator = mediator
...
...     def do_something(self):
...         self._mediator.notify(self, "event")

Finally, we can use the Mediator and Colleague classes like this:

>>> class ConcreteMediator(Mediator):
...     def notify(self, sender, event):
...         print(f"Mediator reacting to event: {event}")
...
>>> class ConcreteColleague(Colleague):
...     def do_something(self):
...         print("Colleague doing something")
...         super().do_something()
...
>>> mediator = ConcreteMediator()
>>> colleague = ConcreteColleague(mediator)
...
>>> colleague.do_something()
Colleague doing something
Mediator reacting to event: event

In this example, the Colleague does something and notifies the Mediator about the event. The Mediator then reacts to the event.

6.8.1. Pattern

  • Input fields which needs to collaborate

  • Cannot submit form if all required fields are not filled

  • If you select article in list of articles, editor form with current article content and title gets populated

  • Auto slug-field based on title content

../../_images/designpatterns-mediator-pattern.png

6.8.2. Problem

../../_images/designpatterns-mediator-problem.png


6.8.3. Solution

../../_images/designpatterns-mediator-solution-1.png
../../_images/designpatterns-mediator-solution-2.png

from abc import ABC, abstractmethod
from dataclasses import dataclass


class DialogBox(ABC):
    """Mediator class"""
    @abstractmethod
    def changed(self, control: 'UIControl') -> None:
        pass


@dataclass
class UIControl(ABC):
    owner: DialogBox


class ListBox(UIControl):
    selection: str

    def __init__(self, owner: DialogBox) -> None:
        super().__init__(owner)

    def get_selection(self) -> str:
        return self.selection

    def set_selection(self, selection: str) -> None:
        self.selection = selection
        self.owner.changed(self)


class TextBox(UIControl):
    content: str

    def __init__(self, owner: DialogBox) -> None:
        super().__init__(owner)

    def get_content(self) -> str:
        return self.content

    def set_content(self, content: str) -> None:
        self.content = content
        self.owner.changed(self)


class Button(UIControl):
    enabled: bool

    def __init__(self, owner: DialogBox) -> None:
        super().__init__(owner)

    def set_enabled(self, enabled: bool) -> None:
        self.enabled = enabled

    def is_enabled(self) -> bool:
        self.owner.changed(self)
        return self.enabled


class ArticlesDialogBox(DialogBox):
    articles_listbox: ListBox
    title_textbox: TextBox
    save_button: Button

    def simulate_user_interaction(self) -> None:
        self.articles_listbox.set_selection('Article 1')
        self.title_textbox.set_content('')
        self.title_textbox.set_content('Article 2')
        print(f'Text box: {self.title_textbox.get_content()}')
        print(f'Button: {self.save_button.is_enabled()}')

    def __init__(self) -> None:
        self.articles_listbox = ListBox(self)
        self.title_textbox = TextBox(self)
        self.save_button = Button(self)

    def changed(self, control: 'UIControl') -> None:
        if control == self.articles_listbox:
            self.article_selected()
        elif control == self.title_textbox:
            self.title_changed()

    def article_selected(self) -> None:
        self.title_textbox.set_content(self.articles_listbox.get_selection())
        self.save_button.set_enabled(True)

    def title_changed(self) -> None:
        content = self.title_textbox.get_content()
        is_empty = (content == None or content == '')
        self.save_button.set_enabled(not is_empty)


if __name__ == '__main__':
    dialog = ArticlesDialogBox()
    dialog.simulate_user_interaction()
Code 6.59. Mediator with Observer Pattern
from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class EventHandler(ABC):
    @abstractmethod
    def __call__(self) -> None:
        pass


@dataclass
class UIControl(ABC):
    observers: list[EventHandler] = field(default_factory=list)

    def add_event_handler(self, observer: EventHandler) -> None:
        self.observers.append(observer)

    def _notify_event_handlers(self):
        for observer in self.observers:
            observer.__call__()


class ListBox(UIControl):
    selection: str

    def get_selection(self) -> str:
        return self.selection

    def set_selection(self, selection: str) -> None:
        self.selection = selection
        self._notify_event_handlers()


class TextBox(UIControl):
    content: str

    def get_content(self) -> str:
        return self.content

    def set_content(self, content: str) -> None:
        self.content = content
        self._notify_event_handlers()


class Button(UIControl):
    enabled: bool

    def set_enabled(self, enabled: bool) -> None:
        self.enabled = enabled
        self._notify_event_handlers()

    def is_enabled(self) -> bool:
        return self.enabled


@dataclass
class ArticlesDialogBox:
    articles_listbox: ListBox = ListBox()
    title_textbox: TextBox = TextBox()
    save_button: Button = Button()

    def __post_init__(self):
        self.articles_listbox.add_event_handler(self.article_selected)
        self.title_textbox.add_event_handler(self.title_changed)

    def simulate_user_interaction(self) -> None:
        self.articles_listbox.set_selection('Article 1')
        self.title_textbox.set_content('')
        self.title_textbox.set_content('Article 2')
        print(f'Text box: {self.title_textbox.get_content()}')
        print(f'Button: {self.save_button.is_enabled()}')

    def article_selected(self) -> None:
        self.title_textbox.set_content(self.articles_listbox.get_selection())
        self.save_button.set_enabled(True)

    def title_changed(self) -> None:
        content = self.title_textbox.get_content()
        is_empty = (content == None or content == '')
        self.save_button.set_enabled(not is_empty)


if __name__ == '__main__':
    dialog = ArticlesDialogBox()
    dialog.simulate_user_interaction()

6.8.4. Assignments

Code 6.60. Solution
"""
* Assignment: DesignPatterns Behavioral Mediator
* Complexity: medium
* Lines of code: 15 lines
* Time: 21 min

English:
    1. Implement Mediator pattern
    2. Create form with Username, Password and Submit button
    3. If Username and Password are provided enable Submit button
    4. Run doctests - all must succeed

Polish:
    1. Zaimplementuj wzorzec Mediator
    2. Stwórz formularz logowania z Username, Password i przyciskiem Submit
    3. Jeżeli Username i Password odblokuj przycisk Submit
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> form = LoginForm()
    >>> form.set_username('root')
    >>> form.set_password('')
    >>> form.submit()
    Traceback (most recent call last):
    PermissionError: Cannot submit form without Username and Password

    >>> form = LoginForm()
    >>> form.set_username('root')
    >>> form.set_password('MyVoiceIsMyPasswordVerifyMe')
    >>> form.submit()
    'Submitted'
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any


@dataclass
class UIElement(ABC):
    name: str
    owner: Form
    value: Any

    def changed(self):
        raise NotImplementedError

    @abstractmethod
    def set_value(self, value: Any) -> None: ...

    @abstractmethod
    def get_value(self) -> Any: ...


@dataclass
class Input(UIElement):
    value: str = ''

    def get_value(self) -> str:
        raise NotImplementedError

    def set_value(self, value: str) -> None:
        raise NotImplementedError


@dataclass
class Button(UIElement):
    value: bool = False

    def set_value(self, value: bool) -> None:
        raise NotImplementedError

    def get_value(self) -> Any:
        raise NotImplementedError

    def enable(self):
        self.set_value(True)

    def disable(self):
        self.set_value(False)

    def is_enabled(self) -> bool:
        return self.value


class Form(ABC):
    @abstractmethod
    def on_change(self): ...


class LoginForm(Form):
    username_input: Input
    password_input: Input
    submit_button: Button

    def __init__(self):
        raise NotImplementedError

    def set_username(self, username: str):
        raise NotImplementedError

    def set_password(self, password: str):
        raise NotImplementedError

    def on_change(self):
        raise NotImplementedError

    def submit(self):
        if self.submit_button.is_enabled():
            return 'Submitted'
        else:
            raise PermissionError('Cannot submit form without Username and Password')