Design Patterns in Action(8)- Observer
A series of programming design patterns illustration with examples with JavaScript/Python
Simply put, the Observer pattern offers a Publication/Subscription model where observers subscribe to an event and get notified when the event happens.
It is one the most important models in event-driven architecture. Essentially it has 2 participants, the observer(s) and the observable.
Observable:
- links to one event type
- maintains list of observers
- supports subscribe or unsubscribe from the observers.
- notifies observers when its state changes (events come in)
Observers:
- subscribes/unsubscribes from the observer based on event topics.
- implements a function signature that will be invoked by the observable when an event occurs.
Note that the Observer pattern, even implementing a pub/sub model, is not strictly equal to Publish/Subscribe pattern, as the latter actually moves the broadcasting stage out of the Publisher’s internal state to an external event channel (often using Queue), comparing to the Observer(Subscriber) to be notified by the Publisher itself directly. This further decouples the dependencies between the Publisher and Subscriber, and demonstrates loose coupling.
First, let’s see an example in Python:
from abc import ABCMeta, abstractmethod
class IObservable(metaclass=ABCMeta):
@staticmethod
@abstractmethod
def subscribe(observer):
pass
@staticmethod
@abstractmethod
def unsubscribe(observer):
pass
@staticmethod
@abstractmethod
def notify(observer):
pass
class BreadObservable(IObservable):
def __init__(self):
self._observers = set()
def subscribe(self, observer):
self._observers.add(observer)
def unsubscribe(self, observer):
self._observers.remove(observer)
def notify(self, data):
for observer in self._observers:
observer.update(data)
class LabelBreadObserver():
def __init__(self, observable):
observable.subscribe(self)
def update(self, data):
print(f"A bread marked as {data}")
class BakeBreadObserver():
def __init__(self, observable):
observable.subscribe(self)
def update(self, data):
print(f"A bread baked as {data}")
class WaitForRiseBreadObserver():
def __init__(self, observable):
observable.subscribe(self)
def update(self, data):
print(f"Waiting for {data} to rise")
breadObserver = BreadObservable()
labelBreadObserver = LabelBreadObserver(breadObserver)
bakeBreadObserver = BakeBreadObserver(breadObserver)
breadObserver.notify("Sourdough")================================
A bread baked as Sourdough
A bread marked as Sourdough
Now an example of JavaScript, note the use of set
, so it’s safe for us if we accidentally passing in the same observer for multiple times as set checks for reference. Also note this way we are passing in the function directly as observer just as the everyday JavaScript event listener you will see on the browser: window.addEventListener(‘click’, () => {console.log(‘Page loaded’)});
class BreadObservable {
observers = new Set();
subscribe(func) {
this.observers.add(func);
}
unsubscribe(func) {
this.observers.delete(func);
}
notify(data) {
this.observers.forEach((observer) => observer(data));
}
}
const labelBread = (kind) => {
console.log(`A bread marked as ${kind}`);
};
const bakeBread = (kind) => {
console.log(`A bread baked as ${kind}`);
};
const waitForRise = (kind) => {
console.log(`Waiting for ${kind} to rise`);
};
const breadObserver = new BreadObservable();
breadObserver.subscribe(labelBread);
breadObserver.subscribe(bakeBread);
breadObserver.subscribe(waitForRise);
breadObserver.unsubscribe(waitForRise);
breadObserver.notify("Sourdough");==============================>
A bread marked as Sourdough
A bread baked as Sourdough
Sometimes we want to have more control of the observer, or just have another way of implementing this. In this way, we have an agreed contract between the Observable and the Observer on the update
method. And instead of invoking the observer directly, we will trigger the update
method on the observer.
class BreadObservable {
observers = new Set();
subscribe(func) {
this.observers.add(func);
}
unsubscribe(func) {
this.observers.delete(func);
}
notify(data) {
this.observers.forEach((observer) => observer.update(data));
}
}
class LabelBreadObserver {
update(kind) {
console.log(`A bread marked as ${kind}`);
}
}
class BakeBreadObserver {
update(kind) {
console.log(`A bread baked as ${kind}`);
}
}
class WaitForRiseBreadObserver {
update(kind) {
console.log(`Waiting for ${kind} to rise`);
}
}
const breadObserver = new BreadObservable();
breadObserver.subscribe(new LabelBreadObserver());
breadObserver.subscribe(new BakeBreadObserver());
breadObserver.subscribe(new WaitForRiseBreadObserver());
breadObserver.unsubscribe(new WaitForRiseBreadObserver());
breadObserver.notify("Sourdough");
===================
A bread marked as Sourdough
A bread baked as Sourdough
Waiting for Sourdough to rise
You can also combine state management in this, to initiate a state object at Observable initiation, and every time a new piece of data passed in, we merge it with the existing data object, and pass this merged data to the observers.
class BreadObservable3 {
observers = new Set();
state = {};
subscribe(func) {
this.observers.add(func);
}
unsubscribe(func) {
this.observers.delete(func);
}
notify(data) {
this.state = Object.assign(this.state, data);
this.observers.forEach((observer) => observer.update(this.data));
}
get() {
return this.state;
}
}
Also, always remember to unsubscribe the observer from the observable when it’s no longer in use, as failing doing so will cause the observer not being garbage collected, aka. the lapsed listener problem .
Another feature that is common in real life scenarios that we haven’t implemented in the blog is the ability to pass in a scope
variable. So instead of just passing in the data, we can pass in a scope to decide the this
of the observer. This happens a lot in event listeners, when the scope is normally bounded to a global object e.g. window if not overwritten.
const notify = (data, extraScope) {
const scope = extraScope || window;
this.observers.forEach((observer) => observer.call(scope, data));
}
That’s so much of it! Happy Reading!