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:
- Define an
Observerinterface: This interface will specify the contract for any object that wishes to be notified of changes. It should have a method to receive updates. - Define a
Subjectinterface: 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. - Implement a concrete
Subjectclass: This class will be a concrete implementation of theSubjectinterface, managing its observers and broadcasting state changes. - Implement concrete
Observerclasses: Create at least two distinct concreteObserverclasses that can be attached to theSubjectand react to updates. - Demonstrate the pattern: Show how a
Subjectcan be updated and how its registeredObservers 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
SubjectandObservertypes flexible enough to handle different data types being passed as updates. - Clear Separation of Concerns: The
Subjectshould not know the concrete types of its observers, only that they implement theObserverinterface. Observers should not directly interact with theSubject'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
Subjectshould be able to hold an arbitrary number ofObserverinstances. - The
SubjectandObservertypes 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
Twill be used in both theObserverandSubjectinterfaces and classes. - Your solution should include the definitions of the
ObserverandSubjectinterfaces, along with at least one concreteSubjectand at least two concreteObserverimplementations. - 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.