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.

๐Ÿงฌ Inheritance: 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.

๐Ÿ’ก Composite Pattern Benefit: Use 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.

RoleClass / ExampleBehavior
Observer InterfaceShellObserverDeclares event methods: on_session_start, on_command_result, etc.
Leaf ObserverFileLogger, ConsoleReporterImplements actual reaction logic.
Composite ObserverMultiObserverStores 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.

Parameters:
  • observers (List[ShellObserver], optional) โ€“ Prepopulated list of observer instances. Defaults to an empty list.
Internal State: 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.
๐Ÿงช Real-world scenario: Use MultiObserver to attach logging, performance metrics, and real-time dashboard updates simultaneously without coupling them together.

๐Ÿ“‹ 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.

MethodDescription
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

๐Ÿ’Ž Note on exception handling: In the provided code, each observer's method is called directly. To ensure robustness, consider wrapping each call in a try/except block to prevent a faulty observer from interrupting notifications to others.