Hone logo
Hone
Problems

Implementing the Observer Pattern in TypeScript

The Observer pattern is a behavioral design pattern that defines a one-to-many dependency between objects. When an object (the subject) changes state, all its dependents (the observers) are notified and updated automatically. This challenge asks you to implement the core types for the Observer pattern in TypeScript, focusing on type safety and flexibility.

Problem Description

You are tasked with creating TypeScript types that represent the core components of the Observer pattern: Subject and Observer. The Subject type should manage a list of observers and provide methods for adding, removing, and notifying observers. The Observer type should define a method for receiving updates from the subject. Your implementation should be generic, allowing it to work with different types of subjects and observers. The notification mechanism should be type-safe, ensuring that observers receive updates with the correct data type.

Key Requirements:

  • Subject<T>:
    • Must maintain a list of Observer<T> objects.
    • Must have an addObserver(observer: Observer<T>): void method to register a new observer.
    • Must have a removeObserver(observer: Observer<T>): void method to unregister an observer.
    • Must have a notify(data: T): void method to notify all registered observers with a given data payload of type T.
  • Observer<T>:
    • Must have an update(data: T): void method that receives the data payload from the subject.

Expected Behavior:

  • Adding an observer should allow it to receive notifications from the subject.
  • Removing an observer should prevent it from receiving further notifications.
  • The notify method should iterate through the list of observers and call their update method with the provided data.
  • The types should be generic, allowing for different data types to be observed.

Edge Cases to Consider:

  • Attempting to add the same observer multiple times. (Consider whether this should be allowed or treated as a no-op).
  • Attempting to remove an observer that is not registered. (Consider whether this should be allowed or treated as a no-op).
  • The subject having no observers. The notify method should handle this gracefully (e.g., do nothing).

Examples

Example 1:

// Subject: NumberSubject
// Observer: NumberObserver

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

class NumberSubject implements Subject<number> {
  private observers: NumberObserver<number>[] = [];

  addObserver(observer: NumberObserver<number>): void {
    this.observers.push(observer);
  }

  removeObserver(observer: NumberObserver<number>): void {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data: number): void {
    this.observers.forEach(observer => observer.update(data));
  }
}

class NumberDisplay implements NumberObserver<number> {
  update(data: number): void {
    console.log("Display updated:", data);
  }
}

const subject = new NumberSubject();
const display = new NumberDisplay();

subject.addObserver(display);
subject.notify(10); // Output: Display updated: 10
subject.notify(20); // Output: Display updated: 20

subject.removeObserver(display);
subject.notify(30); // No output

Explanation: A NumberSubject is created and an observer NumberDisplay is added. Notifications are sent, updating the display. Removing the observer prevents further updates.

Example 2:

// Subject: StringSubject
// Observer: StringObserver

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

class StringSubject implements Subject<string> {
  private observers: StringObserver<string>[] = [];

  addObserver(observer: StringObserver<string>): void {
    this.observers.push(observer);
  }

  removeObserver(observer: StringObserver<string>): void {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data: string): void {
    this.observers.forEach(observer => observer.update(data));
  }
}

class StringLogger implements StringObserver<string> {
  update(data: string): void {
    console.log("String logged:", data);
  }
}

const stringSubject = new StringSubject();
const stringLogger = new StringLogger();

stringSubject.addObserver(stringLogger);
stringSubject.notify("Hello, world!"); // Output: String logged: Hello, world!

Explanation: Demonstrates the pattern with string data.

Constraints

  • The solution must be written in TypeScript.
  • The Subject and Observer types must be generic.
  • The notify method must iterate through all registered observers and call their update method.
  • The code should be well-structured and readable.
  • No external libraries are allowed.

Notes

  • Focus on creating the core types for the Observer pattern. You don't need to implement a specific use case.
  • Consider using TypeScript's type inference to simplify the code.
  • Think about how to handle potential errors or edge cases gracefully.
  • The Observer interface only needs the update method. You can add other methods if you feel it's necessary, but it's not required.
  • The equality check in removeObserver uses strict equality (!==). Consider if this is appropriate for all use cases. A more robust solution might involve a custom comparison function.
Loading editor...
typescript