ADR 0003: Explicit over Composite Protocol Injection¶
Status¶
Accepted
Context¶
With protocol-based dependency injection (ADR 0002), components receive multiple protocol interfaces. A question arose: should we group related protocols into composite interfaces to reduce constructor parameter count?
Protocol usage analysis:
| Component | Protocol Count |
|---|---|
| Device | 16 protocols |
| Hub | 12 protocols |
| DeviceCoordinator | 17 protocols |
| HubCoordinator | 10 protocols |
| CacheCoordinator | 7 protocols |
Common protocol groups identified:
- Core (all components):
central_info,config_provider,task_scheduler - Events (Device, Hub, coordinators):
event_bus_provider,event_publisher - Visibility (Device, Hub, coordinators):
parameter_visibility_provider,paramset_description_provider
Decision¶
Use explicit protocol injection per component rather than grouping protocols into composite interfaces for internal components.
Exception: ClientDependencies¶
ClientDependencies exists as a composite protocol for the external client API, where a stable public interface is valuable. This is the exception, not the rule.
# ClientDependenciesProtocol bundles ~20 methods for external API stability
@runtime_checkable
class ClientDependenciesProtocol(Protocol):
@property
def config(self) -> CentralConfig: ...
@property
def connection_state(self) -> CentralConnectionState: ...
# ... stable public contract
Internal Components: Explicit Injection¶
# Device uses explicit injection (16 parameters)
class Device:
def __init__(
self,
*,
interface_id: str,
device_address: str,
central_info: CentralInfoProtocol,
config_provider: ConfigProviderProtocol,
event_bus_provider: EventBusProviderProtocol,
# ... each dependency explicit
) -> None:
...
Note: All protocol interfaces use the -Protocol suffix (added 2025-12) to prevent name collisions with implementing classes and make protocols instantly recognizable.
Consequences¶
Advantages¶
- Explicitness: Constructor signature documents exactly what a component needs
- Testability: Tests can provide minimal implementations for only required protocols
- No hidden dependencies: No need to inspect composite definitions
- Flexibility: Each component declares precisely what it requires
Disadvantages¶
- Verbose constructors: Some components have 10-17 parameters
- Repetitive declarations: Common protocols repeated across components
Why Not Composite Protocols for Internal Components¶
- Hidden dependencies are an anti-pattern: With explicit injection, dependencies are visible at a glance
- No significant testing benefit: Explicit protocols already enable focused mocking
- Marginal readability improvement: Modern IDEs handle long parameter lists well
- Maintenance overhead: Composite protocols add another layer of types to synchronize
Alternatives Considered¶
1. DeviceDependencies Composite Protocol¶
Rejected: Would hide 16 dependencies in a single parameter. Trades explicitness for brevity without clear benefit.
2. Grouped Composite Protocols (CoreDependencies, EventDependencies, etc.)¶
Rejected: Adds maintenance overhead. Components would need to understand multiple composite definitions instead of individual protocols.
3. Builder Pattern for Complex Objects¶
Rejected: More complex than explicit injection. Would require additional classes and methods.
References¶
aiohomematic/interfaces/client.py- ClientDependencies (the exception)aiohomematic/model/device.py- Device with explicit injectiondocs/architecture.md- "Protocol Injection: Explicit over Composite" section
Created: 2025-12-10 Author: Architecture Review