Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

Observer Pattern 👁️

Overview

The Observer Pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically and updated.

Difficulty: ⭐ Easy
Category: Behavioral Pattern
Interview Frequency: ⭐⭐⭐⭐ Very Common

Problem It Solves

Imagine you have:

  • A data object (e.g., stock price, user status, game state)
  • Multiple objects that need to react when the data changes
  • You want loose coupling—objects shouldn't know about each other

Without Observer, you'd:

  1. Call update methods manually on every dependent
  2. Tightly couple objects
  3. Forget to notify some observers
  4. Have brittle code that breaks when requirements change

Real-World Analogy

Think of a newspaper subscription:

  • Subject: Newspaper publisher
  • Observers: Subscribers
  • When a new issue is published, all subscribers are automatically notified
  • Subscribers don't need to constantly check; they're pushed the news

When to Use

Use Observer Pattern when:

  • Multiple objects need to react to state changes
  • You don't know in advance how many observers you'll have
  • Objects should be loosely coupled
  • You need event-driven architecture
  • You want to avoid tight coupling

Don't use when:

  • Only one or two dependents
  • Observers are tightly related (use Strategy instead)
  • Performance is critical (notification overhead)

Implementation

See observer.ts for complete implementation.

Key Components

  1. Subject/Observable: Maintains list of observers, notifies them
  2. Observer: Interface for receiving notifications
  3. ConcreteObserver: Implements Observer interface
  4. ConcreteSubject: Maintains state, notifies observers when changed

TypeScript Example

// Observer interface
interface Observer {
  update(subject: Subject): void;
}

// Subject
class Subject {
  private observers: Observer[] = [];
  private state: string = "";

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

  detach(observer: Observer): void {
    this.observers = this.observers.filter((o) => o !== observer);
  }

  setState(state: string): void {
    this.state = state;
    this.notifyObservers();
  }

  getState(): string {
    return this.state;
  }

  private notifyObservers(): void {
    this.observers.forEach((observer) => observer.update(this));
  }
}

// Concrete Observer
class ConcreteObserver implements Observer {
  constructor(private name: string) {}

  update(subject: Subject): void {
    console.log(`${this.name} received update: ${subject.getState()}`);
  }
}

// Usage
const subject = new Subject();
const obs1 = new ConcreteObserver("Observer 1");
const obs2 = new ConcreteObserver("Observer 2");

subject.attach(obs1);
subject.attach(obs2);

subject.setState("New State"); // Both observers notified

Benefits

Loose Coupling: Subject doesn't know concrete observer types
Dynamic Relationships: Attach/detach observers at runtime
Reusable: Subject and observers can be reused independently
Separation of Concerns: Subject only manages state; observers handle reactions
One-to-Many: One subject can notify many observers

Drawbacks

❌ Observers notified in unpredictable order
❌ Memory leaks if observers not detached
❌ Performance overhead with many observers
❌ Hard to debug (implicit dependencies)

Interview Questions & Answers

Q1: What's the difference between Observer and Pub-Sub (Message Bus)?

Answer:

  • Observer: Direct coupling between Subject and Observer
    • Subject directly calls observer.update()
    • Synchronous
    • Example: EventEmitter.on()
subject.attach(observer); // Direct reference
subject.setState(newState); // Directly notifies
  • Pub-Sub: Decoupled through a message broker
    • Publishers and subscribers don't know each other
    • Can be asynchronous
    • Example: Message queues, Event buses
// Publish
eventBus.publish("stateChanged", newState);

// Subscribe
eventBus.subscribe("stateChanged", (data) => {
  // Handle
});

Q2: How do you prevent memory leaks in Observer pattern?

Answer: Always detach observers when they're no longer needed:

class Observer implements IObserver {
  subscribe(subject: Subject): void {
    subject.attach(this);
  }

  unsubscribe(subject: Subject): void {
    subject.detach(this);
  }

  destroy(): void {
    // Cleanup
  }
}

// Usage
const observer = new Observer();
observer.subscribe(subject);

// Later, when done
observer.unsubscribe(subject);
observer.destroy();

Q3: Should the Observer pattern be synchronous or asynchronous?

Answer: Depends on use case:

Synchronous (Direct Observer):

  • Simple, immediate updates
  • Issues: Slow observer blocks others, tight coupling
notifyObservers(): void {
  this.observers.forEach(o => o.update(this)); // Blocks until all done
}

