This is the second post in our Software Design Pattern series, and today we will be exploring the Observer pattern. The formal intent of the Observer Pattern, as defined in the book ‘Design Patterns: Elements of Reusable Object-Oriented Software’ by the Gang of Four (GOF), is to ‘define 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 commonly employed, such as when creating a listener in JavaScript to detect when an element is clicked. Whenever there is a need for one object to be aware of another object’s state changes, the Observer pattern proves to be valuable.

Pattern Overview

There are four key elements to consider when implementing the Observer Pattern:

  1. Subject: This is an interface that defines a class type with methods for adding and removing observers, as well as notifying observers when the state of the Concrete Subject changes.
  2. Observer: This is an interface that enforces a class type with a method that is called to update an object of this interface when the state of the Concrete Subject changes.
  3. Concrete Subject: This is the class that the Concrete Observer is interested in, as it wants to be notified when the state of the Concrete Subject changes.
  4. Concrete Observer: This is the class that expresses interest in knowing when the state of a Concrete Subject changes.

News Feed Example – Design

Sometimes, understanding how something works in the abstract can be challenging. To provide a clearer picture, let’s look at an example that demonstrates the Observer pattern in action. In this example, we’ll use a News Feed as our Subject, and the Observers will be various types of Aggregators, such as Email and RSS, which subscribe to the News Feed. These Observers are interested in being notified when a new article is added to the feed, indicating a change in the state of the News Feed. To illustrate the structure of this solution, we have a UML Class Diagram:

News Feed Example – Implementation

I have chosen to implement our News Feed example using TypeScript, primarily because it offers key features that are not easily available in Vanilla JavaScript. By using TypeScript, we can leverage its Object-Oriented features, such as Interfaces, which are essential elements in the design patterns we are exploring. This allows us to provide a clear and concise explanation of how the Observer Pattern works, without getting bogged down in unnecessary complexities. Below, you will find the code used to implement the News Feed Observer Pattern example.

Subject Interface

Let’s begin by examining the implementation of the Subject Interface. In our example, our goal is to handle the subscribers of the News Feed. Therefore, we have defined the methods add(), remove(), and notify() within the interface type definition. These methods will be implemented by the Concrete Subject, which in this case is our NewsFeed class.

/**
 * Observer Pattern - Subject Interface
 */
interface INewsFeed {
  add(aSubscriber: ISubscriber): void
  remove(aSubscriber: ISubscriber): void
  notify(): void
}

Observer Interface

Next, let’s examine the implementation of the Observer Interface. The code for this interface type definition is straightforward. The interface includes an id property and an update() method. The id property serves to identify the Subscriber in the output of our example, while the update() method will be executed on the Subscriber when it receives a notification that a new Article has been added to the news feed.

/**
 * Observer Pattern - Observer Interface
 */
interface ISubscriber {
  id: number
  update(): void
}

Concrete Subject Class

Now, let’s examine the Concrete Subject Class that implements the INewsFeed interface. This class, named NewsFeed, is the most complex part of the solution as it has two responsibilities. Firstly, it manages its own state by adding articles to the object using the addArticle() method. Secondly, it handles the registration, de-registration, and notification of objects that have subscribed to the NewsFeed through the implementations of the add(), remove(), and notify() methods.

The NewsFeed class contains two private properties. The first one is the _subscribers array, which stores the list of ISubscriber objects that have been added to the NewsFeed. The second one is the _feed array, which holds the list of Articles that have been added to the news feed.

Here is the code for the NewsFeed class:

/**
 * Observer Pattern - ConcreteSubject Class
 */
class NewsFeed implements INewsFeed {
    private _subscribers: Array<ISubscriber> = [];
    private _feed: Array<Article> = [];
    get feed(): Array<Article> {
        return this._feed;
    }
    add(aSubscriber: ISubscriber){
        this._subscribers.push(aSubscriber);
        log(`[NewsFeed] - add new subscriber with id = ${aSubscriber.id}`);
    }
    remove(aSubscriber: ISubscriber){
        this._subscribers.splice(this._subscribers.indexOf(aSubscriber), 1);
        log(`[NewsFeed] - remove a subscriber with id = ${aSubscriber.id}`);
    }
    notify(){
        this._subscribers.map((thisSubscriber) => {
            thisSubscriber.update();
        })
        log(`[NewsFeed] - notified ${this._subscribers.length} subscribers`);
    }
    addArticle(anArticle: Article){
        this._feed.push(anArticle);
        this.notify();
    }
}

Concrete Observer Class

Now, let’s examine the Concrete Observer Classes that implement the ISubscriber interface. In this example, we have two types of Observers: Email Aggregator and RSS Aggregator. Each observer type can have one or more objects that want to be notified when a new article is added to the news feed.

