The most important aspects of the framework are flexibility and extendibility to satisfy software designers’ demands stemming from business rules. Thus, the framework provides both blocking and nonblocking event notifications where events are delivered according to logical conditions called further as predicates. Both mechanisms are transparent for the developer and are chosen on the behalf of an event listener’s behavior. Let’s consider several examples of notifications and follow some examples on how to use a framework to build event-based applications faster.
Get the code!
Download the complete source code for this article here.
Types of notifications
Blocking, or synchronous, notification is used when an event listener has to wait on a predicate that blocks its thread of execution. After the predicate is met, the thread proceeds. This provides a way to serialize a sequence of tasks in time. Nonblocking, or asynchronous, notification is similar to the standard event listener paradigm where listeners are registered to an event and notified when it occurs. The difference here is that the listeners are notified only if an event satisfies a predicate on which the listeners are registered. A predicate can be a simple logical condition or a complex one; for example, it may depend upon the results of another event.
How it works
The framework consists of several interfaces and abstract classes (Figure A), which allows extending the functionality. The core interface in the framework hierarchy is the Blackboard. It provides listener, predicate and event registration, event propagation, and listener notification (Listing A). The AbstractBlackboard class is a skeletal implementation of the Blackboard interface (Listing B). The Predicate interface (Listing C) and the AbstractPredicate class (Listing D) realize the main logical concept of the framework and are responsible for event dispatching. The BlackboardAction (Listing E) represents an action that is fired on the BlackboardEvent (Listing F) if a predicate yields to true.
Let’s take a closer look by studying the Blackboard interface. The addPredicate() method is designed to associate a logical condition (Predicate) with an action (BlackboardAction) or a set of actions to be fired by an event (BlackboardEvent). A new event is registered to the Blackboard by calling the putEvent() method. If the Blackboard has a predicate satisfying the event, actions registered to this predicate are fired. A Predicate is said to satisfy a BlackboardEvent, if the test() method returns true. Actions can be fired on an event arrival, and so can any object waiting on a condition to happen. The method waitPredicate() registers an object for a predicate and blocks its thread of execution. If an event satisfies the predicate, the object is unblocked and its thread of execution proceeds.
If there are no events that satisfy this predicate for a specified period of time, the method throws a TimeoutException and unblocks the object. This serves as a barrier pattern, providing a conditional wait on several threads. It is easy to call back an event that caused a predicate to yield true. In this case, a predicate should store an event that satisfies its condition (Listing G). Two methods, removePredicate() and removeEvent(), provide additional control over the Blackboard.
Normally, an event for which a predicate is found can be disposed. An event is also disposed if its time to live has expired. However, a BlackboardAction remains in the Blackboard until it is explicitly removed. Before the Blackboard is disposed, the close() method must be called to release resources (e.g., threads).
The Predicate interface has one method, test(), that is important for the whole event-driven framework. When implementing it, you decide when an expected event or a sequence of events has occurred. For example, you can easily program that a predicate is true when a single event with ID=5 is triggered, or when three events with ID=1, 2, and 3 take place exactly in this order (as in Listing G).
The BlackboardEvent is the root event for all the framework’s events; it supercedes the java.util.EventObject class. The BlackboardEvent is constructed with a reference to the source object, which is the object upon which this event initially occurred, and it is analyzed by predicates. An event is kept until it has been processed (i.e., a predicate yielding to true has been found) or it expires. You can set a time to live for an event using the setTTL() method and check whether it expires or not by calling the isExpired() method.
The BlackboardAction is the abstract class that implements the java.lang.Runnable interface. It runs against an event that satisfies a predicate to which the action is mapped. When you want to create an action, you need only to implement the execute() method, which is part of the run() method implementation. Whenever an action is found for a predicate, the framework runs it in a separate thread by executing its run() method. By extending the BlackboardAction class, you can also override two protected methods, setUp() and tearDown(), which are called just before and after the execute() method and designed to prepare and release resources for this action.
The sequence diagram in Figure B illustrates the framework as a whole.
Client 1 creates a blackboard and fills it with predicate action pairs. Client 2 waits for a predicate by executing the waitPredicate() method. Client 3 generates an event that satisfies the predicate of Client 2, so Client 2 is unblocked. If the event of Client 3 is accepted by a predicate that Client 1 added to the blackboard, that action is fired as well. Client 1 closes the blackboard when it is no longer needed.