[Back to Main Page]

Dispatcher
Behavioral

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:

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

[Back to Main Page]

Last modified: Fri Jul 30 17:09:59 PDT 2004