001. Layered Architecture with Protocol-Based Boundaries¶
Status¶
Accepted
Context¶
The diagnostic output system needs to support multiple usage patterns: developers want simple one-line debug statements, libraries need non-intrusive registration, and applications require fine-grained control over output formatting and routing. Existing approaches either tightly couple components (hard to extend) or require extensive boilerplate (poor developer experience).
The system must accommodate:
Multiple output targets: stderr (default), files, logging integration, custom sinks
Multiple formatting strategies: plain text, Rich-colorized, custom layouts
Library-friendly configuration: per-module settings without global pollution
Optional dependencies: Rich library should enhance but not be required
Testing support: output capture, deterministic formatting
Decision¶
Implement a four-layer architecture with protocol-based boundaries:
Layer 1 - Dispatcher: Entry point managing reporter selection and
activation control. Provides attribute-based access (ctrl.note(...)) and
caches reporter instances per address + flavor combination.
Layer 2 - Reporter: Coordination layer binding compositor to printer for specific address + flavor. Handles active/inactive state and packages user content into structured records.
Layer 3 - Compositor: Transforms records into formatted text lines using composed subsystems (introducer for prefixes, linearizers for content). Receives column constraints from printer layer.
Layer 4 - Printer: Abstracts output targets. Provides textualization control information (columns, colorization capability) and writes formatted output.
Layers communicate through protocols (structural typing) rather than inheritance, enabling independent implementation and testing.
Alternatives¶
Single Monolithic Class
Combine dispatcher, formatting, and output in one class with template methods for customization.
Rejected: Violates single responsibility principle. Hard to test formatting without output. Configuration becomes unwieldy (one class with 20+ template methods). Cannot independently replace formatting vs output strategies.
Observer Pattern with Event Bus
Route messages through event bus to registered handlers.
Rejected: Over-engineering for diagnostic output. Async complexity not needed. Per-module configuration becomes awkward (need module-scoped subscriptions). Testing harder (must mock event bus infrastructure).
Plugin Architecture with Entry Points
Use setuptools entry points for formatters and outputs.
Rejected: Runtime configuration more important than install-time plugins. Adds packaging complexity. Testing requires filesystem manipulation. Per-module configuration unclear (how to specify which module uses which plugin).
Consequences¶
Positive Consequences
Independent evolution: Layers can change implementation without affecting others as long as protocols are maintained.
Testing clarity: Each layer tests independently with mock implementations of dependencies.
Configuration flexibility: Factory pattern enables per-address, per-flavor customization at each layer.
Optional dependencies: Rich integration isolated in compositor layer with graceful fallback.
Clear extension points: Protocols document exactly what custom implementations must provide.
Negative Consequences
Indirection overhead: Four layers add call stack depth compared to monolithic approach (negligible for diagnostic output).
Protocol verbosity: Each protocol requires TypedDict or Protocol definition plus documentation.
Learning curve: Developers must understand layer responsibilities to customize effectively.
Neutral Consequences
Factory pattern ubiquity: Almost every component uses factory pattern for instantiation (consistent but more conceptual overhead).
Protocol over ABC: Structural subtyping more flexible but less explicit than abstract base classes.