📖 JavaScript Observer Design Pattern

The Observer Pattern is a design pattern used to create a subscription mechanism that allows multiple objects (observers) to be notified of changes or events in another object (the subject). You can think of it as being similar to event listeners in JavaScript. Just like how an event listener responds to events like a button click, an observer responds to changes in a subject's state. This pattern is commonly used in scenarios where different parts of an application need to react to those changes in real-time.

Why Use the Observer Pattern?

Decoupling
The Observer Pattern decouples the subject from its observers, allowing them to interact without tight coupling, making your code more modular and easier to maintain.
Real-Time Updates
Observers can be updated in real-time whenever the subject’s state changes, ensuring that different parts of the application remain in sync, much like how event listeners are triggered when a specific event occurs.
Scalability
This pattern allows you to add or remove observers dynamically, making it easier to scale the application as needed, just as you can add or remove event listeners based on the needs of your application.

Observer Pattern in a Nutshell

Observer
A function or a module in your program that "watches" for specific changes or events (like adding a new article) in another part of the program (called the Subject).
Subject
The part of the program that the observers are watching. It manages the list of observers and notifies them when an event or change occurs.
Notification
When the Subject detects a change, it "notifies" all the registered observers, prompting them to execute their respective functions.

How it Works

Registering Observers
Different functions (observers) are registered with the Subject. These observers are essentially handlers that define what should happen when the event occurs.
Monitoring for Changes
The Subject is responsible for monitoring for changes. For example, when a new article is added to the news feed, the Subject knows that it's time to notify the observers.
Executing Observer Functions
Once the change occurs, the Subject notifies all the observers. Each observer then executes its function—whether that's logging the change to the console, updating the UI, or any other task.

Basic Structure of the Observer Pattern

The Observer Pattern can be implemented in JavaScript using a combination of functions and objects. In this pattern, we often use a special type of function called a constructor function to create objects.

Constructor Functions

Constructor functions are named using PascalCase (also known as StudlyCaps) to indicate that they are intended to be used with the new keyword, though in some cases, they may not require new. In the future, you will learn about classes, which also use PascalCase and are closely related to constructor functions.

Here’s a basic example of how to implement the Observer Pattern using a constructor function. You can think of this as similar to setting up an event listener, where the subject is like an event emitter, and the observers are the listeners.


function Subject() {
    this.observers = []; // Array of observer functions
}

Subject.prototype = {
    subscribe: function(fn) {
        this.observers.push(fn);
    },
    unsubscribe: function(fn) {
        this.observers = this.observers.filter(observer => observer !== fn);
    },
    notify: function(data) {
        this.observers.forEach(observer => observer(data));
    }
};

// Usage
const subject = new Subject();

function observer1(data) {
    console.log('Observer 1 received data:', data);
}

function observer2(data) {
    console.log('Observer 2 received data:', data);
}

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify('Hello Observers!'); // Both observers are notified

subject.unsubscribe(observer1);

subject.notify('Goodbye Observer 1!'); // Only Observer 2 is notified

In this example:

  • The Subject function is a constructor function, which is used to create Subject objects that manage observers.
  • The subscribe method allows an observer to register itself to receive updates from the subject, much like adding an event listener.
  • The unsubscribe method allows an observer to deregister itself from the subject’s updates, similar to removing an event listener.
  • The notify method triggers an update, calling each observer in the list with the latest data, akin to how an event triggers all the attached event listeners.

The Observer Pattern in Applications

The Observer Pattern is particularly useful in scenarios where multiple parts of an application need to react to changes in real-time. For example, it can be used to update the user interface based on data changes, or to manage event-driven architectures.


function NewsFeed() {
    this.articles = [];
    this.observers = [];
}

NewsFeed.prototype = {
    subscribe: function(fn) {
        this.observers.push(fn);
    },
    unsubscribe: function(fn) {
        this.observers = this.observers.filter(observer => observer !== fn);
    },
    addArticle: function(article) {
        this.articles.push(article);
        this.notify(article);
    },
    notify: function(article) {
        this.observers.forEach(observer => observer(article));
    }
};

// Usage
const newsFeed = new NewsFeed();

function displayArticle(article) {
    console.log('New article published:', article.title);
}

newsFeed.subscribe(displayArticle);

newsFeed.addArticle({ title: 'Observer Pattern in JavaScript', content: 'Learning about design patterns.' });
        

In this example, the NewsFeed object maintains a list of articles and observers. Whenever a new article is added, all registered observers are notified, allowing them to react to the update, such as displaying the article in the user interface. You can think of each observer as a specialized listener that reacts to the event of a new article being added.

