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
8.10.2. Problem
8.10.3. Solution
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()
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')