Skip to content

Observer Pattern

Video Lecture

Section Video Links
Observer Pattern Observer Observer Pattern 
Observer Use Case Observer Use Case Observer Use Case 

Overview

...Refer to Book or Videos for extra content.

Terminology

...Refer to Book or Videos for extra content.

Observer UML Diagram

Observer Pattern Overview

Source Code

...Refer to Book or Videos for extra content.

./src/observer/observer-concept.ts

// Observer Design Pattern Concept

interface IObservable {
    // The Subject Interface

    subscribe(observer: IObserver): void
    // The subscribe method

    unsubscribe(observer: IObserver): void
    // The unsubscribe method

    notify(...args: unknown[]): void
    // The notify method
}

class Subject implements IObservable {
    // The Subject (a.k.a Observable)
    #observers: Set<IObserver>
    constructor() {
        this.#observers = new Set()
    }

    subscribe(observer: IObserver) {
        this.#observers.add(observer)
    }

    unsubscribe(observer: IObserver) {
        this.#observers.delete(observer)
    }

    notify(...args: unknown[]) {
        this.#observers.forEach((observer) => {
            observer.notify(...args)
        })
    }
}

interface IObserver {
    // A method for the Observer to implement

    notify(...args: unknown[]): void
    // Receive notifications"
}

class Observer implements IObserver {
    // The concrete observer
    #id: number

    constructor(observable: IObservable) {
        this.#id = COUNTER++
        observable.subscribe(this)
    }

    notify(...args: unknown[]) {
        console.log(
            `OBSERVER_${this.#id} received ${JSON.stringify(args)}`
        )
    }
}

// The Client
let COUNTER = 1 // An ID to help distinguish between objects

const SUBJECT = new Subject()
const OBSERVER_1 = new Observer(SUBJECT)
const OBSERVER_2 = new Observer(SUBJECT)

SUBJECT.notify('First Notification', [1, 2, 3])

// Unsubscribe OBSERVER_2
SUBJECT.unsubscribe(OBSERVER_2)

SUBJECT.notify('Second Notification', { A: 1, B: 2, C: 3 })

Output

node ./dist/observer/observer-concept.js
OBSERVER_1 received ["First Notification",[1,2,3]]
OBSERVER_2 received ["First Notification",[1,2,3]]
OBSERVER_1 received ["Second Notification",{"A":1,"B":2,"C":3}]

Observer Use Case

...Refer to Book or Videos for extra content.

Example UML Diagram

Observer Pattern in Context

Source Code

./src/observer/client.ts

// Observer Design Pattern Concept

import { DataController } from './data-controller'
import { DataModel } from './data-model'
import { BarGraphView, PieGraphView, TableView } from './data-view'

// A local data view that the hypothetical external controller updates
const DATA_MODEL = new DataModel()

// Add some visualisation that use the dataview
const PIE_GRAPH_VIEW = new PieGraphView(DATA_MODEL)
const BAR_GRAPH_VIEW = new BarGraphView(DATA_MODEL)
const TABLE_VIEW = new TableView(DATA_MODEL)

// A hypothetical data controller running in a different process
const DATA_CONTROLLER = new DataController() // (Singleton)

// The hypothetical external data controller updates some data
DATA_CONTROLLER.notify([1, 2, 3])

// Client now removes a local BAR_GRAPH
BAR_GRAPH_VIEW.delete()

// The hypothetical external data controller updates the data again
DATA_CONTROLLER.notify([4, 5, 6])

./src/observer/data-view.ts

import { IDataModel } from './data-model'

export interface IDataView {
    // A Subject Interface
    notify(data: number[]): void
    draw(data: number[]): void
    delete(): void
}

export class BarGraphView implements IDataView {
    // A concrete observer
    #observable: IDataModel
    #id: number

    constructor(observable: IDataModel) {
        this.#observable = observable
        this.#id = this.#observable.subscribe(this)
    }

    notify(data: number[]): void {
        console.log(`BarGraph, id:${this.#id}`)
        this.draw(data)
    }

    draw(data: number[]): void {
        console.log(
            `Drawing a Bar graph using data:${JSON.stringify(data)}`
        )
    }

    delete(): void {
        this.#observable.unsubscribe(this.#id)
    }
}

export class PieGraphView implements IDataView {
    // A concrete observer
    #observable: IDataModel
    #id: number

    constructor(observable: IDataModel) {
        this.#observable = observable
        this.#id = this.#observable.subscribe(this)
    }

    notify(data: number[]): void {
        console.log(`PieGraph, id:${this.#id}`)
        this.draw(data)
    }

    draw(data: number[]): void {
        console.log(`Drawing a Pie graph using data:${data}`)
    }

    delete(): void {
        this.#observable.unsubscribe(this.#id)
    }
}

export class TableView implements IDataView {
    // A concrete observer
    #observable: IDataModel
    #id: number

    constructor(observable: IDataModel) {
        this.#observable = observable
        this.#id = this.#observable.subscribe(this)
    }

    notify(data: number[]): void {
        console.log(`TableView, id:${this.#id}`)
        this.draw(data)
    }

    draw(data: number[]): void {
        console.log(`Drawing a Table using data:${JSON.stringify(data)}`)
    }

    delete(): void {
        this.#observable.unsubscribe(this.#id)
    }
}

./src/observer/data-model.ts

import { IDataController, DataController } from './data-controller'
import { IDataView } from './data-view'

export interface IDataModel {
    // A Subject Interface
    subscribe(observer: IDataView): number
    unsubscribe(observerId: number): void
    notify(data: number[]): void
}

export class DataModel implements IDataModel {
    // A Subject (a.k.a Observable)

    #observers: { [id: number]: IDataView } = {}
    #dataController: IDataController
    #counter: number

    constructor() {
        this.#counter = 0
        this.#dataController = new DataController()
        this.#dataController.subscribe(this)
    }

    subscribe(observer: IDataView): number {
        this.#counter++
        this.#observers[this.#counter] = observer
        return this.#counter
    }

    unsubscribe(observerId: number): void {
        delete this.#observers[observerId]
    }

    notify(data: number[]): void {
        Object.keys(this.#observers).forEach((observer) => {
            this.#observers[parseInt(observer)].notify(data)
        })
    }
}

./src/observer/data-controller.ts

import { IDataModel } from './data-model'

// A Data Controller Interface
export interface IDataController {
    // A Subject Interface
    subscribe(observer: IDataModel): void
    unsubscribe(observer: IDataModel): void
    notify(data: number[]): void
}

export class DataController implements IDataController {
    // A Subject (a.k.a Observable)

    static instance: DataController
    #observers: Set<IDataModel> = new Set()

    constructor() {
        if (DataController.instance) {
            return DataController.instance
        }
        DataController.instance = this
    }

    subscribe(observer: IDataModel): void {
        this.#observers.add(observer)
    }

    unsubscribe(observer: IDataModel): void {
        this.#observers.delete(observer)
    }

    notify(data: number[]): void {
        this.#observers.forEach((observer) => {
            observer.notify(data)
        })
    }
}

Output

node ./dist/observer/client.js
PieGraph, id:1
Drawing a Pie graph using data:1,2,3
BarGraph, id:2
Drawing a Bar graph using data:[1,2,3]
TableView, id:3
Drawing a Table using data:[1,2,3]
PieGraph, id:1
Drawing a Pie graph using data:4,5,6
TableView, id:3
Drawing a Table using data:[4,5,6]

Summary

...Refer to Book or Videos for extra content.