Table of Contents
Reactive programming is an important facet of modern software development. It’s often described as a paradigm, but what does that mean? A more useful description is that reactive programming is a way of looking at software as the interaction of event producers, observers, and modifiers. When properly applied, this approach yields powerful benefits to the composability, extensibility, and understandability of your code.
Reactive programming is not a silver bullet, but it is a very valuable technique. It can help improve your application architectures in a wide range of scenarios, including real-time data processing, asynchronous programming, user interfaces, distributed systems, and more.
Reactive programming defined
Reactive programming is an approach to building software that models IO as streams of events. With reactive programming, developers can compose and work with applications in a declarative, functional style that is also standardized.
A simple reactive program
Listing 1. A reactive program coded with RxJS
// Create an event stream from a text input keypress const input = document.getElementById('myInput'); const keypressStream = rxjs.fromEvent(input, 'keypress'); // Apply operators to the stream const filteredStream = keypressStream.pipe( rxjs.operators.map(event => event.key), rxjs.operators.filter(key => key !== ' '), rxjs.operators.throttleTime(500) // Throttle keypress events to a maximum of one event per 500ms ); // Subscribe to the stream and handle the events filteredStream.subscribe(key => console.log('Keypress:', key); );
throttleTime to transform the stream into the characters pressed, eliminate empty keypresses (spacebars), and throttle the frequency of events to 500 milliseconds. Finally, it creates a subscription to the stream that outputs the transformed events to the console. You can see this example running live here.
Benefits and drawbacks of reactive programming
Event streams give a clear and portable means of representing data flows. Using event streams makes code declarative rather than imperative, which can be a huge win for composability and understandability. Reactive systems are designed to handle streams asynchronously, which makes them very scalable and amenable to optimization for concurrency. Taking advantage of these performance characteristics requires little effort on the part of the developer.
On the downside, reactive programming comes with a significant learning curve. As a result, fewer programmers really understand how to program reactively. Another drawback is that once you do grasp the power of reactivity, you might be tempted to see it as a panacea, or the solution for every problem. In general, however, when reactive programming is applied in the right way, in the right scenarios, it is astonishingly effective.
Event streams in reactive programming
At the very heart of reactive programming is the idea of event streams. Streams can represent any kind of flow of data, from network activity to simple things like lines in a text file. Anytime you can take a data source and make it raise discrete events based on elements within the data, it can be wrapped, or modeled, as a reactive stream.
With event streams, you can connect input and output sources as composable elements, while also manipulating them anywhere along the chain. Applications become a network of streams, with producers, consumers, and modifiers.
Reactive programming vs. reactive UIs
In the case of Angular, the relationship is explicit because Angular uses RxJS, a reactive framework, to implement its reactive UI. With other frameworks, like React, the relationship is murkier. These frameworks use reactive principles and often wind up building pseudo-reactive engines, but they do not use a reactive framework under the hood.
Elements of reactive programming
From a high level, reactive programming hinges on several core elements:
- Observables are the concrete means for modeling arbitrary event streams as reusable components. Observables have a well-defined API that produces events, errors, and lifecycle events, which other components can subscribe to or modify. In essence, the
Observabletype makes a portable, programmable unit out of an event stream.
- Observers are the corollary to observables. Observers subscribe to and partake of the events observables produce. The effect is that application code tends to be in an inversion-of-control (IoC) position, where the code is connecting observables and observers instead of dealing directly with the functionality.
- Operators are analogous to functional operators, also known as higher-order functions. Reactive operators allow for manipulating the events that move through the stream in a wide variety of ways. Again, the application code can remain at arms length by injecting the desired functionality into the streams “from above” while keeping the operations closely associated with the data they work on.
Another important type of component is the scheduler. Reactive programming includes schedulers to help manage the way events are handled by the engine. Using our simple example from Listing 1, we could tell the engine to run the operation on an asynchronous scheduler, as shown in Listing 2.
Listing 2. Using a scheduler
const filteredStream = keypressStream.pipe( rxjs.operators.observeOn(rxjs.asyncScheduler), rxjs.operators.map(event => event.key), rxjs.operators.filter(key => key !== ' '), rxjs.operators.throttleTime(500) );
The scheduler concept is a powerful way to define and control the behavior of streams. There are many variations in different frameworks. It’s also possible to write your own custom implementations.
Backpressure is another important concept in reactive programming. In essence, it answers the question: what happens when there are too many events occurring too fast for the system to consume?
Put another way, backpressure strategies help manage the flow of data between event producers and event consumers, ensuring that the consumer can handle the rate of incoming events without being overwhelmed. These fall into several general approaches, including the following:
- Dropping: Basically says: if events are backing up, throw them away. Once the consumer is able to handle more, simply start with the latest events. Prioritizes liveness.
- Buffering: Creates a queue of unprocessed events and gradually hands them off to the consumer as it is able to. Prioritizes consistency.
- Throttling: Regulates the rate of event delivery using strategies, like time throttling, count throttling, or more elaborate mechanisms like token buckets.
- Signaling: Create a way to communicate from the consumer to the producer the state of backpressure, allowing the producer to respond appropriately.
Note that backpressure strategies can change dynamically based on conditions. If we wanted to add count-based buffer backpressure handling to our simple example, we could do so as shown in Listing 3.
Listing 3. Buffering the stream
const filteredStream = keypressStream.pipe( rxjs.operators.throttleTime(500), // Throttle keypress events to a maximum of one event per 500ms rxjs.operators.observeOn(rxjs.asyncScheduler), rxjs.operators.map(event => event.key), rxjs.operators.filter(key => key !== ' '), rxjs.operators.bufferCount(5) );
Of course, this example doesn’t really require backpressure, but it does give you an idea of how it works. Notice that buffering works collaboratively with throttling. That means throttle will keep the rate of keystrokes to 500 milliseconds, and once there have been five of these, they will be let through to the subscriber to be output on the console. (You can see the buffering example running here.)
There are numerous frameworks and engines for reactive programming across the landscape of languages and platforms. The flagship project is ReactiveX, which defines a standard specification implemented by many languages. It is a good framework for exploring reactive programming. Most major languages have a quality implementation of ReactiveX.
In addition, many frameworks deliver reactivity, like the .NET reactive extensions, the Reactor project (which is JVM based), Spring WebFlux (built on top of Reactor), Java’s Play framework, Vert.x (for Java, Kotlin, and Groovy), Akka Streams (Java), Trio (Python), and Nito.AsynxExec (.Net). Some languages also have decent reactive support built in. Go with
goroutines is one example.
Copyright © 2023 IDG Communications, Inc.