Skip to content

Latest commit

 

History

History
613 lines (453 loc) · 17.8 KB

ObserverPattern.md

File metadata and controls

613 lines (453 loc) · 17.8 KB

Observer Design Pattern

This folder demonstrates the Observer pattern - a behavioral design pattern that defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.


What is the Observer Pattern?

The Observer pattern establishes a subscription mechanism to allow multiple objects to listen and react to events or changes that happen to the object they're observing. It's like a notification system where subscribers get automatically notified when something happens.

Real-world analogy:

  • YouTube channel subscriptions
    • You subscribe to a channel (you become an observer)
    • When the channel uploads a new video (subject changes state)
    • You get a notification automatically (observers are notified)
    • You can unsubscribe anytime (remove observer)
    • The channel doesn't need to know who you are or how many subscribers there are

In programming:

  • One object (the Subject) maintains a list of dependents (the Observers)
  • When the Subject's state changes, it automatically notifies all Observers
  • Observers can subscribe or unsubscribe at runtime
  • Loose coupling - Subject and Observers don't need to know much about each other

Files Overview

src/testWithoutObserver.js - The Problem (Without Observer)

This shows the inefficient approach without using the Observer pattern.

src/models/observer.js - The Observer Interface

The base class that all observers must extend.

src/models/subject.js - The Subject Interface

The base class that all subjects must extend.

src/models/client.js - Concrete Observer

A concrete implementation of an observer (a client waiting for products).

src/models/store.js - Concrete Subject

A concrete implementation of a subject (a store that notifies clients).

src/testWithObserver.js - The Solution (With Observer)

Demonstrates how to properly use the Observer pattern.


The Problem: Hardcoded Notifications

Without Observer Pattern

Location: src/testWithoutObserver.js

class Store {
  constructor() {
    this.availableProduct = false;
  }

  notifyClients() {
    if(this.availableProduct) {
      console.log("Notifying clients about new product availability.");

      console.log("Client A: Product Available!");
      console.log("Client B: Product Available!");
    }
  }

  receiveNewProduct() {
    console.log("Product has arrived at the store.");

    this.availableProduct = true
    this.notifyClients()
  }
}

const store = new Store();
store.receiveNewProduct();

Problems with this approach:

  1. Tight coupling:

    • ❌ Store class knows exactly which clients to notify (Client A, Client B)
    • ❌ Hardcoded client names in the Store class
    • ❌ Can't add or remove clients dynamically
    • ❌ Store is responsible for knowing about specific clients
  2. Violates Open/Closed Principle:

    • ❌ Adding a new client requires modifying the Store class
    • ❌ Can't extend the notification system without changing existing code
    • ❌ Risk of breaking existing functionality
  3. No flexibility:

    • ❌ Clients can't subscribe or unsubscribe
    • ❌ Can't have different types of observers
    • ❌ All clients are always notified (no selective notifications)
  4. Scaling problems:

    • ❌ What if you have 100 clients? Add 100 console.log statements?
    • ❌ What if clients need different notification formats?
    • ❌ What if some clients are removed?
  5. Mixing responsibilities:

    • ❌ Store handles both product management AND client notification details
    • ❌ Violates Single Responsibility Principle

The Solution: Using the Observer Pattern

The Observer pattern separates the notification mechanism from the subject's core logic. The subject maintains a list of observers and notifies them automatically without knowing their details.

File Structure

  1. observer.js - The Observer interface (base class for all observers)
  2. subject.js - The Subject interface (base class for all subjects)
  3. client.js - Concrete Observer (a client that gets notified)
  4. store.js - Concrete Subject (a store that sends notifications)
  5. testWithObserver.js - Client code (demonstrates the pattern)

How Our Code Works

1. observer.js - The Observer Interface

This is the base class that defines the contract all observers must follow.

class Observer {
  update() {
    throw new Error("Method update() must be implemented.");
  }
}

module.exports = Observer;

Key features:

  • Defines the interface: All observers must implement update()
  • Forces implementation: Throws an error if not overridden
  • Common contract: Guarantees all observers respond to notifications the same way

2. subject.js - The Subject Interface

This is the base class that defines how subjects manage their observers.

class Subject {
  constructor() {
    this.observers = [];
  }

  addObservers(observer) {
    throw new Error("Method addObserver must be implemented.");
  }

  removeObserver(observer) {
    throw new Error("Method removeObserver must be implemented.");
  }

  notifyObservers() {
    throw new Error("Method notifyObservers must be implemented.");
  }
}

module.exports = Subject;

Key features:

  • Manages observers: Maintains a list of subscribed observers
  • Subscription methods: addObservers() and removeObserver() for managing the list
  • Notification method: notifyObservers() to alert all observers
  • Template: Subclasses implement the specific logic

3. client.js - Concrete Observer

This class represents a client who wants to be notified about product availability.

const Observer = require("./observer");

class Client extends Observer {
  constructor(name) {
    super();
    this.name = name
  }

  update() {
    console.log(`${this.name}: Product Available!`);
  }
}

module.exports = Client;

