Reactive programming patterns
We already know what reactive programming is (if you don't, check out this article). In this article, we will look at some of the most common design patterns used in reactive programming and what common design principle they all have that enables the actual reactivity.
List of the patterns
Here is the list of the patterns that we are going to explore:
Observable/Observer
Event emitter
Signal
These are the most frequently used ones when it comes to reactive programming and shed some light on the whole picture of this world.
Observable and observer
Those two patterns go hand in hand. There is no point in having one without another. The idea behind them is pretty simple. There is a list of clients that want to know about some changes. The Observable can notify them about those changes, but it is required to establish a connection between the Observable and the clients. That's where Observer comes into play. It is basically the contract of the Observable that the client has to fulfill to get notification about the changes.
Here is what a simple observer looks like.
class Observer {
public notify() {}
}
That's right, there is only one method and nothing else.
Observable in this regard looks more complex.
class Observable {
private subscribers: Observer[];
public subscribe() {}
public unsubscribe() {}
public notifyObservers() {}
}
Whenever some client wants to get updates from a particular Observable, he provides an observer interface. It can be a basic function, object, or an observer class.
observable.subscribe(new Observer(...))
observable.subscribe(function() {...})
The important part here is to follow the contractor of the observer that the observable declares. If it only accepts classes, you can't use functions, and vice versa.
Under the hood, the Observable adds the new observer to the list of subscribers.
class Observable {
// other class fields
private subscribers: Observer[];
public subscribe(observer: Observer) {
this.subscribers.push(observer);
}
}
Whenever the Observable updates its value, it notifies each Observer from the list of subscribers.
class Observable {
// other class fields
private subscribers: Observer[];
public notifySubscribers(value) {
// iterates over the the array of subscribers
// and calls notify(value) method with the new value
}
}
Event emitter
The name itself gives us a hint about this pattern. The primary focus of the pattern is events and their handlers. Here is the structure of a simple event emitter.
class EventEmitter {
private eventsMap: Map<string, EventHandler[]>;
public addEventHandler(event, handler) {};
public removeEventHandler(event, handler) {};
public notify(event) {};
}
If someone wants to add a new event/handler to the emitter, he passes an event name alongside the handler that is called whenever this event is fired.
class EventEmitter {
// other class fields
private eventsMap: Map<string, EventHandler[]>;
public addEventHandler(event, handler) {
// 1. check if there is such event in the `eventsMap`
// 2. if there is no such event add it
// 3. add handler for this event
}
}
Whenever this particular event is triggered, all handlers are called one by one.
class EventEmitter {
// other class fields
private eventsMap: Map<string, EventHandler[]>;
public notify(event) {
// 1. Check if event registered in the map
// 2. If there is no such event exit
// 3. If there is such event call its handlers
}
}
Notice that the notify
method can accept not only the event itself but also some payload that event handlers of this particular event require.
Signal
Signal is a bit different from the previous patterns in terms of structure.
class Signal {
private children: Node;
private value: string;
public getValue() {}
public setValue() {}
}
Basically, it represents a tree, and all subscribers/children of the signal are stored as child nodes of the root node. Whenever the signal has some value updates, it traverses the tree of children to update them respectively.
Note that changes in a particular signal only affect its children but not the whole tree structure. More precisely, it affects all dependent signals. I won't dive any further in this article in this pattern. Maybe there will be a separate one in the future.
Summary of the patterns
While all of the three patterns look different, they actually have the common principle lying behind all of them: store depending entities and update them whenever some action happens. This general abstraction over those patterns allows us to see the picture behind a particular pattern and switch between them simultaneously.
Conclusion
There are multiple patterns used to build the code in a reactive paradigm, such as Observable/Observer, Event emitter, and Signal. We saw their simple implementations and how they function to achieve reactivity. While they look quite different, all of these patterns have a common ground: they all have to store subscribers and notify/update them whenever a new value comes. This high-level abstraction model allows us to see through all the patterns and quickly navigate through them.