Introduction
The Switch and Providers is an architectural pattern for communications based on Publish/Subscribe messaging, extended with the provision for custom Providers. The central Publish/Subscribe messaging system is known as the Switch. Providers, or interface adapters which generate and consume messages, are implemented to interface with a specific protocol or technology, and connect it with the Switch.
The Switch and Providers pattern is intended primarily to address the problem of providing external interfaces to a software system, the specifications for which may change over time. Internal software components (those which are within the primary app-domain) should use the Switch directly (preferably via the interface and Dependency Injection), and external components (those in remote app-domains) communicate through the Providers. Although the pattern addresses the problem of external interfaces, it is as well suited for adapting interfaces between internal components.
The pattern is extremely well suited for decoupling the details of the methods for communication between components, such that the components’ implementation details are fully isolated from the implementation of the communication methods. For example, a component which provides an external interface based on UDP packets may abstract the handling of the packets as a Provider that creates messages based on the data from the packets; the messages are then passed to the Switch, which distributes the message to the subscribed components.
Design
Switch
The Switch is the central component in the Switch and Providers pattern. Although the critical concepts of this pattern do not require a specific implementation of the Switch, the implementation described here is considered as the basis for the later discussion of the pattern’s benefits.
The subscription mechanism in the Switch is implemented as a tree in which each node may have any number of children. Each node contains a single unique rule, a collection of subscribers to notify, and a collection of children.
When a message is sent using the Switch, nodes in the tree are processed to identify whether the message passes the test for that node’s rule. If the test passes, then the handlers subscribed for notification are kept to invoke later and the child nodes are processed. If the test fails, then the handlers and children are ignored. After the tree has been searched, the message is delivered by invoking the handlers that were identified during the search.
Rules
The subscription mechanism of the Switch is dependent upon rules and rule chains, and a subscription is defined by a handler delegate and a rule chain, which determines whether or not the handler should be notified for any particular message.
Rules are simple tests that inspect messages and determine whether the message passes or fails. Rules should be created with simple tests of minimal instructions. A useful rule, for example, is one which tests the type of a message against a provided type of interest. If a subscription is concerned with all messages of some type AddMessage, then the IsTypeRule rule would be ideal for identifying those messages.
A rule chain is a set of rules which are tested in order to specify a more complex test based on simple parts. In terms of defining subscriptions, rule chains are ordered lists, arrays, or other collections of rules, all of which must pass for the subscribing handler to be notified with the message. In terms of the subscription tree, a rule chain exists by identifying the rule at each node in the path to a subscribed handler. In the following example tree, a subscription defined with a rule chain A-C would place the associated handler at the node containing rule C.
Rule chains for each subscription should be chosen such that the simplest rule which eliminates the largest possible unwanted sets of messages is first in the chain, and subsequent rules are similarly ordered. This method for selecting the rule chains will ensure that the subscription tree performs optimally.
Providers
Providers, which are built upon the Adapter pattern, are implemented at each point where a custom interface is desired. In the case of an application which receives UDP packets containing data which can be reconstructed as messages, a custom Provider is responsible for assembling the data into messages that are then handed off to the Switch.
For example, when it is desirable to provide a custom interface such as access to the application via WS-* interfaces, the Provider is an adapter for the RPC based interface to the message based interface. If that Web Service API contains an Add
method, then the Provider will implement the Add
method by creating an Add message and delivering it via the Switch.
A generic Provider that simply exposes the interface of the Switch as a WS-* service might be used to allow a client component to interact directly with the Switch. Such a Provider may be useful within an application, during testing, and during debugging. The Provider would be used to inject messages directly into the Switch and receive messages based on subscriptions in the same way as the internal components. If it becomes necessary to move an existing component outside of the application’s primary app-domain, then the provider implementing the Switch interface may be injected in place of the actual Switch.
Performance
One of the goals of the Switch and Providers pattern is to provide optimal performance. Providers should create messages from incoming data in an optimal fashion, and the quality of their implementations will determine their impact on performance. The Switch passes messages by reference, and therefore does not introduce any copy operation overhead. The Switch provides a minimal amount of overhead when sending messages due to the rule testing process. The use of properly selected rule chains can exponentially reduce the overhead of selecting subscribers when compared to a set of single rule subscriptions which match complex tests.
In cases where rule chains have been carefully selected, the complexity will approach O(log n), where n is the total number of nodes, and in the worst case, delivery of a message through the switch is O(n) complexity. Subscribe and Unsubscribe operations are of similar complexity.
Performance sensitive components may choose to use an instance of the Switch devoted to their purpose.
Maintainability
The Switch and Providers pattern promotes the decoupling of components in a software system, and improves the ability to modify and replace components without having to undertake large refactoring initiatives. Additional components can be plugged into the system, and components which generate unused messages may not require that those messages be handled.
When coupled with Dependency Injection, instances of the Switch and instances of Providers may be configured dynamically to match application behavior and performance requirements, as well as to facilitate improved testing flexibility.
Test Automation
The potential for use during testing is among the useful benefits of adapting a Switch and Providers architecture. A subscription can be added to the Switch that serializes messages to a file as they come in, so that they can be replayed later by a custom provider which reads the file and delivers the messages. Using the capabilities of the Switch and Provider in testing an application provides a convenient method for exercising code that would otherwise be very difficult to activate without adding custom code to the application to support the testing.
An example of a testing scenario using the Switch and Providers pattern is replacing a driver (which sends messages through the Switch containing incoming data) with a scripted provider, which sends various combinations of messages for ideal and failure cases that would be difficult to reproduce with the normal driver.
History
- 2009-06-01 - Initial article version.
- 2009-07-27 - Updated for publication.
- 2009-07-28 - Added ExampleApp project to illustrate pattern usage.