Asynchronous (Message Bus):

  • Non-blocking, better performance
  • Issues: Harder to debug, ordering issues
async notifyObservers(): Promise<void> {
  await Promise.all(this.observers.map(o => o.update(this)));
}

Best Practice: Use synchronous for simple cases, async with message bus for complex systems.

Q4: Observer vs Listener—what's the difference?

Answer: Mostly terminology:

  • Observer: More formal, emphasizes pattern
  • Listener: Common in event systems, implies listening for events
  • Both are essentially the same pattern
// Observer terminology
interface Observer {
  update(subject: Subject): void;
}

// Listener terminology
type EventListener<T> = (event: T) => void;

Q5: How do you pass data to observers?

Answer: Several approaches:

  1. Pull Model: Observer queries subject
update(subject: Subject): void {
  const state = subject.getState();
}
  1. Push Model: Subject sends data to observer
update(data: string): void {
  console.log(data);
}
notifyObservers(): void {
  this.observers.forEach(o => o.update(this.state));
}
  1. Event Object: Send complete event
interface StateChangedEvent {
  oldState: string;
  newState: string;
  timestamp: Date;
}
update(event: StateChangedEvent): void {
  // Handle event
}

Real-World Use Cases

  1. UI Frameworks: Reactive components, Vue/React hooks
  2. Event Systems: DOM events, Node.js EventEmitter
  3. Stock Market: Price changes → trader notifications
  4. Chat Systems: New message → all subscribers notified
  5. Game Development: Player actions → UI, sound, physics updates
  6. MVC/MVVM: Model changes → all views update
  7. Notification Systems: Email/SMS alerts
  8. Real-time Data: WebSocket updates to multiple clients

Common Patterns

1. WeakMap for Memory Safety

private observersMap = new WeakMap<Observer, boolean>();

attach(observer: Observer): void {
  this.observersMap.set(observer, true);
}

detach(observer: Observer): void {
  this.observersMap.delete(observer);
}

2. Priority-based Notification

interface PriorityObserver {
  update(subject: Subject): void;
  priority: number;
}

notifyObservers(): void {
  const sorted = this.observers.sort((a, b) =>
    b.priority - a.priority
  );
  sorted.forEach(o => o.update(this));
}

3. Conditional Notifications

notifyObservers(): void {
  this.observers.forEach(observer => {
    if (observer.isInterested(this.state)) {
      observer.update(this);
    }
  });
}

4. Error Handling

notifyObservers(): void {
  this.observers.forEach(observer => {
    try {
      observer.update(this);
    } catch (error) {
      console.error(`Observer error: ${error}`);
      // Don't let one observer break others
    }
  });
}

Code Smell Fixes

Before:

class UserService {
  updateUser(id: string, data: any) {
    const user = this.getUser(id);
    user.update(data);

    // Manual notification - what if we forget?
    emailService.sendUpdateEmail(user);
    uiComponent.refresh(user);
    analyticsService.track("user-updated", user);
    // And we need to modify this every time!
  }
}

After:

class UserService extends Subject {
  updateUser(id: string, data: any) {
    const user = this.getUser(id);
    user.update(data);
    this.notifyObservers(user); // All observers notified automatically
  }
}

// Subscribe once
const emailObserver = new EmailObserver();
const uiObserver = new UIObserver();
const analyticsObserver = new AnalyticsObserver();

userService.attach(emailObserver);
userService.attach(uiObserver);
userService.attach(analyticsObserver);

// Now updates automatically notify all!
userService.updateUser("123", newData);

Testing Tips

// Easy to test—mock observers
const mockObserver = {
  update: jest.fn(),
};

subject.attach(mockObserver);
subject.setState("new");

expect(mockObserver.update).toHaveBeenCalled();

Related Patterns

  • Mediator: Encapsulates how set of objects interact
  • Pub-Sub: Looser coupling than Observer
  • Command: Can be used with Observer for queuing
  • Strategy: Can work with Observer for dynamic behavior

Summary

Observer Pattern is essential for:

  1. Event-driven architecture
  2. Building reactive systems
  3. Decoupling components
  4. Maintaining separation of concerns

Master it by understanding when to use synchronous vs async, and always remember to detach observers to prevent leaks.

Key Takeaway: "When one object's state changes, tell everyone interested—without them asking."

Next: Try the Decorator Pattern