Skip to content

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

  1. Managers are independent — they can be developed, tested, and modified without touching others
  2. No circular imports — managers import protocols.py, not window.py
  3. Easy testing — mock the protocol instead of the entire window
  4. Clear contracts — the protocol documents exactly what managers can access