8.10. 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.

8.10.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

8.10.2. Problem

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

8.10.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 8.3. 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 = field(default_factory=ListBox)
    title_textbox: TextBox = field(default_factory=TextBox)
    save_button: Button = field(default_factory=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()

8.10.4. Assignments

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: DesignPatterns Behavioral Mediator
# - Difficulty: medium
# - Lines: 15
# - Minutes: 21

# %% 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
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

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

>>> form = LoginForm()
>>> form.set_username('mwatney')
>>> form.set_password('Ares3')
>>> 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')