The Observer pattern establishes an explicit dependency between the Concrete Subject and the Concrete Observer. In our implementation, the Concrete Observer contains a parameter of type NewsFeed. This allows the observer to encapsulate the way it accesses and uses the state of the Subject entirely within its own object. To achieve this, we pass the Subject (NewsFeed) as a parameter to the constructor method of the Observer. Additionally, in our example, we also pass the id as a parameter to the constructor. This id is used for identification purposes, primarily to ensure that the correct Observers are being utilized in the output of our example.

Here are the two Concrete Observer classes: EmailSubscriber and RSSSubscriber. These classes aggregate the Articles in the NewsFeed.

/**
 * Observer Pattern - ConcreteObserver Class Type 1
 */
class EmailSubscriber implements ISubscriber{
    id: number;
    private _newsFeed: NewsFeed;
    constructor(aNewsFeed: NewsFeed, anId: number){
        this._newsFeed = aNewsFeed;
        this.id = anId;
    }
    update(){
        log(`[EmailSubscriber] with id = ${this.id} notified.`);
        this._newsFeed.feed.map((thisArticle) => {
            log(`[EmailSubscriber] id = ${this.id} prints article title = ${thisArticle.title}`);
        });
    }
}
/**
 * Observer Pattern - ConcreteObserver Class Type 2
 */
class RssSubscriber implements ISubscriber{
    id: number;
    private _newsFeed: NewsFeed;
    constructor(aNewsFeed: NewsFeed, anId: number){
        this._newsFeed = aNewsFeed;
        this.id = anId;
    }
    update(){
        log(`[RssSubscriber] with id = ${this.id} notified.`);
        this._newsFeed.feed.map((thisArticle) => {
            log(`[RssSubscriber] id = ${this.id} prints article title = ${thisArticle.title}`);
        });
    }
}

Article Class

Finally, we have the Article Class, which isn’t a key element of the observer pattern, but it is used in our example to represent Article objects held in the News Feed. Here is the code:

/**
 * An class created for this example (not a part of the
 * official observer pattern persay)
 */
class Article {
    private _title: string;
    constructor(aTitle: string){
        this._title = aTitle;
    }
    get title(): string {
        return this._title;
    }
}

After compiling this code from TypeScript into Vanilla JavaScript and running it either via the command line with Node.js or in a browser, the following output can be observed. The output clearly demonstrates the addition of Observers to the Subject. The Observers are notified when the state of the Subject changes by adding a new article. When an Observer is subsequently removed, and the state changes again, only the remaining registered Observers are notified. This example effectively showcases the Observer Pattern in action.

Criticisms

As you delve into the Observer pattern, it becomes evident that there are numerous practical scenarios where it can be effectively utilized. However, it’s crucial to consider certain factors and trade-offs. One of the challenges with Design Patterns is that inexperienced software engineers may believe they can apply a newly learned pattern universally, without fully understanding the trade-offs involved. It is the responsibility of a software engineer to make informed decisions, weighing the potential benefits against the trade-offs.

In some cases, the trade-offs may be worthwhile due to the advantages offered by the pattern. However, there have been discussions questioning the use of the Observer Pattern. Martin Odersky, whom we encountered in our Singleton Pattern post, is not particularly fond of Design Patterns. In his paper titled “Deprecating the Observer Pattern,” he presents a comprehensive argument against using the observer pattern. Odersky argues that the observer pattern violates fundamental coding principles such as encapsulation, composability, and separation of concerns, among others, suggesting that it should not be employed.

While I may not fully agree with Odersky’s viewpoint, I do believe it is essential to be aware of the potential drawbacks and make informed decisions when applying design patterns like the observer pattern to solve coding problems.

Indeed, one argument against the Observer pattern is that passing the instance of the Subject to the Observer’s constructor may be perceived as violating encapsulation. Critics argue that by having the Observer hold a reference to the Subject, it allows the Observer to potentially access and manipulate the Subject’s internal state, which could be seen as a violation of encapsulation principles.

However, it’s important to note that the purpose of the Observer pattern is not to modify the state of the Subject directly through the Observer. Instead, the Observer is typically interested in observing and reacting to changes in the Subject’s state. As long as the Subject is designed properly, with appropriate access controls and encapsulation, other objects, including Observers, should not be able to directly mutate the state of the Subject.

In this context, the Observer’s access to the changed state of the Subject can be viewed as a form of observation rather than direct manipulation. It allows the Observer to react or perform necessary actions based on the updated state of the Subject. As long as the Observer does not modify the Subject’s state directly, encapsulation is not necessarily violated.

It’s worth noting that encapsulation is a fundamental principle in software engineering, and it is crucial to design classes and objects with proper access controls and encapsulation boundaries. The Observer pattern, when implemented correctly and with consideration for encapsulation, can provide a useful mechanism for decoupling objects and facilitating communication while preserving encapsulation.

Conclusion

Design patterns have emerged due to the prevalence of common design challenges faced by software engineers when determining how to leverage a programming language to address specific problems. It is imperative for software engineers to grasp not only the valuable solutions that design patterns offer but also the potential pitfalls they entail. Consequently, it becomes the software engineer’s responsibility to make well-informed decisions, considering all available information, to determine the most appropriate course of action.