Firstly, decide if using an event-based paradigm for the application makes sense. Event-driven systems are very useful if they meet the following characteristics:
On the other hand, the following are not suitable for (OSGi) event-driven systems:
The first step in designing an event-driven system is to create components out of the parts of the application that need to talk to each other. This might correspond with the natural boundary of OSGi bundles, or it may be more fine-grained. There may be other boundaries—such as package boundaries or Declarative Services components—that more naturally represent the components in the application.
Once the components are known, it becomes easier to track the relationships between them, including what the messages are that the various components will need to send to one another to work.
For each of the components, there should be one or more input events, and one or more output events (or other side-effect changes). These should be represented as entry and exit points of the components, with a separate input for each type of event that might flow in.
For each of the input and output channels of the components, the main purpose needs to be identified. In the first iteration, this can be as simple as a noun (such as "mouse event" or "mail message"). Subsequent iterations will fill out details on the channels that get passed.
The result of this should be a high-level event diagram of the system. It may not be as detailed or object specific as an object interaction diagram, but it should show the graph of input events, followed by the directed triggers that could flow from them. For example, an incoming mail message might trigger a mail processing script, which in turn fires more events to send auto-response mails or log the message to a database.
Identify whether the channels are firing an event for the purpose of causing a downstream event to occur, or whether they may be firing events for informational purposes (such as logging). Having events fired at different points in a life cycle means that it is easier to add additional functionality afterwards.
For each of the events that are sent, there may be zero or more properties containing additional information regarding the event. In the case of an incoming mail message, this could include the sender of the e-mail, the subject, the time the mail was sent, the importance, and of course the e-mail body as well.
The first iteration of the properties is likely to be a rough cut, and will evolve over time. As the event system is fleshed out, it may be necessary to record additional details that weren't captured in the first place. This may include things such as the time zone of the sender, or what hosts it hopped through to be delivered. The flexibility of the event pattern is that it's easy to evolve by adding additional information in subsequent releases and clients who do not need to know this information can simply ignore it.
Once the channels and event properties have been decided, the next step is to map these to topics so they can be used in EventAdmin
. Topics are represented in slash-separated format, and this is important because the wildcard character *
can be used to subsume additional levels in the topic hierarchy.
Typically, the event hierarchy is based on a reverse domain name style prefix. This allows events produced by one organization to not conflict with other events when installed into the same runtime. In the case of OSGi bundles, it is very often the case that the event topic prefix will be a variation on the bundle name itself.
The topic may then be further segregated by the sub-channel, depending on what level of granularity is needed. In the E4 model, the topics for items changing in the workspace model begin with org/eclipse/e4/ui/model/
and then continue with a type such as commands
or application
.
Since topics can be matched with wildcards, it may make sense to add another name segment for an event channel (such as application/ApplicationElement/*
instead of just application/ApplicationElement
) as this will permit future partitioning of the event space. A terminal leaf node cannot be split down into more children, whereas a segment can have more segments added afterwards. This was a pattern identified by the E4 platform, which initially just used the terminal node but then subsequently switched to a more partitioned space so that changes in individual attributes could be nominated using a common prefix.
One advantage of an event-driven system is that it is very easy to test in isolation. Besides having events driven by the EventAdmin
specification directly, it is also possible to simulate the arrival of an event by calling the method directly. It is thus possible to test individual components by setting up listeners looking for output events and simulating the incoming events.
This also helps to test the component in a black box manner. Provided that the events delivered are well formed, and the events generated have the correct data, it is possible to show that the component is operating as expected.
It may be necessary to set up other mock services, event sources, or event sinks in order to test the functionality of that component, but the principle of segregated components making it easy to test are still important.
Because event-based systems are inherently loosely typed, it is necessary to define both the values of event topics and the schema of those events in an external location. This may be part of the project's documentation, or there may be other systems that record this information externally to the project or with schemas such as RelaxNG (though that is more suited for XML documents).
Changing the event's properties or modifying the event's topics will not be picked up by a static compiler. This results in a higher testing requirement being placed on the system itself, but also gives it additional flexibility for being able to respond to changes in the future.
When the version of the API changes, it may be necessary to implement version numbering information in the payload of the event itself. This can be used to communicate the state of the API to clients at any time, and if backward compatible changes are required, then these can be brought into play. It may also be possible for the client and server to agree on the version number to use, even if it means degrading to a lower version.
It may be desirable to store the version number as a single integer, or it may be a pair or triplet of numbers. Whatever value is chosen, it should be treated as a semantic version, with major digits indicating a backwardly incompatible change, and minor versions being backwardly compatible but with potentially new features being added.
Servers (or event sources) hardly ever get rolled back, so typically these numbers will be monotonically increasing. It is thus usually sufficient to represent just the major number, or possibly the major and minor number as part of the API definition. It is usually an error to include the micro/patch numbers in the public part of the API as this binds the client too tightly to the version of the API in use. The main reason for exposing the minor version is in case a client is implemented to selectively enable additions for newer functions; this comes in handy if the same client is exposed to both old and new versions for an extended period of time.
With a known version and a known set of event properties and types, it is possible to document changes and upgrade the API when necessary to add new features or to document backward compatibility issues.
Since the
Event
objects are an in-memory representation, and the map that is passed can store objects of any type, it is possible to put any kind of object into the event object itself. For example, the Bundle
can be embedded in the Event
object or UI-specific components such as Color
or even open InputStream
objects.
The OSGi specification suggests limiting the use of the Event
properties to the set of primitive values such as int
(or their object counterparts such as Integer
) along with String
and single-dimensional arrays of the same. In other words, although it is possible to store URL
instances in the map directly, it is recommended that it be stored as a String
and then converted on the client into a URL
object.
The reason for this recommendation is that while EventAdmin
is a system designed for use within a single VM, it is not limited to being used in a single VM. In fact, in conjunction with OSGi Remote Services, it is possible to set up a distributed EventAdmin
fabric, where events generated on one node get transported over the network and then handled on a remote node. To make this possible, all the values in the Event
object need to be Serializable
, and because it may be the case that the events are processed in a different language (such as JavaScript or C), having a standard set of known datatypes facilitates that translation.
Similarly, objects placed into an OSGi Event
should be immutable. If an object placed into an Event
is not immutable (such as the old Date
class), then it would be possible to dispatch an event, and later modify its contents before a consumer has time to process the original value. No runtime checks are made by EventAdmin
, but violating this rule can lead to surprises.
Designing an event-driven system looks very similar to designing a message-driven system using an API such as Java Messaging Service (JMS). Both follow a similar paradigm for being able to build an application; the system is modeled as a set of state changes triggered by incoming events (messages), resulting in either system updates or subsequent events (messages) being fired.
The following differences are worth observing between the event-driven and message-driven systems:
EventAdmin
, there is no separate standalone broker process, although the EventAdmin
service acts like a centralized in-process broker.EventAdmin
.EventAdmin
uses for Event
delivery. JMS also provides a point-to-point mechanism (called queues), which ensures that only one subscriber gets each message. Queues are often used to allow scaling by adding additional workers. The EventAdmin
service does not have a concept of queues or single event delivery.EventAdmin
only has transient support; if the OSGi runtime crashes, then all in-flight events are lost. For unimportant states (such as which button in a GUI was being clicked at the time), this may not be an issue, but for data-specific processing, this may be a problem.EventAdmin
doesn't officially support other languages, but leaves it open to implementors of the frameworks to support them if desired. In practice, it is fairly easy to hook up a set of events to something like JSON messages, which are becoming the de facto interchange format between systems as well as between clients and browsers.The advice is to use an in-memory system such as EventAdmin
where the state of the workflow is transient or does not need transactional persistence, and use a more heavy-weight solution such as JMS when queues or transactional storage is required.