Learning Objectives

  • Understand the Observer pattern concept
  • Implement publish-subscribe mechanisms
  • Use observers for event systems
  • Apply reactive programming patterns
  • Build state management with observers

What is the Observer Pattern?

An Observer (subscriber) registers with a Subject (publisher) to receive notifications when the subject's state changes. It's a one-to-many relationship where one object notifies many dependents.

Basic Implementation

class Subject {
    constructor() {
        this.observers = [];
    }
    
    subscribe(observer) {
        this.observers.push(observer);
    }
    
    unsubscribe(observer) {
        this.observers = this.observers.filter(obs => obs !== observer);
    }
    
    notify(data) {
        this.observers.forEach(observer => observer.update(data));
    }
}

class Observer {
    constructor(name) {
        this.name = name;
    }
    
    update(data) {
        console.log(`${this.name} received:`, data);
    }
}

// Usage
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify('Hello observers!');
// Observer 1 received: Hello observers!
// Observer 2 received: Hello observers!

subject.unsubscribe(observer1);
subject.notify('Only one observer now');
// Observer 2 received: Only one observer now

Event Emitter Pattern

class EventEmitter {
    constructor() {
        this.events = {};
    }
    
    on(event, listener) {
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(listener);
        
        // Return unsubscribe function
        return () => this.off(event, listener);
    }
    
    off(event, listener) {
        if (!this.events[event]) return;
        this.events[event] = this.events[event].filter(l => l !== listener);
    }
    
    emit(event, data) {
        if (!this.events[event]) return;
        this.events[event].forEach(listener => listener(data));
    }
    
    once(event, listener) {
        const onceWrapper = (data) => {
            listener(data);
            this.off(event, onceWrapper);
        };
        this.on(event, onceWrapper);
    }
}

// Usage
const emitter = new EventEmitter();

const unsubscribe = emitter.on('data', (data) => {
    console.log('Received:', data);
});

emitter.on('data', (data) => {
    console.log('Also received:', data);
});

emitter.emit('data', { message: 'Hello' });
// Received: { message: 'Hello' }
// Also received: { message: 'Hello' }

// Use once for one-time listeners
emitter.once('init', () => {
    console.log('Initialized once');
});

emitter.emit('init'); // Logs once
emitter.emit('init'); // Doesn't log

unsubscribe(); // Remove first listener

Real-World Examples

State Management Store

class Store {
    constructor(initialState = {}) {
        this.state = initialState;
        this.listeners = [];
    }
    
    getState() {
        return { ...this.state };
    }
    
    setState(newState) {
        const prevState = this.state;
        this.state = { ...this.state, ...newState };
        this.notify(this.state, prevState);
    }
    
    subscribe(listener) {
        this.listeners.push(listener);
        
        // Return unsubscribe function
        return () => {
            this.listeners = this.listeners.filter(l => l !== listener);
        };
    }
    
    notify(state, prevState) {
        this.listeners.forEach(listener => listener(state, prevState));
    }
}

// Usage
const store = new Store({ count: 0, user: null });

const unsubscribe = store.subscribe((state, prevState) => {
    console.log('State changed from', prevState, 'to', state);
});

store.setState({ count: 1 });
// State changed from { count: 0, user: null } to { count: 1, user: null }

store.setState({ user: { name: 'John' } });
// State changed from { count: 1, user: null } to { count: 1, user: { name: 'John' } }

unsubscribe(); // Stop listening

DOM Observer

class DOMObserver {
    constructor(element) {
        this.element = element;
        this.observers = [];
        this.events = new Set();
    }
    
    observe(event) {
        if (this.events.has(event)) return;
        
        this.events.add(event);
        this.element.addEventListener(event, (e) => {
            this.notify(event, e);
        });
    }
    
    subscribe(callback) {
        this.observers.push(callback);
        
        return () => {
            this.observers = this.observers.filter(obs => obs !== callback);
        };
    }
    
    notify(event, data) {
        this.observers.forEach(observer => observer(event, data));
    }
}

// Usage
const button = document.querySelector('#myButton');
const domObserver = new DOMObserver(button);

domObserver.observe('click');
domObserver.observe('mouseover');

const unsubscribe = domObserver.subscribe((event, data) => {
    console.log(`Button ${event}:`, data);
});

News Publisher

class NewsPublisher {
    constructor() {
        this.subscribers = [];
        this.articles = [];
    }
    
    subscribe(subscriber) {
        this.subscribers.push(subscriber);
        console.log(`${subscriber.name} subscribed`);
    }
    
    unsubscribe(subscriber) {
        this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
        console.log(`${subscriber.name} unsubscribed`);
    }
    
    publish(article) {
        this.articles.push(article);
        this.notifySubscribers(article);
    }
    
    notifySubscribers(article) {
        this.subscribers.forEach(subscriber => {
            subscriber.receive(article);
        });
    }
}

class Subscriber {
    constructor(name) {
        this.name = name;
        this.articles = [];
    }
    
    receive(article) {
        this.articles.push(article);
        console.log(`${this.name} received: ${article.title}`);
    }
}

// Usage
const publisher = new NewsPublisher();

const subscriber1 = new Subscriber('Alice');
const subscriber2 = new Subscriber('Bob');

publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);

publisher.publish({
    title: 'Breaking News',
    content: 'Something important happened'
});
// Alice received: Breaking News
// Bob received: Breaking News

publisher.unsubscribe(subscriber1);

publisher.publish({
    title: 'Update',
    content: 'More news'
});
// Bob received: Update

When to Use Observer Pattern

Good use cases:
  • Event systems and event handling
  • State management (Redux, MobX)
  • Real-time updates and notifications
  • Reactive programming
  • Pub-sub messaging systems
  • Model-View updates (MVC)

Benefits

Best Practices

1. Return unsubscribe function
subscribe(listener) {
    this.listeners.push(listener);
    return () => this.unsubscribe(listener);
}

const unsubscribe = store.subscribe(listener);
unsubscribe(); // Easy cleanup
2. Prevent memory leaks
class Component {
    constructor() {
        this.unsubscribers = [];
    }
    
    mount() {
        const unsub = store.subscribe(this.handleChange);
        this.unsubscribers.push(unsub);
    }
    
    unmount() {
        this.unsubscribers.forEach(unsub => unsub());
        this.unsubscribers = [];
    }
}
3. Use weak references for DOM
class DOMObserver {
    constructor() {
        this.observers = new WeakMap();
    }
}

Key Takeaways