In last week’s post, we solved the now familiar top-25-word-frequencies-in-a-text-file problem by using Event-Driven Programming.
This is the 10th post in the Exercises in Programming Style focus series.Other posts include:
- Introducing Exercises in Programming Style
- Exercises in Programming Style, stacking things up
- Exercises in Programming Style, Kwisatz Haderach-style
- Exercises in Programming Style, recursion
- Exercises in Programming Style with higher-order functions
- Composing Exercises in Programming Style
- Exercises in Programming Style, back to Object-Oriented Programming
- Exercises in Programming Style: maps are objects too
- Exercises in Programming Style: Event-Driven Programming
- Exercises in Programming Style and the Event Bus (this post)
- Reflecting over Exercises in Programming Style
- Exercises in Aspect-Oriented Programming Style
- Exercises in Programming Style: FP & I/O
- Exercises in Relational Database Style
- Exercises in Programming Style: spreadsheets
- Exercises in Concurrent Programming Style
- Exercises in Programming Style: sharing data among threads
- Exercises in Programming Style with Hazelcast
- Exercises in MapReduce Style
- Conclusion of Exercises in Programming Style
A bit of theory
The Observer pattern is a messaging pattern:
an Observer
subscribes to a Subject
.
When the later emits an event, the former is notified.
This is Point-to-Point messaging:
if multiple observers are interested in an event, they all must subscribe to the subject.
Point-to-point messaging comes with with a huge downside: each observer needs to reference each relevant subject at one point or another. This exponentially increases the complexity when the number of observer-subject pairs grows.
Another way to do messaging is Publish-Subscribe. In that case, messages are sent to a queue. Subscribers can register to a specific queue, to be notified of messages sent to that queue. As an option, messages can be persisted, so that subscribers registering to a queue can be notified of messages sent prior to the registration.
The Event Bus is an implementation of the Publish-Subscribe pattern, where there’s a single dedicated queue for each event type. Here’s the class diagram:
You can read more about it in this previous post.
Modeling the event bus
As with the Observer
pattern from last week, an event handler is a simple function, wrapped in a lambda.
The event bus, named EventManager
in the Python code, needs:
- To register event-handlers
-
This associates a lambda with an event type
- To handle events
-
This invokes lambdas associated with an event type when it’s passed an event
The model looks something like the following:
Note that there’s a single generic event class with a Type
attribute.
Alternatively, one could design a subclass per event type.
Here’s how to use the bus:
class DataStorage(private val eventManager: EventManager) { (1)
private lateinit var data: List<String>
init {
eventManager.subscribe<String>(Load) { load(it) } (2)
eventManager.subscribe<Unit>(Start) { produceWords() } (2)
}
private fun load(event: Event<String>) {
if (event.payload != null) {
data = read(event.payload)
.flatMap { it.split("\\W|_".toRegex()) }
.filter { it.isNotBlank() && it.length >= 2 }
.map(String::toLowerCase)
}
}
private fun produceWords() {
for (word in data) {
eventManager.publish(Event(Word, word)) (3)
}
eventManager.publish(Event(EOF)) (3)
}
}
1 | Embed the event manager. Each class that wants to take part in the same event management system needs to use the same event manager instance. |
2 | Associate event handlers under a specific event type.
The subscribe() function allows a generic type that matches the payload of the event.
Payload-less events use Unit . |
3 | Send a specific event, triggering the call of the stored lambda |
Not reinventing the wheel
The event manager could easily be made generic enough so that it could be reused across different projects. However, as a developer, I’m lazy so I prefer to use an existing library if there’s one, even if it goes beyond the scope of the exercise. The good news is that there are different Event Bus libraries out there that work really great:
Among them, I used - and liked - Guava’s. Guava makes the process of registering and sending events a breeze. Here’s the same code as above ported to Guava:
class DataStorage(private val eventBus: EventBus) {
private lateinit var data: List<String>
init {
eventBus.register(this) (1)
}
@Subscribe (2)
private fun load(event: LoadEvent) { (3)
data = read(event.filename)
.flatMap { it.split("\\W|_".toRegex()) }
.filter { it.isNotBlank() && it.length >= 2 }
.map(String::toLowerCase)
}
@Subscribe (2)
private fun produceWords(event: StartEvent) { (3)
for (word in data) {
eventBus.post(WordEvent(word)) (4)
}
eventBus.post(EOFEvent) (4)
}
}
1 | Compared to our code above, registration is quite simple:
instead of registering handlers one by one, register() scans for all functions of the class annotated with @Subscribe . |
2 | A function annotated with @Subscribe is considered to be an event-handler function.
It must accept a single parameter, and return nothing. |
3 | The parameter’s type plays the role of EventManager.Type in our custom implementation.
One needs a dedicated type for each event. |
4 | Posting is also straightforward:
create an instance of the event - or use a singleton object for events with no payload - and send it to the event bus with the post() function |
Conclusion
Messaging comes into two flavors: on one hand, the Observer pattern is Point-to-Point, and its complexity grows with the number of observer-subject pairs as the observer needs to get a reference on the relevant subject. On the other hand, the Event Bus has Publish-Subscribe semantics. It allow to introduce an intermediate component - the Event Bus - between the observer-subject pair, so that each only needs a reference to the bus.
While it’s perfectly possible to implement one’s own implementation of the Event Bus, it’s easier to use an existing battle-hardened one. It’s out of the scope of the exercise, but Guava’s EventBus is a solid library in that regard.