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:

  • AttrHandlerR previously Updater - Protocol for reading/updating attribute values

  • AttrHandlerW previously Sender - Protocol for writing/setting attribute values

  • AttrHandlerRW previously Handler - Combined read-write handler

  • SimpleAttrHandler - Basic implementation for internal parameters

There are a few limitations with this architecture:

  1. 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.

  2. 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)

  3. 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:

  1. 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

  2. AttributeIO - Shared I/O behavior:

    • Single instance per Controller handles multiple Attributes

    • Generic class parameterized by data type T and reference type AttributeIORefT

    • Accesses the AttributeIORef from the attribute to know which resource to access

    • Only needs to know about connections, not controllers

  3. Parameterized Attributes:

    • Attributes are now parameterized with AttributeIORef types

    • AttrR[T, AttributeIORefT] - Read-only attribute with typed I/O reference

    • AttrRW[T, AttributeIORefT] - Read-write attribute with typed I/O reference

    • Type system and programmatic validation ensures matching between AttributeIO and AttributeIORef

  4. 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"))