Event Notifier, a Pattern for Event NotificationAn object behavioral design pattern for general-purpose type-based event notification.In our complex world, events are constantly occurring. Any one person is only interested in a very small subset of all these events, so humans have worked out ways of getting just the information of interest, which works to a degree. We may periodically check to see if the event has occurred, or we ask someone to notify us when the event occurs. Often there is more than one source of a particular type of event such as disaster related events, but we do not typically care about who notifies us, just that the event has occurred. Ideally, we would subscribe to just those types of events in which we are interested, and be notified of them when they occur. Event notification is a useful communication paradigm in computing systems as in real life. This article documents general event notification in the form of a design pattern, which provides a useful way of exposing readers to the important concepts and issues involved in event notification, and provides a language-neutral pattern for implementing event notification in a variety of scenarios. Concurrently, we discuss design issues using examples as appropriate to demonstrate effective use of event notification. Diagrams use the Unified Modeling Notation (UML) [1]. IntentEnable components to react to the occurrence of particular events in other components without knowledge of one another, while allowing dynamic participation of components and dynamic introduction of new kinds of events.Also Known AsDispatcher, Decoupler, Publish-SubscribeMotivationTo understand the need for Event Notifier, we will take a simple example of a network management system, and implement it in a simplistic way. Then we will look at the problems with this approach, and incrementally show how we might solve them using Event Notifier.Consider a large network of distributed components, such as
computers, hubs, routers, software programs, and so forth. We
wish to monitor and manage these components from a central
location. We will refer to the components being managed as
managed objects. Problems are typically infrequent and
unpredictable, but when they do occur we wish to be notified
without having to constantly poll the managed objects. The
notification may appear on a management system such as
a central console, pager, or electronic mail reader. For our
example, suppose we have both a console and a paging system.
In the simplistic implementation, shown in Figure 1a, a
managed object must send notification of problems to both the
console and the paging system. If we later wish to change the
interface to the console or paging system, or add an
electronic mail system, every managed object must be modified.
Apart from being unscalable, this approach is very error
prone, since each managed object must essentially duplicate
the same sequence for notification, making consistency
difficult to achieve. Encapsulating the notification behavior
in a common superclass only partially mitigates the
problem.
In a system of any size, we would like to minimize the number of dependencies and interconnections between objects to keep the system from becoming brittle and hard to change. The more dependencies there are, the more a change in any particular point in the system propagates to other points in the system. The simplistic approach requires each managed object to maintain a reference to each management system. The number of these references increases geometrically with the number of managed objects and management systems. A better approach that keeps this to a linear increase is to have a mediator that encapsulates and coordinates the communication, as shown in Figure 1b. To report a problem, each managed object notifies the mediator, which in turn notifies the console and paging system as appropriate. Now, to modify an existing management system or add a new one, such as an electronic mail system, we need only to modify the mediator. This is a use of the Mediator design pattern [2]. An alternative approach to solving the dependency problem is to introduce the notion of notification to the system in a generic way, using the Observer design pattern [2]. As shown in Figure 1c, each managed object implements a common "subject" interface which allows interested observers such as the paging system and console to register interest in its events. When a managed object has a problem to report, it traverses its internal list of interested observers, calling each in turn. Unlike in the simplistic approach, the managed object does not need to know a priori which systems to notify; the management systems themselves are responsible for dynamically registering with the managed objects for notification. However, we have introduced a new problem: now the management systems need to know about each managed object, to register interest in them. If anything, this is worse, because there may be an arbitrarily large number of managed objects on a large network, and they may come and go frequently. We need a mechanism that requires neither the managed objects nor the management system to have a priori knowledge of each other, but to be able to still communicate problems. The Observer approach has the benefit of allowing more dynamic behavior than the Mediator approach: new management systems may be added without impacting the rest of the system, although we cannot do the same with managed objects. It also does not require the presence of an omniscient mediator that understands and controls the flow of interactions: behavior that naturally fits in the managed objects or management systems may stay there. However, each subject has the burden of maintaining a list of observers and calling them as necessary, which the mediator approach nicely centralizes. It is possible to implement the system in a way that combines the benefits of both the Mediator and Observer approaches, as shown in Figure 1d. Like in the Mediator approach, we have a central event service that mediates notification, so that managed objects and management systems do not need to know about each other. Like in the Observer approach, a registration system allows us to add and remove observers (called subscribers) dynamically. Unlike the Observer approach, however, this functionality is centralized in the event service, relieving subjects (called publishers) of this burden. We give the name Event Notifier to this best-of-both-worlds approach. Event Notifier derives many of its benefits from the fact that subscribers only know about events, not about publishers. For example, routers and hubs might both generate events of the same type, say FaultEvent, when problems occur. In an Observer implementation, each management system needs to know which managed objects generate fault events and register with each. The same is essentially true of Mediator, except that the mediator encapsulates this knowledge. However, using Event Notifier, a management system needs only to register interest in the FaultEvent type to get all fault events, regardless of who publishes them. ApplicabilityUse the Event Notifier pattern in any of the following situations:
StructureThe class diagram in Figure 2 shows the structure of the Event Notifier pattern. For more detail on the purpose of each class and the interactions between them, see the next two sections.
The EventService class contains the aggregations filters, subscribers, and eventTypes. Although not readily apparent from the structure, corresponding elements from these aggregations comprise a tuple. Each tuple corresponds to the parameters passed to a single call to subscribe. ParticipantsThis section describes the responsibilities of the classes shown in the Event Notifier structure.
CollaborationsThe collaboration diagram in Figure 3 shows the typical sequence of interactions between participating objects.
The subscriber invokes the subscribe method on the event service (1) specifying the event type it is interested in and passes a reference to itself (or possibly another object) and a filter. The eventType argument represents the type of the event. When an event occurs, the publisher invokes the publish method on the event service (2) passing an event object. The event service determines which subscribers are interested in this event, and for each of them applies the filter (3) provided by the subscriber. If no filter is provided, or if application of the filter results in true, then the event service invokes the inform method of the subscriber (4), passing the event as an argument. Notice that all publication and subscription is done through the event service. The event service maintains all information about which subscribers are interested in which events, so that publishers and subscribers need not be aware of each other. Moreover, anyone may publish or subscribe to events using the well-defined interface provided by the event service without having to implement any special logic to handle interaction with other entities for which it has no other reason to communicate. ConsequencesThis section discusses the results and tradeoffs associated with the use of Event Notifier.Subscription is based on event type rather than publisher. In some event notification models, a subscriber registers interest in receiving events from a particular publisher or type of publisher. This is useful when subscribers have knowledge of the publisher. When processing events from a graphical user interface (GUI), for example, the event handler knows about the individual controls that can publish events. In Event Notifier, a subscriber registers interest based solely on event type, without regard to publisher. This is more suitable for services in a distributed environment that are not coupled and may cooperate using an enterprise-wide event hierarchy without any knowledge of each other at compile time or run time. For those cases where subscribers are interested in events from a particular publisher, include an event attribute that identifies the source, and define a filter that uses this attribute to discard uninteresting events. Subscribing to an event type automatically subscribes to all its subtypes. Because event types are structured in an inheritance hierarchy, when a subscriber subscribes to a particular event type, the event service notifies it of all events of that type or any subtype in the hierarchy. This enables subscribers to specify interest in events as broadly or narrowly as necessary. This feature is easier to implement in languages like Java and Smalltalk that provide rich run-time type information. Events can be filtered. Filtering allows a subscriber to programmatically select the events of interest. By specifying an event type at subscription time, the subscriber narrows its interest to a certain class of events. An explicit filter allows further selection of events prior to notification, based on custom criteria such as the values of certain event attributes. For example, a filter might use an event source attribute to restrict events to those from a certain source. In the network management example described earlier, regional monitoring centers could use filters to limit events to those received from the regions of interest. Subscribers and publishers can vary independently. Event subscribers and publishers do not have knowledge of each other and can vary independently. The understanding between the two is via agreement on a set of legal event types, the semantics of what an event means, and event data associated with an event type. There can be multiple publishers and subscribers for a given kind of event. Some patterns for event notification require a subscriber to "know" each publisher of an event type. This can be difficult or impossible if the publishers of an event cannot be determined at compile time. Subscribers can be interested in events for a limited duration. They can subscribe and unsubscribe at will. Support for dynamic registration and unregistration allows for this freedom. Subscribers and publishers may be transient. A new subscriber or publisher can appear or disappear without impacting other components of the system. This is particularly important because it allows relocation of services in a distributed environment. Event types can be introduced with minimal impact. In languages like Java that support dynamic loading of classes, one can add new event types to an existing application dynamically. Existing subscribers will receive events of the new event type if they are already subscribing to a supertype. One can dynamically add publishers for the new event type without having to rebuild the entire application. In environments without support for dynamic class loading, one may need to rebuild the application to add new event types, but no changes to subscriber or event service code are required. Event Notifier makes a tradeoff between genericity and static type safety. In large distributed systems, change can entail recompilation and redistribution of many components to multiple locations in a network. It is imperative to design such systems to be resilient to change, minimizing the impact of change to the smallest number of components possible. Using Event Notifier helps accomplish this goal by keeping components decoupled from the beginning and allowing for change by keeping interfaces generic in terms of the types they deal with. However, this genericity and extensibility comes at the cost of some type safety provided by compile-time type checking. If the same event can emanate from one of many sources then this flexibility pays off. The Reclaiming Type Safety subsection under Implementation describes one way to mitigate the lack of static type checking for events. The event service could be a single point of failure or bottleneck. This could be the case if a single event service brokers all events. However, we can mitigate these and other problems associated with centralization by distributing the work without changing the semantics, as discussed in the Enhancing Fault Tolerance and Performance section of Implementation. ImplementationThis section discusses specific issues that one must address when implementing Event Notifier in a real situation. We divide the issues into more or less autonomous sections, discussing implementation techniques where appropriate.Accessing the Event ServiceAn event service always has a well-known point of access so that subscribers and publishers in a given environment can share the same event service. One can provide this well-known point of access using the Singleton or Abstract Factory pattern [2], or by registering the event service with a well-known naming service.Modeling EventsWhen modeling events, one must think about how to decompose communication in a system into generic messages that convey certain standard information in the form of an event object. Such an object could be of any type, but there are certain advantages to requiring event types to be subclasses of an Event base class as shown in Figure 4. One advantage is that the Event class can enforce a common interface or set of attributes on all events, such as a time stamp which captures the time of occurrence of an event. Such an event hierarchy also enables type-based subscription: subscribers can specify a non-leaf node in the event tree to be informed of all events of that type or below.
The natural way to implement an event hierarchy in an object-oriented language is by using inheritance. However, it could also be implemented using a string representation, with the levels of the hierarchy separated by colons. For example, the event types in Figure 4 might be represented as Event, Event:ManagementEvent, Event:ManagementEvent:FaultEvent, and so forth. The inheritance mechanism, not surprisingly, has several advantages. Principally, it allows an event subscriber to more easily subscribe to all events in a subtree of the type hierarchy. One subscriber may want all management events, while another may only be interested in data events. This is expressed simply by subscribing to the event at the root of the hierarchy of interest. Furthermore, since events are objects, attributes may be packaged naturally with them, so the subscriber need not invoke methods on (say) the publisher to retrieve those attributes. The inheritance mechanism also takes advantage of compile-time type checking. The compiler will not catch errors in string representations, such as a misspelled event type, but it will catch errors in class names. The advantages to the string technique are that it works in languages that are not object-oriented, and strings may be generated dynamically, while in many languages, objects in an event hierarchy must have classes that are defined at compile time. Events can have various characteristics, which may lead to
alternative ways of arranging them into a single hierarchy.
Figure 5 shows two ways to model management events. To take
advantage of type-based event notification, it is useful to
choose a model based on the kind of events in which one
anticipates subscribers having interest. If subscribers may
need to be informed of all fault events, regardless of whether
they are hardware or software faults, then modeling the events
as in Figure 5a would be appropriate. On the other hand, if
subscribers are interested in all hardware events regardless
of whether they are fault or performance events, devising an
event hierarchy as shown in Figure 5b makes
sense.
Event characteristics may appear as attributes of events instead of being modeled as types in the event hierarchy. However, this approach prevents subscription based on that characteristic. Filters can be used to achieve a similar effect, although they are less efficient and not as type safe. Identification of event source (publisher) is useful in certain situations. As alluded to earlier, when processing events from a GUI, the action taken by a subscriber in response to an event may depend on the source of the event. On a button click event, the subscriber would want to known which button was clicked. Event source could also be used to advantage in situations where one would otherwise need to specify a number of attributes that have information related to the source. In this case, storing an event source handle as an event attribute will allow a subscriber to get the information it needs about the event source from the event source directly. The tradeoff is that including the event source as an attribute leads to coupling between subscriber and publisher. Determining Event Type at Run TimeTo subscribe to events of a particular type, a subscriber passes an instance of a metaclass. When an event is published, the event service checks whether any subscribers are interested in it. Different implementation languages support the notion of a metaclass and the ability to check if an object is of a certain type to varying degrees.In Java, the Class class is like a metaclass. One can check whether an object is of a certain type by invoking the isAssignableFrom method on the type's Class object, passing the Class object for the object in question. If an instance of the class specified as an argument to isAssignableFrom could be assigned to an instance of the class on which this method is invoked, then the class specified in the argument must be the same class as, or a subclass of, the class on which the method is invoked. This is the essential mechanism for supporting type-based notification in Event Notifier. Smalltalk supports a similar mechanism as part of its Class class for determining if an object is of a certain type. C++ also provides run-time type information. To check if an event object is of a certain event type, one might think of using the dynamic_cast operator. However, this is not sufficient, since the event service only knows the event type to which to cast at run time. We would prefer to implement the event service generically, without the need to modify it each time we use it in a different environment with a different set of event types. Even in a single environment, we would prefer not to have to alter the implementation of the event service whenever we introduce a new event type, so as to promote reuse. A second alternative for C++ uses the type_info class as an approximation of a metaclass. Using the typeid operator, the event service can check whether an event is of the same type as that specified by the type_info object. This technique, however, does not fully address the requirement since the published event could be a subtype of the type in the type_info object. Dealing with this issue requires type information beyond that supported in C++. However, there are well-known ways to accomplish getting the additional type information. One technique is for each class in the event hierarchy to implement an isKindOf method. The NIH class library [3] provides a good example of how this can be done. Every NIH class has a private static member variable named classDesc that is an instance of a class named Class. This class descriptor object contains items of information about an object's class such as its name and superclass(es). Objects instantiated from NIH classes support an isKindOf method that in turn relies on information in the corresponding Class object to ascertain whether an object is of a certain type. Reclaiming Type SafetyAs mentioned in Consequences, the genericity of Event Notifier comes at the expense of some type safety. This problem manifests itself in the implementation of the subscriber's inform method and the filter's apply method. Below, we describe the problem and potential solutions in terms of the subscriber, but the same applies to the filter.The subscriber receives an event of type Event, which it must downcast to the type of event it is expecting. If a subscriber subscribes to more than one type of event, it first needs to check the event type. Even if it subscribes to only a single event type, it should first validate the event type to prevent run-time errors due to improper coding. For example, the subscriber may inadvertently have given the wrong event type at the time of subscription. In a language like Java, it is possible to eliminate the
need for each subscriber to downcast by relying on the
extensive run-time type information and the reflection API
provided by the language. For each event type to which a
concrete subscriber subscribes, it implements a type-specific
inform method that takes as an argument an event of
that type. The abstract Subscriber class implements
an inform method that takes a parameter of type
Event. When an event occurs, the event service calls
this inform method. The inform method uses
the reflection API to invoke the appropriate type-specific
inform method provided by the concrete subscriber,
passing the event as an argument. If there is no appropriate
type-specific inform method, the abstract subscriber
throws an exception. Alternatively, this error could be caught
even earlier by the event service at subscription time. Figure
6 illustrates a paging system that subscribes to two event
types and provides type-specific inform methods to
handle them. Note that the concrete subscriber must not
override the inform method that takes an
Event type as a parameter, since this will neutralize
the dispatching mechanism described.
Advertisement and DiscoveryIn some situations, it may be useful for the subscriber to be able to query the event service as to which event types are currently available. For example, a management console might present a list of all possible event types to the user, allow the user to pick those of interest, and then subscribe to those events on the user's behalf.Support for this capability requires a simple modification to Event Notifier. Publishers could explicitly advertise the types of events they can publish with the event service by calling an advertise method, and the event service could provide an interface that allows subscribers to discover the list of advertised events. Distributed Event NotificationThe Event Notifier pattern is well suited to a distributed implementation, where publishers, subscribers, and the event service itself reside on distinct machines. A distributed system can realize additional benefits by using Event Notifier in conjunction with the Proxy pattern [2], as shown in Figure 7.
The RemoteEventService is an event service in a distributed environment. An EventProxy resides locally and serves as a proxy for the remote event service, and is implemented as a singleton. It maintains a list of local subscriptions, and subscribes to events from the remote event service on behalf of the local subscribers. For simplicity, we show the common EventService superclass maintaining the aggregations. It may be necessary to actually implement EventService as an abstract class, with the aggregations implemented in the subclasses RemoteEventService and EventProxy. For example, the event proxy may choose not to maintain a list of subscribers, but to delegate all operations directly to the remote event service. Alternatively, the type of objects stored in the aggregations may be different for the event proxy and the remote event service: the former may store local subscribers, and the latter may store remote subscribers (proxies). Figure 8 shows a typical interaction between the various
objects.
Use of an event proxy insulates publishers from the issues that arise in a distributed environment. In particular, an event proxy can yield the following benefits:
Enhancing Fault Tolerance and PerformanceLarge scale use of Event Notifier can potentially result in the event service being a single point of failure and a performance bottleneck. One solution to this problem is to have multiple event services, where each brokers a group of related events; for example, one event service for network management, and another for financial services. The failure of the event service for one area will not impact the systems dependent only on events from the other area.Another way to deal with the problem is to use a variation of Event Notifier in which the subscription list is not stored in the event service, but is instead distributed across publishers. One can achieve this without breaking the semantics of a centralized event service by having publishers advertise the event types they intend to publish. The event service would maintain this association of publishers and event types. On subscription to an event type, the event service would pass the subscriber to the publishers of that event type. Now publishers can inform subscribers directly. Since the subscription list is distributed among the set of publishers instead of being stored centrally in the event service, failure of any single component is likely to have less impact on the system as a whole. Since the frequency of publication is typically much higher than the frequency of subscription, and at publication time events are not being channeled through a single point, there is no single point that could become a bottleneck. Since the publication list changes less frequently than the subscription list, it is feasible to maintain a "hot spare" for the event service. Whenever an advertisement occurs, the event service updates the hot spare, thus keeping the information current in both places. If the event service goes down, the hot spare can take over. Asynchronous NotificationIf one implements the event service as a synchronous service, when a publisher publishes an event, the event service returns control to the publisher only after it informs each of the interested subscribers. If there are a large number of subscribers, or if subscribers do not return control to the event service in a timely manner, the publisher is blocked, and the event service cannot process other requests. One can fix this problem by providing asynchronous notification. For example, the event service could use a separate thread to handle the publication, and return control to the publisher immediately. The event service could start a separate thread to inform each subscriber, or rely on a thread pool.Push and Pull SemanticsEvent Notifier supports the canonical push model semantics [4], in which the publisher pushes events to the event service, and the event service pushes events to the subscriber. It is possible to have an active subscriber that pulls information from the event service, or a passive publisher from which information is pulled by the event service. If a pull model is desired at either leg, event notification semantics can be reestablished from the user's point of view by hiding it behind a proxy.Subscriber AnchoringGenerally in distributed environments, the subscriber executes the response to an event within its own address space. We refer to this type of subscriber as anchored, and it passes its own remote handle to the event service at subscription time. At other times, it is not important where the inform method is executed. An example of such an unanchored subscriber is one that pages an appropriate person, which can execute on its own. An unanchored subscriber passes a copy of itself to the event service, and the event service executes the inform method in its own address space. The process that called subscribe no longer needs to exist to process the events. An unanchored subscriber acts as an agent, which takes action in response to events independently of the process that called subscribe.Sample CodeThis section provides a skeletal implementation of Event Notifier in Java, and shows how it can be used.Event ServiceThe EventService class is implemented as a singleton, providing a well-known point of access and implementations for publish, subscribe, and unsubscribe.public class EventService { static private EventService singleton = null; // Provides well-known access point to singleton
EventService public void publish(Event event)
{ public void subscribe(Class eventType, Filter
filter, Subscriber subscriber) // Prevent duplicate
subscriptions public void unsubscribe(Class eventType, Filter
filter, Subscriber subscriber) private Class eventClass; // Stores information about a single
subscription public Class eventType; public class InvalidEventTypeException extends RuntimeException {} EventThe interface construct in Java is used to implement the abstract Event class shown in the Event Notifier structure.public interface Event {} public class FaultEvent implements ManagementEvent
{ public int severity; FaultEvent() { FaultEvent(int aSeverity)
{ FilterWhen the apply method on the CriticalFaultFilter class is called with an event, it returns true if the event is a critical fault event. A subscriber that provides this filter thus manages to filter out all but the critical fault events.public interface Filter { public class CriticalFaultFilter implements Filter
{ SubscriberAn abstract subscriber class and an example concrete subscriber are shown below. The PagingSystem subscriber subscribes to and handles FaultEvent.public abstract class Subscriber { public class PagingSystem extends Subscriber
{ public void inform(Event event)
{ // Respond to the
event PublisherThe Router class is an example publisher, which publishes a critical fault event.public class Router { Using Dynamic Typing to Restore Type SafetyAs described in the Reclaiming Type Safety subsection of Implementation, it is possible for the abstract subscriber's inform method to determine at run time which inform method of a concrete subscriber to invoke using the Java reflection API. Notice that the PagingSystem provides a type-specific inform method for handling fault events which does not need to downcast the event.public abstract class Subscriber { public class PagingSystem extends Subscriber
{ Known UsesIn this section, we compare two other event notification models with the Event Notifier model.JavaBeansThe JavaBeans API defined as part of the Java platform introduces reusable software components called JavaBeans. The user can connect and manipulate beans programmatically or visually using a bean builder. Beans communicate with each other via event notification. JavaBeans event notification is type safe, instance based, synchronous, and multicasting. It is restricted to a single process. For every event, there is a corresponding EventListener interface. The event source (publisher) advertises one or more EventListener interfaces that EventListener beans (subscribers) implement to receive events directly from the source beans. Source and listener beans know about each other, and the source handle is always passed as part of an event. Each source bean is responsible for maintaining a list of interested listeners. In Event Notifier, a well-known event service takes on this responsibility. Using Java's introspection API, beans can discover listener interfaces advertised by a source and implemented by a listener. Unless one uses an event adapter, the subscriber and publisher are coupled.CORBAThe CORBA Event Service [4] provides a way of delivering event data from supplier (publisher) to consumer (subscriber) without them knowing about each other. It supports typed and untyped event systems, neither of which allows events as objects.The CORBA Event Service supports both pull and push models, which allow suppliers and consumers to play active or passive roles. This results in unnecessary complexity. Furthermore, the CORBA Event Service provides no filtering. Related PatternsAs mentioned in the Motivation section, Event Notifier is closely related to the Observer and Mediator patterns [2], and in fact, can be viewed as a cross between them. The Multicast pattern [5] is also related, but emphasizes static type checking at the expense of genericity and extensibility.References
|