The Game Panel specification

ADR-0002: Dependencies select their instance through parameter attributes

Context

A dependency’s type often does not determine which instance it needs. Several implementations or instances can share one interface: every database connection is a Connection. A parameter typed against that interface says what it needs, but not which one, so the choice has to be expressed somewhere else.

What is known about the instances varies. Sometimes every concrete, and what maps to it, is known when the panel boots. Sometimes the concrete is only decided at runtime, from configuration or other state, by the subsystem that owns the instances.

The panel is dynamic and built for one purpose, and third-party modules consume what its subsystems provide. A module cannot be expected to know the exact final concrete it needs, especially when configuration can change it.

Decision

A dependency says which instance it needs with an attribute on its own parameter, and what acts on that attribute depends on what is known. Where every concrete and its mapping are known at boot, a Named attribute maps a string, or a qualifier attribute maps its class, to a binding registered for the type, and a factory on that binding produces the instance where construction needs to be dynamic. Where the concrete depends on configuration or other runtime state, a resolvable attribute is paired with a resolver belonging to the subsystem that owns the instances, and the resolver decides the instance entirely, with nothing about it known to the bindings. The consuming class plays no part in the choice, and the correct bindings and resolvers are trusted to exist.

Alternatives

Contextual binding by consuming class, as in Laravel’s container, where registration states which concrete a given consuming class receives for a dependency. It requires knowing the exact final concrete for each consumer, which a third-party module cannot be expected to know, particularly when configuration can change it. In a system this dynamic and built for one purpose, it would add a great deal of complication, where the correct bindings and resolution can instead be trusted to exist.

Contextual attributes, as in Laravel’s container. Their shape comes from being added to an existing system under a strict backwards compatibility policy. A contextual attribute is either paired with a resolver closure registered for it, or carries a resolve() method, which receives the attribute instance as a redundant argument. The panel’s author built the original implementation of the idea and co-authored the one Laravel merged, which shipped both forms together. A resolvable attribute paired with a resolver class is a more structured implementation of the same idea, without the constraint of fitting an existing system.

No other alternatives were weighed.

Consequences

Easier:

Harder:

Constrained:

Sources