MultiObserver COMPOSITE PATTERN
A composite observer that manages multiple ShellObserver instances and
broadcasts every shell lifecycle event to all registered observers.
๐ Class Overview
MultiObserver is a concrete implementation of the Composite pattern in the observer ecosystem.
It extends ShellObserver and acts as a collection (or a composite) that can hold any number of child observers.
When any shell event occurs (session start/end, context change, command execution, errors),
MultiObserver iterates through its internal list and delegates the notification to every registered observer.
ShellObserver (abstract base) โ MultiObserver
This design allows you to treat a group of observers as a single entity, making it easy to attach or detach entire sets of observers without modifying core event logic.
MultiObserver to combine logging, metrics, debugging,
and real-time monitoring observers โ all triggered by one notification call.
๐งฉ Composite Pattern in Action
Component: ShellObserver (abstract observer interface)
Leaf: Individual concrete observers (e.g., LoggingObserver, MetricsObserver)
Composite: MultiObserver โ manages child observers and forwards all events to them.
| Role | Class / Example | Behavior |
|---|---|---|
| Observer Interface | ShellObserver | Declares event methods: on_session_start, on_command_result, etc. |
| Leaf Observer | FileLogger, ConsoleReporter | Implements actual reaction logic. |
| Composite Observer | MultiObserver | Stores observers, forwards notifications. |
The MultiObserver itself is a ShellObserver, meaning composites can be nested,
offering hierarchical observer structures.
โ๏ธ Constructor & Initialization
__init__(observers: List[ShellObserver] = None)
Initializes a new composite observer that holds an optional initial list of observers.
observers(List[ShellObserver], optional) โ Prepopulated list of observer instances. Defaults to an empty list.
self.observers โ list that stores all child observers.
# Example: create empty composite
multi = MultiObserver()
# Or with initial observers
multi = MultiObserver([console_observer, file_observer])
๐ Public Methods
MultiObserver implements two categories of methods: observer management and
event notification forwarding (fulfilling the ShellObserver contract).
โ Observer Management
add_observer(observer: ShellObserver) -> None
Adds a new ShellObserver to the internal collection. The added observer will receive future shell events.
๐ก Event Forwarding (ShellObserver Implementation)
on_session_start(shell_name: str)
Invoked when a shell session begins. Notifies all registered observers by calling their on_session_start.
on_session_end(shell_name: str, error: Exception = None)
Signals the end of a shell session. Propagates the event (and optional error) to every child observer.
on_context_change(key: str, value: Any)
Broadcasts context variable changes (e.g., environment or shell state) to all managed observers.
on_command_start(executable: str, final_args: List[str])
Notifies all observers that a command is about to execute, providing the executable path and its arguments.
on_command_result(result: CommandResult)
Forwards the command execution result (exit code, stdout/stderr, etc.) to each observer in the composite.
on_error(message: str, error: Exception = None)
Distributes error notifications to all child observers, useful for centralized error handling across plugins.
๐ป Usage Example (Python)
The following example demonstrates how to use MultiObserver to combine two different observers:
a simple console logger and a mock metrics collector. The composite observer is attached to a shell
controller (hypothetical) and all events are broadcasted simultaneously.
from typing import List, Any
from dataclasses import dataclass
# Mock ShellObserver base and CommandResult (for demonstration)
class ShellObserver:
def on_session_start(self, shell_name: str): pass
def on_session_end(self, shell_name: str, error: Exception = None): pass
def on_context_change(self, key: str, value: Any): pass
def on_command_start(self, executable: str, final_args: List[str]): pass
def on_command_result(self, result: 'CommandResult'): pass
def on_error(self, message: str, error: Exception = None): pass
@dataclass
class CommandResult:
command: str
exit_code: int
stdout: str
stderr: str
# Concrete observer 1: prints events to console
class ConsoleObserver(ShellObserver):
def on_session_start(self, shell_name: str):
print(f"[Console] Session '{shell_name}' started")
def on_command_result(self, result: CommandResult):
print(f"[Console] Command '{result.command}' finished with code {result.exit_code}")
def on_error(self, message: str, error: Exception = None):
print(f"[Console] Error: {message} | {error}")
# Concrete observer 2: counts commands
class CounterObserver(ShellObserver):
def __init__(self):
self.command_count = 0
def on_command_start(self, executable: str, final_args: List[str]):
self.command_count += 1
print(f"[Counter] commands seen so far: {self.command_count}")
def on_session_end(self, shell_name: str, error: Exception = None):
print(f"[Counter] total commands in session: {self.command_count}")
# ----- Using MultiObserver -----
if __name__ == "__main__":
console = ConsoleObserver()
counter = CounterObserver()
# Create composite observer and attach both
multi = MultiObserver([console, counter])
# Simulate shell events
multi.on_session_start("bash")
multi.on_command_start("/bin/ls", ["ls", "-la"])
multi.on_command_result(CommandResult("ls -la", 0, "file1 file2", ""))
multi.on_command_start("/usr/bin/python", ["python", "--version"])
multi.on_command_result(CommandResult("python --version", 0, "Python 3.11", ""))
multi.on_error("Config file missing", None)
multi.on_session_end("bash")
# Output will show both observers reacting to every event.
# The counter observer increments on command_start,
# ConsoleObserver prints each event line.
๐ ShellObserver Contract
The MultiObserver relies on the standard observer interface. Each child observer must implement
the following methods. This design aligns with the Observer pattern and guarantees
polymorphic event handling.
| Method | Description |
|---|---|
on_session_start(shell_name: str) | Triggered when a new shell session is initialized. |
on_session_end(shell_name: str, error: Exception) | Called when the session terminates; may contain an error if abnormal exit. |
on_context_change(key: str, value: Any) | Notifies observers about dynamic context updates (e.g., $PWD change). |
on_command_start(executable: str, final_args: List[str]) | Emitted just before a subprocess or shell command runs. |
on_command_result(result: CommandResult) | Delivers the result of an executed command (including exit code, output). |
on_error(message: str, error: Exception) | Broadcasts error events that occur during shell operations. |
By implementing the same interface, MultiObserver transparently composes any number of observers
and behaves exactly like any other ShellObserver.
๐ฏ Design Highlights & Best Practices
- Open/Closed Principle:
MultiObserveris closed for modification but open for extension โ you can add new types of observers without altering the composite. - Fail-Safe Iteration: The composite iterates over a list copy? The current implementation iterates directly over
self.observers. For production, consider handling exceptions per observer so one failing observer doesn't break others. - Dynamic Composition: Use
add_observerto attach observers at runtime, making the system flexible. - Nesting composites: Since
MultiObserverinherits fromShellObserver, you can add aMultiObserverinside anotherMultiObserverfor hierarchical grouping.