Design Patterns in TypeScript: Observer Pattern

Learn how to easily implement the Observer Pattern in TypeScript

In this series, we are looking at various design patterns, their implementation in TypeScript, and how they improve your life as a developer.

If you haven’t read the previous articles in the series on Design Patterns in TypeScript you might want to check them out, since the difficulty of patterns increases as we go through the series.

The Observer Pattern is a behavioral design pattern that lets you set up a subscription model to notify other objects of events that happen to the object they are observing.

“The subject only knows one thing about its observers, that they implement a certain interface, they have zero knowledge of which class it is, what it does or what it contains.”

Real-world example

This pattern is very similar to a magazine subscription. A person subscribing to a magazine no longer has to check the store for the latest issue of a magazine on a regular basis. The publisher of the magazine holds a list of all subscribers and sends new issues to its subscribers as they become available.

Structure of the Observer Pattern

UML diagram explaining the Observer Pattern

How to implement the Observer Pattern in TypeScript

In order to implement the Observer pattern in TypeScript, we need to understand how the pattern works first.

  1. An object that holds some form of interesting state is called a Subject. This subject also holds a list of subscribers who are interested in this state.
  2. When this state changes the Subject calls the update() method on all of those subscribers.

Let’s start defining two interfaces to help us set up the pattern easier.

interface Observer {
// Receive update from subject.
update(subject: Subject | CustomEvent): void;
}
interface Subject {
// Attach an observer to the subject.
attach(observer: Observer): void;
// Detach an observer from the subject.
detach(observer: Observer): void;
// Notify all observers about an event.
notify(event: CustomEvent | undefined): void;
}
view raw Observer.d.ts hosted with ❤ by GitHub

This helps us enforce the proper methods, but every Subject has to implement the attach, detach and notify methods which all contain the same logic. Let’s also create an abstract class to write this logic in a single place.

export default abstract class AbstractSubject implements Subject {
#observers: Observer[] = [];
public get observers(): Observer[] {
return this.#observers;
}
public attach(observer: Observer): void {
if (this.#observers.includes(observer)) {
return;
}
this.#observers.push(observer);
}
public detach(observer: Observer): void {
this.#observers = this.#observers.filter((o) => o !== observer);
}
public notify(): void {
for (const observer of this.#observers) {
observer.update(this);
}
}
}

This implementation is quite simple.

  • We keep track of an array of Observers (Which are just classes that have an update method as defined in our interface)
  • The attach() method checks if the passed-in observer is already in the array, if it isn’t it will push it in there.
  • The detach() method removes the passed-in observer from the array.
  • The notify() method will call the update method on every observer, passing a reference to itself as a parameter.
class SubjectExample extends AbstractSubject {
#state = 'some interesting state';
get state(): string {
return this.#state;
}
set state(value: string) {
this.#state = value;
this.notify();
}
}
const subject = new SubjectExample();
class ObserverExample implements Observer {
constructor() {
subject.attach(this);
}
public update(subjectExample: SubjectExample): void {
console.log('Subject notified us of an update', subjectExample.state);
}
}
const observer = new ObserverExample();
subject.state = 'new';

Advantages of using the Observer Pattern

  1. The subject only knows one thing about its observers, that they implement a certain interface, they have zero knowledge of which class it is, what it does or what it contains.
  2. We can add / remove / replace observers at any given time. Since the subject keeps a simple list of observers who need to be notified it will keep notifying anything in the list without problems.
  3. We never have to modify the subject in order to add new observers. Since an observer calls the public attach() method we never have to touch the subject.
  4. If the subject or observer gets another use down the line, we can easily reuse them since the two are not tightly coupled.
  5. Changes to one do not affect each other. Since the subject and observer are loosely-coupled changes will not affect each other as long as they adhere to the rules of their inherent interfaces.

Conclusion

The Observer pattern is one of the easiest design patterns to understand. It’s a great introduction to how design patterns can improve your code and I would suggest reading more on the topic.

Thanks for reading. If you have thoughts on this, be sure to leave a comment.

If you like my content and want to support my efforts, consider becoming a Medium subscriber through my affiliate link. It will cost you nothing extra, but Medium will give parts of the proceeds to me for referring you.

And if you want, you can connect with me on LinkedIn or Twitter!

LEAVE A REPLY

Your email address will not be published. Required fields are marked *