Key features:

  • Extends Observer: Implements the update() method
  • Personalized response: Each client has a name and responds individually
  • Encapsulates behavior: Client knows how to handle notifications
  • Independent: Doesn't know about the Store or other Clients

4. store.js - Concrete Subject

This class represents a store that notifies clients when products arrive.

const Subject = require("./subject");

class Store extends Subject {
  constructor() {
    super();
  }

  addObservers(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(sub => sub !== observer)
  }

  notifyObservers() {
    console.log("Notifying clients...");
    this.observers.forEach(observer => observer.update());
  }

  receiveNewProduct() {
    console.log("Product has arrived at the store!");
    this.notifyObservers();
  }
}

module.exports = Store;

Key features:

  • Extends Subject: Implements observer management methods
  • Dynamic subscription: Can add/remove observers at runtime
  • Automatic notification: Calls update() on all observers
  • Decoupled: Doesn't know details about observers, just calls their update() method
  • Business logic: receiveNewProduct() triggers the notification

5. testWithObserver.js - Using the Observer Pattern

This shows how the client code uses the Observer pattern.

const Client = require("./models/client");
const Store = require("./models/store");

const store = new Store();
const clientA = new Client("John");
const clientB = new Client("Megan");
const clientC = new Client("Anna");

store.addObservers(clientA);
store.addObservers(clientB);
store.addObservers(clientC);

store.removeObserver(clientB);

store.receiveNewProduct();
// Output:
// Product has arrived at the store!
// Notifying clients...
// John: Product Available!
// Anna: Product Available!

What's happening:

  1. Create a Store (the subject)
  2. Create multiple Client instances (the observers)
  3. Subscribe clients to the store using addObservers()
  4. Remove one client using removeObserver()
  5. When receiveNewProduct() is called, the store automatically notifies all subscribed clients
  6. Each client's update() method is called automatically

Comparison: Before vs After

❌ Before (testWithoutObserver.js)

class Store {
  notifyClients() {
    if(this.availableProduct) {
      console.log("Client A: Product Available!");
      console.log("Client B: Product Available!");
      // Need to hardcode every client!
    }
  }

  receiveNewProduct() {
    this.availableProduct = true
    this.notifyClients()
  }
}

// Can't add clients dynamically
// Can't remove clients
// Store knows about specific clients

Problems:

  • Hardcoded client notifications
  • Can't add/remove clients
  • Tight coupling
  • Violates SRP and OCP

✅ After (testWithObserver.js + Observer Pattern)

// Observer interface
class Observer {
  update() { /* must implement */ }
}

// Concrete observer
class Client extends Observer {
  constructor(name) {
    super();
    this.name = name;
  }

  update() {
    console.log(`${this.name}: Product Available!`);
  }
}

// Subject manages observers
class Store extends Subject {
  addObservers(observer) {
    this.observers.push(observer);
  }

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

  receiveNewProduct() {
    console.log("Product has arrived!");
    this.notifyObservers();  // Automatically notify all
  }
}

// Dynamic subscription
store.addObservers(clientA);
store.addObservers(clientB);
store.removeObserver(clientB);

// Automatic notification
store.receiveNewProduct();

