Hone logo
Hone
Problems

Implementing the Observer Pattern in TypeScript

The Observer pattern is a fundamental behavioral design pattern that allows an object (the subject) to notify a list of dependent objects (observers) about any changes in its state. This is incredibly useful for decoupling components, enabling event-driven architectures, and creating reactive systems. Your challenge is to implement the core types for this pattern in TypeScript.

Problem Description

You are tasked with creating a robust and type-safe implementation of the Observer pattern using TypeScript. This involves defining the interfaces and classes that will form the foundation of any system utilizing this pattern.

What needs to be achieved:

  1. Define an Observer interface: This interface will specify the contract for any object that wishes to be notified of changes. It should have a method to receive updates.
  2. Define a Subject interface: This interface will specify the contract for any object that maintains a list of observers and notifies them. It should have methods to attach, detach, and notify observers.
  3. Implement a concrete Subject class: This class will be a concrete implementation of the Subject interface, managing its observers and broadcasting state changes.
  4. Implement concrete Observer classes: Create at least two distinct concrete Observer classes that can be attached to the Subject and react to updates.
  5. Demonstrate the pattern: Show how a Subject can be updated and how its registered Observers are notified.

Key Requirements:

  • Type Safety: Leverage TypeScript's features to ensure type safety throughout the pattern. Observers should be able to receive specific types of data from the subject.
  • Generics: Use generics to make the Subject and Observer types flexible enough to handle different data types being passed as updates.
  • Clear Separation of Concerns: The Subject should not know the concrete types of its observers, only that they implement the Observer interface. Observers should not directly interact with the Subject's internal state, only receive updates.

Expected Behavior:

  • When an observer is attached to a subject, it should be added to the subject's list of subscribers.
  • When an observer is detached from a subject, it should be removed from the subject's list.
  • When the subject's state changes (or it explicitly notifies observers), all attached observers should receive the update through their designated notification method.
  • Different observers should be able to react to the same update in different ways.

Edge Cases to Consider:

  • Detaching an observer that has not been attached.
  • Attaching the same observer multiple times (should ideally be handled gracefully, e.g., by not adding duplicates or by returning a boolean indicating success/failure).

Examples

Example 1: Basic Notification

Let's imagine a WeatherStation (Subject) that reports temperature changes to two observers: ConsoleLogger and EmailNotifier.

Input:

class TemperatureUpdate {
  constructor(public temperature: number) {}
}

interface Subject<T> {
  attach(observer: Observer<T>): void;
  detach(observer: Observer<T>): void;
  notify(data: T): void;
}

interface Observer<T> {
  update(data: T): void;
}

class WeatherStation implements Subject<TemperatureUpdate> {
  private observers: Observer<TemperatureUpdate>[] = [];
  private currentTemperature: number = 0;

  attach(observer: Observer<TemperatureUpdate>): void {
    this.observers.push(observer);
  }

  detach(observer: Observer<TemperatureUpdate>): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(data: TemperatureUpdate): void {
    for (const observer of this.observers) {
      observer.update(data);
    }
  }

  setTemperature(temperature: number): void {
    this.currentTemperature = temperature;
    this.notify(new TemperatureUpdate(this.currentTemperature));
  }
}

class ConsoleLogger implements Observer<TemperatureUpdate> {
  update(data: TemperatureUpdate): void {
    console.log(`[Logger] Temperature changed to: ${data.temperature}°C`);
  }
}

class EmailNotifier implements Observer<TemperatureUpdate> {
  update(data: TemperatureUpdate): void {
    console.log(`[Email] Sending alert: Temperature is now ${data.temperature}°C.`);
  }
}

const weatherStation = new WeatherStation();
const logger = new ConsoleLogger();
const emailer = new EmailNotifier();

weatherStation.attach(logger);
weatherStation.attach(emailer);

weatherStation.setTemperature(25);

Output:

[Logger] Temperature changed to: 25°C
[Email] Sending alert: Temperature is now 25°C.

Explanation:

The WeatherStation is the subject. We attach a ConsoleLogger and an EmailNotifier as observers. When setTemperature(25) is called, the WeatherStation notifies both attached observers, and they execute their update methods, printing to the console.

Example 2: Detaching an Observer

Continuing from Example 1, let's detach the email notifier and see how subsequent changes are handled.

Input:

// ... (previous code for WeatherStation, ConsoleLogger, EmailNotifier) ...

const weatherStation = new WeatherStation();
const logger = new ConsoleLogger();
const emailer = new EmailNotifier();

weatherStation.attach(logger);
weatherStation.attach(emailer);

console.log("--- First update ---");
weatherStation.setTemperature(25);

console.log("\n--- Detaching EmailNotifier ---");
weatherStation.detach(emailer);

console.log("\n--- Second update ---");
weatherStation.setTemperature(30);

Output:

--- First update ---
[Logger] Temperature changed to: 25°C
[Email] Sending alert: Temperature is now 25°C.

--- Detaching EmailNotifier ---

--- Second update ---
[Logger] Temperature changed to: 30°C

Explanation:

After detaching the EmailNotifier, only the ConsoleLogger receives the update when the temperature changes to 30°C.

Example 3: Handling an Unattached Observer

Input:

// ... (previous code for WeatherStation, ConsoleLogger, EmailNotifier) ...

const weatherStation = new WeatherStation();
const logger = new ConsoleLogger();
const anotherLogger = new ConsoleLogger(); // An observer not yet attached

weatherStation.attach(logger);

console.log("--- Attempting to detach an unattached observer ---");
weatherStation.detach(anotherLogger); // This should be handled gracefully

console.log("\n--- Setting temperature ---");
weatherStation.setTemperature(15);

Output:

--- Attempting to detach an unattached observer ---

--- Setting temperature ---
[Logger] Temperature changed to: 15°C

Explanation:

The detach method gracefully handles the case where anotherLogger was not attached to weatherStation. The subsequent setTemperature call only notifies the logger that was actually attached.

Constraints

  • The Subject should be able to hold an arbitrary number of Observer instances.
  • The Subject and Observer types should be generic, allowing them to work with any data type passed during notifications.
  • The implementation should be efficient for typical use cases, with notification taking O(N) time where N is the number of observers.
  • No external libraries are allowed for implementing the core Observer pattern logic.

Notes

  • Consider how you will manage the list of observers within the Subject. An array is a common choice.
  • Think about how the generic type parameter T will be used in both the Observer and Subject interfaces and classes.
  • Your solution should include the definitions of the Observer and Subject interfaces, along with at least one concrete Subject and at least two concrete Observer implementations.
  • The focus is on the types and the mechanism of the observer pattern. You don't need to implement complex business logic within the observers.
  • When implementing detach, ensure it doesn't throw an error if the observer isn't found.
Loading editor...
typescript