/ EXERCISE, PROGRAMMING, STYLE

Exercises in Programming Style: Event-Driven Programming

In the post from two weeks ago, we solved the problem using Object-Oriented Programming: we modeled the problem space using objects. For an object to communicate with another one, a dispatch() method was made available.

This is the 9th post in the Exercises in Programming Style focus series.Other posts include:

  1. Introducing Exercises in Programming Style
  2. Exercises in Programming Style, stacking things up
  3. Exercises in Programming Style, Kwisatz Haderach-style
  4. Exercises in Programming Style, recursion
  5. Exercises in Programming Style with higher-order functions
  6. Composing Exercises in Programming Style
  7. Exercises in Programming Style, back to Object-Oriented Programming
  8. Exercises in Programming Style: maps are objects too
  9. Exercises in Programming Style: Event-Driven Programming (this post)
  10. Exercises in Programming Style and the Event Bus
  11. Reflecting over Exercises in Programming Style
  12. Exercises in Aspect-Oriented Programming Style
  13. Exercises in Programming Style: FP & I/O
  14. Exercises in Relational Database Style
  15. Exercises in Programming Style: spreadsheets
  16. Exercises in Concurrent Programming Style
  17. Exercises in Programming Style: sharing data among threads
  18. Exercises in Programming Style with Hazelcast
  19. Exercises in MapReduce Style
  20. Conclusion of Exercises in Programming Style

Event-Driven Programming

Remember before the web was ubiquitous? Graphical user interfaces were already a thing. One great and widespread way to handle user interactions was - and still is - Event-Driven Programming: this has been popularized as the Observer design pattern.

Observer design pattern class diagram

The Observer is a viable alternative to the dispatch() method. The main difference is that by sending messages, there’s no return value.

Modeling the solution

The final class model looks like the following:

Solution class diagram

The model - as well as the next one - comes from the original Python solution. For Kotlin, I’ve just added the types.

The sequence diagram is:

Solution sequence diagram

The most important difference with the previous design is: there’s no dispatch() method anymore. As a consequence, there’s no return value as well, but at the end of the diagram.

Note that the initial design had no return value at all! The reason for that is that it only printed the word frequencies. Hence, it was not possible to test easily. Because one of my requirements is to make sure the implementation is correct, I changed the design to actually return the map of word frequencies as a value.

Managing event handlers

An event handler is just a higher-order function, a function that can be passed around as any other type.

In the above design, classes register their methods as higher-order functions to other classes, e.g. in the init block of WordFrequencyCounter:

  • dataStorage.registerForWordEvents { incrementCount(it) }
  • wfApp.registerForEndEvents { getTop25() }

There are several issues to solve regarding event handlers:

Ordering

Registering an event handler is easy: just provide a function with the right signature, and off you go. However, event handlers are triggered by events, and events might come in a different order than the one in which they need to be processed. Note that while this is not true with synchronous messaging - as it’s the case here - it’s still is harder to reason about than direct API calls.

To benefit from ordering anyway, one can store event handlers in different "buckets". Buckets can store handlers that don’t need to be ordered. At that point, one can invoke handlers from those buckets, starting with "Bucket 1", then "Bucket 2", etc. In the above design, there are 3 buckets: one for initialization handlers, one for work handlers, and the

Storing

In the Python sample, storing handlers in different buckets is just to call them in the order they were meant to be. In Kotlin, one also needs to keep types into account:

Description Type

Initialization event handlers consume something

(String) → Unit

Work handlers run by changing their internal state. Neither input nor output is necessary.

() → Unit

The single end handler needs to provide the word frequencies

() → Map<String, Int>

For the problem at hand, lambda types are compatible with each other.

In a real-world scenario, this probably wouldn’t be the case because there would be many handlers. Thus, the signature would need to be more generic, and casting necessary.

Invoking

Just storing handlers are not the end: at some point, they need to be invoked. As I mentioned above, some return a value, while some need a parameter - or many. For example, reading the text sample requires the file name. Hence, the following code:

class DataStorage(
    wfApp: WordFrequencyFramework,
    private val stopWordsFilter: StopWordsFilter
) {

    init {
        wfApp.registerForLoadEvents { load(it) }
    }

    private fun load(filename: String) {
        data = read(filename)
            .flatMap { it.split("\\W|_".toRegex()) }
            .filter { it.isNotBlank() && it.length >= 2 }
            .map(String::toLowerCase)
    }

    // Abridged for readability
}

Another initialization takes place in the StopWordsFilter class. It also loads a file - the stop words file - but it’s not parameterized by the file name. However, because it needs to be stored in the same bucket, it needs to accept an ignored parameter of type String:

class StopWordsFilter(wfApp: WordFrequencyFramework) {

    init {
        wfApp.registerForLoadEvents { load(it) }
    }

    private fun load(ignore: String) {                     (1)
        stopWords = read("stop_words.txt")[0].split(",")
    }

    // Abridged for readability
}
1 This is unfortunately required. At least, let’s name the parameter in a way it provides a hint.

The main issue with Event-Driven Programming

Event-Driven Programming suffers from a big issue: complexity. While in the context of our simple exercise, it’s pretty manageable, it can quickly escalate when the number of classes grows.

Let’s have two classes, and picture them as nodes: there can be a single edge between them. With three nodes, the count is 3. Beyond that, it grows according to this table:

Number of nodes Number of edges

2

1

3

3

4

6

5

10

6

15

…​

…​

n

n * (n - 1) / 2

Visibly, reasoning about event handling quickly becomes impossible.

Conclusion

Event-Driven Programming is a great asset in some contexts e.g. GUI. In that context, the number of event-sending classes, of event-receiving classes and possible relationships between them is pretty limited. As soon as the later count grows, the Observer pattern becomes quite complex because each observer needs to reference each subject. In next week’s post, we will study a possible solution to that issue.

The complete source code for this post can be found on Github.
Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Currently working for Hazelcast. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More