Intent
Dynamically selects between a set of behaviors
based on an identifier. Dispatchers provide a way to
implement and include behaviors independently of
the component that uses them.
Example TinyOS Components
AMStandard, AMPromiscuous, MateEngineM
Motivation
Sometimes we need an application to have a
collection of operations it performs in response to
input. The details of these operations are
independent from each other, and not important to
the component that invokes them. Additionally, it's
useful to be able to easily extend the set of
supported operations. For example, a node can
receive many kinds of active messages, some of which
it may need to respond to. The component that
signals the arrival of a message (the networking
stack) shouldn't need to know the processing an
application performs on reception.
One way to achieve this is with a series of
conditionals or a switch statement: "if the data is
of type T1, do this; else if it is of type
T2, do this." However, this binds the
implementation of the operations into the component
that chooses which operation to perform.
The implementation of the operations can be
decoupled from choosing which operation to perform
by using an interface. A component implements an operation by
providing the interface. The choosing component,
instead of embedding the implementation in its
conditional statement, calls one of a set of
interfaces: "if the data is of type T1, call
interface I1; else if it is of type T2,
call interface
I2. However, this couples the set of
supported operations to the implementation of the
choosing component. Adding a new operation requires
modifying the choosing component.
A more flexible approach is for the choosing
component to be a dispatcher and invoke
operations using a paramterized interface. The set
of supported operations is independent of the
dispatcher's implementation, as they are wired to
it.
For example, the TinyOS networking stack
dispatches packet reception based on AM message
type. The dispatcher (AMStandard or
AMPromiscuous) doesn't know which
message types the application processes, or what
processing it performs. Similarly, the core component
of the Maté virtual machine,
MateEngineM, doesn't know its own
instruction set: it dispatches to components that
implement instructions based on their opcode value.
Depending on the nature of the operation, it could either
be a command or an event. A dispatcher provides or uses
the appropriate interface depending on the direction of
the interaction: AMStandard provides ReceiveMsg, but
MateEngineM uses MateBytecode.
Applicability
Use the dispatcher pattern when:
- A component supports a set of operations,
but other components need to be able to customize what is in
the set.
- A primitive integer type can identify which operation
to perform.
- The operations can all be implemented in terms
of a single interface.
Structure
Participants
- Dispatcher: invokes its parameterized interface based on an integer type.
- Operation: implements the desired functionality and wires it to the dispatcher.
Collaborations
The Dispatcher calls (or signals) its Operation
components based on the dispatch key. The Operations
are responsible for maintaining any state they
require. Often, an operation is not responsible for
wiring itself to a dispatcher; instead, a separate
configuration wires the operations to include them
in the application.
Consequences
By leaving the selection of supported operations
to nesC wirings, the dispatcher component's
implementation remains independent of
them. Additionally, if the dispatcher component
exports its dispatching interfaces, then services
can wire additional operations; adding services will
also increased the set of supported operations.
However, if a dispatcher allows other components
to add operations, easily seeing the full set of
supported operations can require looking at many
files. Unless the operation interface is designed
carefully, inadvertent fanout on operation calls can
occur. Tools such as nesdoc can make
finding these sorts of problems easier by displaying
a full wiring graph.
The key aspects of the dispatcher pattern are:
- It allows you to easily extend or modify
the functionality an application supports.
Including an operation requires a single wiring to
the dispatcher's parameterized interface.
- It allows the elements of functionality to
be independently implemented and to be
re-used. Because each operation is implemented
in a component, it can be easily re-used across
many applications. Some tightly coupled operations
may requiring sharing a single implementing
component, but generally keeping implementations
separate can also simplify testing, as the
components will be smaller, simpler, and easier to
pinpoint faults in.
- It requires the elements of functionality
to all follow a uniform interface. The
dispatcher is usually not well suited to
operations that have a wide range of semantics. As
all implementations have to meet the same
interface, this situation leads to the interface
being overly general. An overly general interface
pushes error checks from compile-time to run-time;
an operation implementor forgetting a run-time
parameter check can cause the system to fail in a
hard to diagnose fashion.
Implementation
Typically, a dispatcher just provides or uses a
parameterized interface. Wiring to this interface
adds an operation. As the set of operations is
intended to be customizable (and therefore not
always completely full), the parameterized interface
will almost always have default handlers.
The main implementation decision when using a
dispatcher is the interface it supports. Services
(such as a networking stack) are likely to
provide an interface, while application
level components with customizable functionality
(such as TinyDB or Maté) are more likely to
use an interface.
Sample Code
Consider the TinyOS Active Messages
abstraction. There are 256 types of AM packets, and
a network service may use one or more of them to
exchange data. The set of AM types a mote responds
to depends on what services it supports. Including a
service should automatically extend the set of AM
types an application responds to. The specification
of GenericComm in TinyOS 1.x includes:
configuration GenericComm {
provides interface ReceiveMsg[uint8_t id];
...
}
Components that receive active messages wire to
GenericComm:
configuration Service {}
implementation {
components ServiceM, GenericComm;
ServiceM.BeaconReceive -> GenericComm.ReceiveMsg[AM_SERVICEBEACONMSG];
}
When the networking stack receives a packet, it
signals the parameterized interface based on the
type field in the AM header:
event TOS_MsgPtr receive(TOS_MsgPtr packet) {
uint8_t type = packet->type;
return signal ReceiveMsg.receive[type](packet);
}
Alternatively, consider the Maté virtual
machine, which allows users to customize its
instruction set. A component implements an
instruction by providing the MateBytecode
interface:
interface MateBytecode {
command result_t execute(MateBytecode instr, MateContext* context);
command uint8_t byteWidth();
}
The core interpreter component,
MateEngineM, fetches an opcode from the
program, then dispatches on the opcode to the
component that implements the corresponding
instruction:
module MateEngineM {
uses interface MateBytecode as BytecodeImpls[MateOpcode opcode];
...
}
implementation {
...
static inline result_t computeInstruction(MateContext* context) {
MateOpcode instr;
...
// Fetch instr from context
...
context->pc += call BytecodeImpls.byteLength[instr]();
call BytecodeImpls.execute[instr](instr, context);
return SUCCESS;
}
}
Known Uses
The Active Messages networking layer
(AMStandard, AMPromiscuous) uses a
dispatcher for packet reception. It also provides a
parameterized packet sending interface, so services
can easily match packet sends to reception
handlers.
The Maté virtual machine uses a dispatcher
to allow easy cusomization of instruction sets.
Related Patterns
A dispatcher is the basis for many other
patterns. The service instance pattern uses a
dispatcher for each instance of a service, but also
counts the number of instances for state management
purposes.
Dispatchers depend on having a key which can
identify an operation: dispatchers almost always
require a namespace pattern such as global
keyset, local keyset, or a keyset
mapper.
|