Benefits:

  • ✅ Dynamic subscription/unsubscription
  • ✅ Loose coupling (Store doesn't know Client details)
  • ✅ Scalable (handle any number of observers)
  • ✅ Flexible (different observer types possible)
  • ✅ Follows SRP and OCP

How Everything Works Together

The Flow:

  1. Observers register with the Subject using addObservers()
  2. Subject maintains a list of all subscribed observers
  3. Subject's state changes (e.g., new product arrives)
  4. Subject calls notifyObservers()
  5. Subject iterates through all observers and calls their update() method
  6. Each observer reacts according to its own implementation
  7. Observers can unsubscribe using removeObserver() at any time

Visual Representation:

              Subject (Store)
         ┌──────────────────────┐
         │ observers: []        │
         │                      │
         │ addObservers()       │
         │ removeObserver()     │
         │ notifyObservers()    │
         │ receiveNewProduct()  │
         └──────────┬───────────┘
                    │
         ┌──────────┼──────────┐
         ↓          ↓          ↓
    Observer1   Observer2   Observer3
    (Client)    (Client)    (Client)
    ┌──────┐    ┌──────┐    ┌──────┐
    │John  │    │Megan │    │Anna  │
    │      │    │      │    │      │
    │update│    │update│    │update│
    └──────┘    └──────┘    └──────┘

When receiveNewProduct() is called:
1. Subject.notifyObservers()
2. Calls update() on each observer
3. Each observer responds independently

Key Design Principles

1. Open/Closed Principle

"Software entities should be open for extension but closed for modification."

✅ With Observer Pattern:

  • Want to add a new client? Just create a new Client instance and call addObservers()
  • No need to modify Store, Observer, Subject, or other clients
  • Extend functionality without touching existing code

❌ Without Observer Pattern:

  • Must open Store class and add more hardcoded console.log statements
  • Risk breaking existing functionality

2. Single Responsibility Principle

"A class should have only one reason to change."

✅ With Observer Pattern:

  • Store - manages products and observer list
  • Client - handles its own notification response
  • Observer - defines observer interface
  • Subject - defines subject interface
  • Each class has a clear, single responsibility

❌ Without Observer Pattern:

  • Store knows about product management AND client notification details
  • Multiple reasons to change

3. Dependency Inversion Principle

"Depend on abstractions, not concretions."

✅ With Observer Pattern:

  • Store depends on Observer interface, not specific Client implementations
  • Can add any observer type that implements the interface
  • Loose coupling through abstraction

❌ Without Observer Pattern:

  • Store directly implements specific client notifications
  • Tight coupling to concrete implementations

4. Loose Coupling

"Minimize dependencies between components."

✅ With Observer Pattern:

  • Subject and Observers are loosely coupled
  • Subject only knows observers implement update()
  • Observers don't know about each other or the subject's internals
  • Can change either side without affecting the other

❌ Without Observer Pattern:

  • Tight coupling between Store and specific clients
  • Changes ripple through the system

Benefits of the Observer Pattern

1. Loose Coupling

  • Subject and observers are independent
  • Subject doesn't need to know observer details
  • Easy to change or extend either side

2. Dynamic Relationships

  • Subscribe/unsubscribe at runtime
  • Add or remove observers on the fly
  • Flexible notification lists

3. Broadcast Communication

  • One-to-many notification automatically
  • Single change triggers multiple updates
  • Efficient event propagation

4. Open/Closed Principle

  • Add new observers without modifying the subject
  • Extend functionality through new observer types
  • Existing code remains stable

5. Reusability

  • Same observer can watch multiple subjects
  • Same subject can notify different observer types
  • Compose complex notification systems

6. Separation of Concerns

  • Subject focuses on its core responsibility
  • Observers handle their own responses
  • Clear boundaries between components

7. Event-Driven Architecture

  • Enables reactive programming
  • Push-based updates (instead of polling)
  • Foundation for event systems

Real-World Use Cases

  1. Event Listeners (UI):

    • Button clicks, mouse movements, keyboard events
    • Multiple listeners can respond to the same event
    • Add/remove listeners dynamically
  2. Model-View Architecture (MVC, MVVM):

    • Model changes → Views automatically update
    • Multiple views can display the same model
    • Foundation of reactive frameworks (React, Vue, Angular)
  3. Messaging Systems:

    • Pub/Sub pattern (Redis, RabbitMQ, Kafka)
    • Publishers send messages → Subscribers receive them
    • Decoupled communication between services
  4. Stock Market:

    • Stock price changes → Notify all subscribers
    • Investors, apps, bots watching the same stock
    • Real-time updates to multiple parties
  5. Social Media:

    • New post → Notify all followers
    • YouTube subscriptions, Twitter followers, Instagram
    • One update, many notifications
  6. Newsletter/Email Lists:

    • New content published → Email all subscribers
    • Subscribers can join/leave anytime
    • Broadcasting to a dynamic audience

Potential Drawbacks

  1. Notification order is uncertain:

    • ⚠️ Observers are notified in array order, but this might not be guaranteed
    • Can't rely on specific notification sequences
  2. Memory leaks:

    • ⚠️ Observers must unsubscribe when no longer needed
    • Forgotten observers can cause memory leaks
  3. Performance overhead:

    • ⚠️ With many observers, notifications can be slow
    • Every state change triggers all observers
  4. Unexpected updates:

    • ⚠️ Cascading updates can be hard to track
    • Observer's update might trigger other updates

When to Use the Observer Pattern

Use it when:

  • ✅ Changes to one object require changing others, and you don't know how many
  • ✅ An object should be able to notify others without knowing who they are
  • ✅ You need a subscription/notification mechanism
  • ✅ You want to implement event handling systems
  • ✅ You're building reactive or event-driven architectures

Don't use it when:

  • ❌ The relationship between subject and observers is always the same
  • ❌ You have a simple one-to-one notification
  • ❌ Performance is critical and you have many observers
  • ❌ The overhead of managing subscriptions isn't worth it

Summary

The Observer pattern is about establishing a subscription mechanism where multiple objects can listen to and react to changes in another object. Like YouTube subscriptions, observers get automatically notified when the subject they're watching changes state.

Key Takeaways:

  1. One-to-Many Dependency:

    • One subject notifies many observers
    • Automatic broadcast communication
  2. Loose Coupling:

    • Subject doesn't know observer details
    • Observers don't know about each other
  3. Dynamic Subscription:

    • Subscribe/unsubscribe at runtime
    • Flexible notification lists
  4. Event-Driven:

    • Push-based updates
    • Reactive architecture foundation
  5. Design Principles:

    • Open/Closed Principle
    • Single Responsibility Principle
    • Dependency Inversion Principle

The implementation in this folder demonstrates a proper Observer pattern where the Store (subject) maintains a list of Client observers and automatically notifies them when products arrive, compared to the old testWithoutObserver.js approach that hardcoded specific client notifications.