Benefits of the Observer Pattern

Modularity
By decoupling the subject and observers, the Observer Pattern allows for more modular and maintainable code, similar to how event listeners keep the event handling logic separate from the main application logic.
Flexibility
Observers can be dynamically added or removed, making the system more flexible and adaptable to changes, much like adding or removing event listeners in response to user interactions.
Real-Time Synchronization
The pattern ensures that all observers remain in sync with the subject’s state, making it ideal for real-time applications, just as event listeners ensure that the UI responds instantly to user actions.

Real-World Example

Using the Observer Pattern in a News Aggregator

In a news aggregator application, the Observer Pattern can be used to update the display of news articles in real-time as they are published. Multiple parts of the application, such as the headline display and article list, can subscribe to updates from a central news feed object.


function NewsAggregator() {
    this.articles = [];
    this.observers = [];
}

NewsAggregator.prototype = {
    subscribe: function(fn) {
        this.observers.push(fn);
    },
    unsubscribe: function(fn) {
        this.observers = this.observers.filter(observer => observer !== fn);
    },
    addArticle: function(article) {
        this.articles.push(article);
        this.notify(article);
    },
    notify: function(article) {
        this.observers.forEach(observer => observer(article));
    }
};

// Usage
const aggregator = new NewsAggregator();

function updateHeadline(article) {
    console.log('Headline:', article.title);
}

function updateArticleList(article) {
    console.log('Article added to list:', article.title);
}

aggregator.subscribe(updateHeadline);
aggregator.subscribe(updateArticleList);

aggregator.addArticle({ title: 'Observer Pattern in News Aggregator', content: 'Real-time updates in action.' });
Why Use the Observer Pattern in a News Aggregator?
Real-Time Updates
The Observer Pattern allows the news aggregator to push updates to different parts of the application as soon as new articles are available, similar to how event listeners respond instantly to user actions.
Decoupled Architecture
The pattern decouples the news feed logic from the UI components, making it easier to manage and extend the application, just like how event listeners keep different parts of the application independent yet responsive.
Scalability
As the application grows, more observers can be added or removed without disrupting the existing system, much like how you can easily scale event handling in an application by adding or removing event listeners.

Putting It Into Action

Create an HTML file with the following structure and include the provided script. This script will demonstrate how to use the Observer Pattern to update a news feed in real-time.


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Observer Pattern Example</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>
    <div class="container mt-5">
        <h1>Observer Pattern Example</h1>
    </div>

    <script>
        function NewsFeed() {
            this.articles = [];
            this.observers = [];
        }

        NewsFeed.prototype = {
            subscribe: function(fn) {
                this.observers.push(fn);
            },
            unsubscribe: function(fn) {
                this.observers = this.observers.filter(observer => observer !== fn);
            },
            addArticle: function(article) {
                this.articles.push(article);
                this.notify(article);
            },
            notify: function(article) {
                this.observers.forEach(observer => observer(article));
            }
        };

        const newsFeed = new NewsFeed();

        function displayArticle(article) {
            const articleElement = document.createElement('div');
            articleElement.innerHTML = `<h2>${article.title}</h2><p>${article.content}</p>`;
            document.body.appendChild(articleElement);
        }

        newsFeed.subscribe(displayArticle);

        newsFeed.addArticle({ title: 'Observer Pattern Implemented', content: 'This is a live update!' });
    </script>
</body>
</html>

Challenge

Extend the example to allow for multiple types of observers that react differently to new articles. For instance, one observer could log the article's title to the console, while another could display the article in a list on the page.

In order to check your learning, you should attempt to create a solution before revealing the provided solution below.


// JavaScript Code
function NewsFeed() {
    this.articles = [];
    this.observers = [];
}

NewsFeed.prototype = {
    subscribe: function(fn) {
        this.observers.push(fn);
    },
    unsubscribe: function(fn) {
        this.observers = this.observers.filter(observer => observer !== fn);
    },
    addArticle: function(article) {
        this.articles.push(article);
        this.notify(article);
    },
    notify: function(article) {
        this.observers.forEach(observer => observer(article));
    }
};

const newsFeed = new NewsFeed();

function logArticleTitle(article) {
    console.log('New article title:', article.title);
}

function displayArticleInList(article) {
    const articleListElement = document.getElementById('article-list');
    const listItem = document.createElement('li');
    listItem.textContent = article.title;
    articleListElement.appendChild(listItem);
}

newsFeed.subscribe(logArticleTitle);
newsFeed.subscribe(displayArticleInList);

newsFeed.addArticle({ title: 'Observer Pattern Challenge', content: 'Extend the functionality!' });

References