9. Replace Handler with AttributeIO/AttributeIORef Pattern#
Date: 2025-10-03
Related: PR #218
Status#
Accepted
Context#
Currently the Handler pattern is used to manage attribute I/O operations. This design has several classes:
AttrHandlerRpreviouslyUpdater- Protocol for reading/updating attribute valuesAttrHandlerWpreviouslySender- Protocol for writing/setting attribute valuesAttrHandlerRWpreviouslyHandler- Combined read-write handlerSimpleAttrHandler- Basic implementation for internal parameters
There are a few limitations with this architecture:
Handler Instance per Attribute: Every attribute needs its own Handler instance because that’s where the specification connecting the attribute to a unique resource is defined. This means redundant Handler instances when multiple attributes use the same I/O pattern.
Circular Reference Loop:
Controller → Attributes (controller owns attributes)
Attributes → Handlers (each attribute has a handler)
Handlers → Controller (handlers need controller reference to communicate with device)
Tight Coupling to Controllers: Handlers need direct references to Controllers, coupling I/O logic to the controller structure rather than just to the underlying connections (e.g., hardware interfaces, network connections)
The system needs a more flexible way to:
Break the circular dependency chain
Separate resource specification from I/O behavior
Decision#
Replace the Handler pattern with a two-component system: AttributeIO for behavior and AttributeIORef for configuration.
Key architectural changes:
AttributeIORef - Lightweight resource specification per-attribute:
Lightweight dataclass specifying resource connection details
Containers static fields like resource names, register addresses, etc.
Attributes have unique AttributeIORef instances
Dynamically connected to a single AttributeIO instance at runtime
AttributeIO - Shared I/O behavior:
Single instance per Controller handles multiple Attributes
Generic class parameterized by data type
Tand reference typeAttributeIORefTAccesses the AttributeIORef from the attribute to know which resource to access
Only needs to know about connections, not controllers
Parameterized Attributes:
Attributes are now parameterized with
AttributeIOReftypesAttrR[T, AttributeIORefT]- Read-only attribute with typed I/O referenceAttrRW[T, AttributeIORefT]- Read-write attribute with typed I/O referenceType system and programmatic validation ensures matching between AttributeIO and AttributeIORef
Initialization Validation:
Controller validates at initialization that it has exactly one AttributeIO to handle each Attribute
Ensures all attributes are properly connected before serving the Controller API
Consequences#
Migration Impact#
Before:
# Temperature controller that communicates via TCP/IP
class TempControllerHandler(AttrHandlerRW):
def __init__(self, register_name: str, controller: TempController):
self.register_name = register_name
self.connection = connection
self.update_period = 0.2
self.controller = controller
async def initialise(self, controller):
self.controller_ref = controller # Creates circular dependency
async def update(self, attr):
# Each attribute needs its own handler instance
query = f"{self.register_name}?"
response = await self.connection.send_query(f"{query}\r\n")
value = float(response.strip())
await attr.set(value)
async def put(self, attr, value):
command = f"{self.register_name}={value}"
await self.connection.send_command(f"{command}\r\n")
controller = TempController()
ramp_rate = AttrRW(Float(), handler=TempControllerHandler("R", controller))
power = AttrR(Float(), handler=TempControllerHandler("P", controller))
setpoint = AttrRW(Float(), handler=TempControllerHandler("S", controller))
After:
@dataclass
class TempControllerIORef(AttributeIORef):
name: str # Register name like "R", "P", "S"
update_period: float = 0.2
class TempControllerAttributeIO(AttributeIO[float, TempControllerIORef]):
def __init__(self, connection: IPConnection):
self.connection = connection
async def update(self, attr: AttrR[float, TempControllerIORef]) -> None:
query = f"{attr.io_ref.name}?"
response = await self.connection.send_query(f"{query}\r\n")
value = float(response.strip())
await attr.update(value)
async def send(self, attr: AttrRW[float, TempControllerIORef], value: float) -> None:
command = f"{attr.io_ref.name}={value}"
await self.connection.send_command(f"{command}\r\n")
connection = IPConnection()
temp_io = TempControllerAttributeIO(connection)
ramp_rate = AttrRW(Float(), io_ref=TempControllerIORef(name="R"))
power = AttrR(Float(), io_ref=TempControllerIORef(name="P"))
setpoint = AttrRW(Float(), io_ref=TempControllerIORef(name="S"))