Design Patterns: Observer Pattern

Object-Oriented Design (OOD) patterns, often referred to as Design Patterns, are general reusable solutions to common problems encountered in software design and development. These patterns provide a structured and efficient way to solve design problems and improve the quality and maintainability of software systems. Design patterns are a form of best practice and collective wisdom distilled from the experience of software developers.

I've worked most of my life as a UI developer, and for a long time, I hadn't thought of myself as someone who uses Object Classes. Sure, some frameworks use them such as Angular and React to name a few, but to me, that was just boilerplate. I never created my own Classes, and did't really care much about OO design. That was until I started creating larger pieces of software and just importing functions from modules wasn't going to cut it.

With all that being said let's jump into our first pattern, the Observer Pattern.

Observer Pattern: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

This pattern is used to notify other objects that data has changed in the main object. The relationship is a one-to-many type. The "one" is referred to as the Subject and the "many" are referred to as the Observers. For example, let's say we have a newspaper, the Subject, and a group of subscribers, the Observers.

The implementation of the pattern is basic. When a change happens in the Subject it will notify all of the observers by looping through them and calling the update method. "Update" can be called whatever you'd like, as long as they all follow the same interface contract outlined by the Subject's abstract class so the Observer knows what to call.

There are then two ways the subject can update the Observer's data: one way is to push data into the update method, for example, pass a name string into the method and then use the new name. The problem with this is it can cause re-work in other classes when params get added or taken away. It may not always be the best way to go about it.

/**
 * OBSERVERS
 */
abstract class Observer {
  abstract update(message: string): void
}

export class ConcreteObserver implements Observer {
  constructor(private name: string) {}
  update(message: string) {
    console.log(`${this.name} received message: ${message}`)
  }
}

/**
 * SUBJECT
 */
type SubjectT = {
  name: string
}

export class Subject {
  name: string
  subs: Observer[] = []

  constructor({ name }: SubjectT) {
    this.name = name
  }

  setName(name: string) {
    this.name = name
    this.executeChange()
  }

  addObserver(sub: Observer) {
    this.subs.push(sub)
  }

  executeChange() {
    this.onChange()
  }

  onChange() {
    this.subs.forEach((sub) => {
      sub.update(`Sending new name: ${this.name}`)
    })
  }
}

// Create instances of ConcreteObserver.
const observer1 = new ConcreteObserver("Observer 1")

// Create a Subject and attach observers to it.
const subject = new Subject({ name: "Subject" })
subject.addObserver(observer1)

// Notify observers when something changes.
subject.setName("new subject")

// Output: "Observer 1 received message: Sending new name: new subject"

Adding Getters

The second, and I believe, the better way, is to add getters and setters for all the data on the Subject you plan to expose and then have the Subject pass itself to the Observer. The Observer can then call whatever getters it would like on the Subject.

/**
 * OBSERVERS
 */
abstract class Observer {
  abstract update(subject: Subject): void
}

export class ConcreteObserver implements Observer {
  constructor(private name: string) {}

  update(subject: Subject) {
    console.log("address: ", subject.getAddress())
  }
}

export class ConcreteObserver2 implements Observer {
  constructor(private name: string) {}

  update(subject: Subject) {
    console.log("name: ", subject.getName())
  }
}

/**
 * SUBJECT
 */
type SubjectT = {
  name: string
  address: string
}

export class Subject {
  name: string
  address: string
  subs: Observer[] = []

  constructor({ name, address }: SubjectT) {
    this.name = name
    this.address = address
  }

  getName() {
    return this.name
  }

  setName(name: string) {
    this.name = name
    this.executeChange()
  }

  getAddress() {
    return this.address
  }

  setAddress(address: string) {
    this.address = address
    this.executeChange()
  }

 addObserver(sub: Observer) {
    this.subs.push(sub)
  }

  onChange() {
    this.subs.forEach((sub) => {
      sub.update(this)
    })
  }

  executeChange() {
    this.onChange()
  }
}

// Create instances of ConcreteObserver.
const observer1 = new ConcreteObserver("Observer 1")

// Create a Subject and attach observers to it.
const subject = new Subject({ name: "Subject", address: "2114 Elm St" })
subject.addObserver(observer1)

// Notify observers when something changes.
subject.setAddress("3333 Elm St")

// Output: address: 3333 Elm St

This is more code why is it better? Well, now we have officially decoupled the Observer update method from the Subject. Before there was a parameter signature that both the Subject and the Observer had to agree on. Now, the Subject notifies the Observers that there has been a general change, and the Observer can use the Subject's getters to get what data it wants.

In the example above I added a new piece of data: address. In the Subject onChange method, you can see it says nothing about addresses. But in our new and improved ConcreteObserver, we call subject.getAddress() to get the new address. For additional clarity, I created a ConcreteObserver2 that tracks the name field.