Mediator & Protocol¶
The Problem¶
codemol has 8 managers, a console, a viewer, a tool system, and various panels. If each component referenced others directly, you'd get a tangled web of imports and tight coupling.
The Mediator Pattern¶
MolbotWindow acts as the mediator — the central hub that all components communicate through. Managers don't know about each other; they only know about the AppContext interface.
graph TD
SM[StructureManager] -->|AppContext| W[MolbotWindow]
MM[MeasurementManager] -->|AppContext| W
AM[AgentManager] -->|AppContext| W
DM[DockingManager] -->|AppContext| W
W --> Console
W --> Viewer
W --> Tools
W --> Session
Without the mediator, you'd have:
graph TD
SM <--> MM
SM <--> AM
MM <--> DM
AM <--> Console
DM <--> Viewer
SM <--> Console
The Protocol Pattern¶
AppContext is defined as a typing.Protocol in codemol/app/protocols.py:
class AppContext(Protocol):
@property
def viewer(self) -> Viewer: ...
@property
def console(self) -> Console: ...
@property
def session(self) -> StructureSession: ...
@property
def tools(self) -> dict: ...
def scope(self) -> str | None: ...
def handle_command(self, text: str) -> None: ...
def update_status_bar(self, **kwargs) -> None: ...
def update_window_title(self, name: str = "") -> None: ...
Why Protocol (not ABC)?¶
| Feature | Protocol | ABC |
|---|---|---|
| Import needed | Just protocols.py |
Must import base class |
| Circular imports | Avoided | Risk of window ↔ manager cycles |
| Structural typing | Yes (duck typing) | No (explicit inheritance) |
| Testability | Easy to mock | Need to subclass |
Structural Typing¶
MolbotWindow doesn't inherit from AppContext — it just has the same methods and properties. Python's Protocol checks this structurally:
# MolbotWindow satisfies AppContext without inheriting it
class MolbotWindow(QMainWindow):
@property
def viewer(self): return self._viewer
@property
def console(self): return self._console
# ... etc
Testability¶
For testing, you can create a minimal mock that satisfies AppContext:
class MockContext:
def __init__(self):
self.viewer = MagicMock()
self.console = MagicMock()
self.session = StructureSession()
self.tools = discover_tools()
def scope(self): return None
def handle_command(self, text): pass
def update_status_bar(self, **kwargs): pass
def update_window_title(self, name=""): pass
# Use in tests:
manager = StructureManager(MockContext())
Benefits¶
- Managers are independent — they can be developed, tested, and modified without touching others
- No circular imports — managers import
protocols.py, notwindow.py - Easy testing — mock the protocol instead of the entire window
- Clear contracts — the protocol documents exactly what managers can access