Communicating between components with events is one of the fundamental programming paradigms. One module publishes an event, and other modules respond via listeners. It's an asynchronous model that provides an excellent separation of responsibility yet still allows several components to work together. Beyond just messages, it also introduces a dispatcher, which is a useful general-purpose mechanism.
Why?
Modern applications run in an active environment. Data comes in from the network. The user clicks a mouse button or drags their finger across the screen. Another program enters the foreground, pushing the current one into the background. The GPS and accelerometers update the device's position. A scheduled alarm fires at the requested time.
Event programming is, as its name indicates, a way to connect all of these events. Rather than polling interfaces, we wait for messages to tell us when something happens. Programs driven primarily by events remain idle most of the time, only consuming resources when something requires their attention. They are asynchronous, being able to respond to different triggers whenever needed.
This article focuses on the benefits and core qualities of event programming. As with all paradigms, there are limitations. I’ll have to look at those in a future article.
The publisher
Each event requires a publisher, a component that advertises it can raise a particular event.
service Keyboard {
event KeyPressed<KeyMessage>
event KeyReleased<KeyMessage>
}
Here we have a Keyboard
service that raises KeyPressed
and KeyReleased
events as the user interacts with the keyboard. The KeyMessage
is the type of message it generates for the event. Messages include the pertinent details about the event. In this case, we'd expect the code of the key pressed, and possibly a timestamp.
The publisher has this focused role of publishing these events. It doesn't know, nor care, what the listeners do with the messages. This type of separation provides a limited API that is clear to implement, simple to use, and relatively easy to test.
The listener
Somebody interested in the keyboard subscribes to these events.
Keyboard.KeyPressed.Subscribe( OnKeyPressed );
Keyboard.KeyReleased.Subscribe( OnKeyReleased );
When the user presses a key, the OnKeyPressed
function will be called. This function accepts a KeyMessage
as an argument which provides it with the key that was pressed.
The subscribing module can listen to events from several different publishers. Perhaps it is also listening to a Pointer.Pressed
or Gamepad.ButtonPressed
event. The listener has to deal with getting messages from all the events at any time. On its own, it won't execute anything. It remains inactive until it receives a message.
Non-imperative
While the above syntax is somewhat imperative in nature, one nice aspect of event programming is that it works well with several paradigms. We could use it in a declarative UI syntax:
<Image>
<KeyPressed Key="Up">
<Scale Factor="2" Duration="1"/>
<Spin/>
</KeyPressed>
<KeyPressed Key="Down">
<Scale Factor="0.5" Duration="1"/>
<Bounce/>
</KeyPressed>
</Image>
Or it might serve as the base to a reactive model:
exports.lastKeyPressed = Keyboard.KeyPressed.listen( function( args ) {
return args.Key
})
Additionally, messages can be serialized and passed between threads or even across a network. The publisher-listener contract is limited; therefore it's a good model to apply to concurrent and distributed computing.
Event dispatcher and loop
Event programming requires a dispatcher. Publishers need somewhere to post their messages. The dispatcher might be a core part of the OS, like the classic Windows PostMessage
function, or it may be a library facility. In any case, each publisher connects to this dispatcher. Similarly, each listener subscribers to the dispatcher so it can receive the messages. In many APIs the actual dispatching mechanism is somewhat hidden, and publishers and subscribers don't need to think about it much.
The concept of an "event loop" nonetheless arises. The dispatcher is constantly listening for all messages, but only dispatching them one at a time. Each message is processed to completion before the next message starts (at least when single-threaded). This loop results in an async model. A publisher can post a message at any time, but it will not be processed until the program returns to the event loop. This async model allows the publisher to continue processing without worrying about interruptions from a listener.
We also gain the concept of deferred processing. Any code can take advantage of the message loop even without a formal event. Instead of a posting a message, a callback function can be posted to the dispatcher. This code will be executed once the dispatcher finished with the other messages in the queue. This model can be extended to allow cross-thread dispatching to offset load, or a priority queue to defer some processing to an opportune moment.
For example, in NodeJS this is exposed as the
nextTick
function. For Fuse, I added theUpdateManager.AddDeferredAction
function.
The blurry lines
Some APIs or languages, like C#, expose synchronous "events": these are processed immediately without an event loop. This is a weak form of the event paradigm as the synchronous nature makes it unsafe. The listener executes at the point where the publisher posts the message, therefore the publisher has to fear that the listener could call back into the publisher. To be safe, this requires limited message handling or dealing with reentrancy -- something which is not pleasant to do. The ability to escape out of the publisher's stack is a huge advantage of the async event model.
The sync-model can also inevitably lead to listeners making false assumptions about the ordering of events and the global state. For example, a keyboard listener can look at the current Keyboard
state rather than just using the provided KeyMessage
properties. The async model better enforces separation of components.
Targeted messages and remote execution may also be confused for the event paradigm. Consider a message that is created then posted directly to a single consumer, either locally or over the network. This sounds like the description of imperative programming. Indeed, Objective-C refers to function calling as message passing. This is not the event programming paradigm.
Use it everywhere
The event paradigm has become a staple of most programming. It's the basis of virtually all UI programming. A lot of async IO follows this model, posting messages when data becomes available. Deferred processing, and promises, are also derived from this model, albeit with a more closed relationship between publisher and listener.
The event paradigm is always combined with other paradigms. On its own, it can't do any processing. It's a good way to connect parts of an async system where things can happen at any time. Listeners are most commonly written with imperative programming, though events blend equally well into declarative and reactive paradigms.
Top comments (0)