Observer Pattern - Defining One-to-Many Dependencies Between Objects

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand the Observer pattern and its role in creating loosely coupled systems
  • Implement push and pull models of observer notification
  • Apply Observer pattern in real-world scenarios like MVC architectures and event systems
  • Recognize the relationship between Observer pattern and modern reactive programming
  • Design efficient notification systems that avoid common pitfalls like memory leaks

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Think about how you get notifications on your phone - news alerts, social media updates, stock price changes. You subscribe to these services once, and they automatically notify you when something interesting happens. How is this similar to the way objects might need to communicate in software?

Lesson Overview

  • Duration: 120 minutes
  • Prerequisites: Object-oriented programming, interfaces, basic understanding of events
  • Materials Needed: Development environment with Java, Python, or JavaScript support

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Problem Definition - Issues with tight coupling and polling for state changes
  2. Observer Pattern Structure - Subject, Observer, and notification mechanisms
  3. Implementation Variants - Push vs Pull models, typed observers, event objects
  4. Real-world Applications - MVC patterns, event-driven architectures, reactive systems
  5. Advanced Topics - Weak references, thread safety, and performance considerations
  6. Modern Alternatives - Reactive streams, pub-sub systems, and event buses

Success Criteria:

By the end of this session, you should be able to implement robust observer systems and understand when to use observer pattern versus modern alternatives.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

What is the Observer Pattern?

The Observer pattern defines a one-to-many dependency between objects so that when one object (Subject) changes state, all its dependents (Observers) are automatically notified and updated. It promotes loose coupling by ensuring that subjects and observers interact through well-defined interfaces.

Real-World Analogy: Consider a newspaper subscription service. You (Observer) subscribe to a newspaper (Subject). When a new edition is published, the newspaper automatically delivers it to all subscribers. You don’t need to constantly check if a new edition is available, and the newspaper doesn’t need to know anything specific about you other than your delivery address.

Why This Matters:

  • Loose Coupling: Subjects and observers are independent and can evolve separately
  • Dynamic Relationships: Observers can subscribe/unsubscribe at runtime
  • Broadcast Communication: One subject can notify multiple observers efficiently
  • Separation of Concerns: Business logic is separated from presentation/notification logic

Problems Observer Pattern Solves

Problem 1: Tight Coupling Between Objects

// Bad: Tight coupling between Model and View
public class StockPrice {
    private String symbol;
    private double price;
    private StockDisplay display; // Tightly coupled to specific display
    private PriceAlert alert;     // Hard to add new consumers
    
    public void setPrice(double price) {
        this.price = price;
        // Directly calling specific objects - hard to extend
        display.updatePrice(symbol, price);
        alert.checkPrice(symbol, price);
        // What if we want to add more displays or alerts?
    }
}

Problem 2: Polling for State Changes

// Bad: Inefficient polling approach
public class PriceMonitor {
    private StockPrice stock;
    
    public void startMonitoring() {
        while (true) {
            double currentPrice = stock.getPrice();
            if (currentPrice != lastKnownPrice) {
                // Process price change
                handlePriceChange(currentPrice);
                lastKnownPrice = currentPrice;
            }
            Thread.sleep(1000); // Waste CPU cycles checking every second
        }
    }
}

Problem 3: Inflexible Notification System

// Bad: Hard-coded notification logic
public class OrderSystem {
    public void processOrder(Order order) {
        // Process order...
        
        // Hard-coded notifications - can't be customized
        emailService.sendOrderConfirmation(order);
        smsService.sendOrderAlert(order);
        inventoryService.updateStock(order);
        // Adding new notification types requires code changes
    }
}

Key Terminology

  • Subject (Observable): Object that maintains a list of observers and notifies them of state changes
  • Observer: Interface that defines the update method called when subject changes
  • ConcreteSubject: Specific implementation of subject with actual state
  • ConcreteObserver: Specific implementation that responds to subject notifications
  • Push Model: Subject sends detailed information about the change to observers
  • Pull Model: Subject only notifies that a change occurred; observers pull details

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Observer Pattern Structure

Classic Observer Pattern Implementation

import java.util.*;

// Observer interface
interface Observer {
    void update(Object data);
}

// Subject interface
interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

// Concrete Subject
class StockPrice implements Subject {
    private String symbol;
    private double price;
    private List<Observer> observers;
    
    public StockPrice(String symbol) {
        this.symbol = symbol;
        this.observers = new ArrayList<>();
    }
    
    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    
    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    
    @Override
    public void notifyObservers() {
        // Create notification data
        Map<String, Object> data = new HashMap<>();
        data.put("symbol", symbol);
        data.put("price", price);
        data.put("timestamp", new Date());
        
        // Notify all observers
        for (Observer observer : observers) {
            observer.update(data);
        }
    }
    
    public void setPrice(double price) {
        this.price = price;
        notifyObservers(); // Automatic notification
    }
    
    // Getters
    public String getSymbol() { return symbol; }
    public double getPrice() { return price; }
}

// Concrete Observers
class PriceDisplay implements Observer {
    private String name;
    
    public PriceDisplay(String name) {
        this.name = name;
    }
    
    @Override
    public void update(Object data) {
        @SuppressWarnings("unchecked")
        Map<String, Object> priceData = (Map<String, Object>) data;
        System.out.printf("[%s] %s: $%.2f at %s%n", 
                         name,
                         priceData.get("symbol"),
                         priceData.get("price"),
                         priceData.get("timestamp"));
    }
}

class PriceAlert implements Observer {
    private double threshold;
    
    public PriceAlert(double threshold) {
        this.threshold = threshold;
    }
    
    @Override
    public void update(Object data) {
        @SuppressWarnings("unchecked")
        Map<String, Object> priceData = (Map<String, Object>) data;
        double price = (Double) priceData.get("price");
        String symbol = (String) priceData.get("symbol");
        
        if (price > threshold) {
            System.out.printf("🔔 ALERT: %s price $%.2f exceeds threshold $%.2f!%n",
                             symbol, price, threshold);
        }
    }
}

class TradingBot implements Observer {
    private double buyThreshold;
    private double sellThreshold;
    
    public TradingBot(double buyThreshold, double sellThreshold) {
        this.buyThreshold = buyThreshold;
        this.sellThreshold = sellThreshold;
    }
    
    @Override
    public void update(Object data) {
        @SuppressWarnings("unchecked")
        Map<String, Object> priceData = (Map<String, Object>) data;
        double price = (Double) priceData.get("price");
        String symbol = (String) priceData.get("symbol");
        
        if (price <= buyThreshold) {
            System.out.printf("🤖 Trading Bot: BUY %s at $%.2f%n", symbol, price);
        } else if (price >= sellThreshold) {
            System.out.printf("🤖 Trading Bot: SELL %s at $%.2f%n", symbol, price);
        }
    }
}

// Demonstration
public class StockMarketDemo {
    public static void main(String[] args) {
        // Create stock
        StockPrice appleStock = new StockPrice("AAPL");
        
        // Create observers
        PriceDisplay mainDisplay = new PriceDisplay("Main Display");
        PriceDisplay mobileDisplay = new PriceDisplay("Mobile App");
        PriceAlert highPriceAlert = new PriceAlert(150.0);
        TradingBot bot = new TradingBot(140.0, 160.0);
        
        // Subscribe observers
        appleStock.addObserver(mainDisplay);
        appleStock.addObserver(mobileDisplay);
        appleStock.addObserver(highPriceAlert);
        appleStock.addObserver(bot);
        
        // Simulate price changes
        System.out.println("=== Stock Price Updates ===");
        appleStock.setPrice(145.50);
        System.out.println();
        
        appleStock.setPrice(138.75);
        System.out.println();
        
        appleStock.setPrice(155.20);
        System.out.println();
        
        // Unsubscribe an observer
        appleStock.removeObserver(mobileDisplay);
        System.out.println("Mobile display unsubscribed");
        
        appleStock.setPrice(162.10);
    }
}

Type-Safe Observer with Generics

// Generic Observer interface
interface TypedObserver<T> {
    void update(T data);
}

// Generic Subject interface
interface TypedSubject<T> {
    void addObserver(TypedObserver<T> observer);
    void removeObserver(TypedObserver<T> observer);
    void notifyObservers(T data);
}

// Event class for type safety
class PriceChangeEvent {
    private final String symbol;
    private final double oldPrice;
    private final double newPrice;
    private final Date timestamp;
    
    public PriceChangeEvent(String symbol, double oldPrice, double newPrice) {
        this.symbol = symbol;
        this.oldPrice = oldPrice;
        this.newPrice = newPrice;
        this.timestamp = new Date();
    }
    
    // Getters
    public String getSymbol() { return symbol; }
    public double getOldPrice() { return oldPrice; }
    public double getNewPrice() { return newPrice; }
    public Date getTimestamp() { return timestamp; }
    public double getPriceChange() { return newPrice - oldPrice; }
    public double getPriceChangePercent() { return (newPrice - oldPrice) / oldPrice * 100; }
}

// Type-safe Stock implementation
class TypeSafeStock implements TypedSubject<PriceChangeEvent> {
    private final String symbol;
    private double price;
    private final List<TypedObserver<PriceChangeEvent>> observers;
    
    public TypeSafeStock(String symbol, double initialPrice) {
        this.symbol = symbol;
        this.price = initialPrice;
        this.observers = new ArrayList<>();
    }
    
    @Override
    public void addObserver(TypedObserver<PriceChangeEvent> observer) {
        observers.add(observer);
    }
    
    @Override
    public void removeObserver(TypedObserver<PriceChangeEvent> observer) {
        observers.remove(observer);
    }
    
    @Override
    public void notifyObservers(PriceChangeEvent event) {
        for (TypedObserver<PriceChangeEvent> observer : observers) {
            observer.update(event);
        }
    }
    
    public void setPrice(double newPrice) {
        if (newPrice != this.price) {
            double oldPrice = this.price;
            this.price = newPrice;
            notifyObservers(new PriceChangeEvent(symbol, oldPrice, newPrice));
        }
    }
    
    public String getSymbol() { return symbol; }
    public double getPrice() { return price; }
}

// Type-safe observers
class PercentageChangeTracker implements TypedObserver<PriceChangeEvent> {
    @Override
    public void update(PriceChangeEvent event) {
        double percentChange = event.getPriceChangePercent();
        String direction = percentChange > 0 ? "📈" : "📉";
        
        System.out.printf("%s %s: %.2f%% change (%.2f -> %.2f)%n",
                         direction, event.getSymbol(), percentChange,
                         event.getOldPrice(), event.getNewPrice());
    }
}

class VolatilityAnalyzer implements TypedObserver<PriceChangeEvent> {
    private final List<Double> priceChanges = new ArrayList<>();
    private static final int WINDOW_SIZE = 5;
    
    @Override
    public void update(PriceChangeEvent event) {
        double change = Math.abs(event.getPriceChange());
        priceChanges.add(change);
        
        if (priceChanges.size() > WINDOW_SIZE) {
            priceChanges.remove(0);
        }
        
        if (priceChanges.size() >= 3) {
            double avgVolatility = priceChanges.stream()
                                              .mapToDouble(Double::doubleValue)
                                              .average()
                                              .orElse(0.0);
            
            System.out.printf("📊 %s volatility (avg of last %d): $%.2f%n",
                             event.getSymbol(), priceChanges.size(), avgVolatility);
        }
    }
}

Event-Driven Observer with Multiple Event Types

from abc import ABC, abstractmethod
from typing import List, Dict, Any, Type
from datetime import datetime
from enum import Enum

# Event base class
class Event:
    def __init__(self, source: str):
        self.source = source
        self.timestamp = datetime.now()
    
    def __str__(self):
        return f"{self.__class__.__name__}(source={self.source}, time={self.timestamp})"

# Specific event types
class UserRegisteredEvent(Event):
    def __init__(self, source: str, user_id: str, email: str, name: str):
        super().__init__(source)
        self.user_id = user_id
        self.email = email
        self.name = name

class UserLoginEvent(Event):
    def __init__(self, source: str, user_id: str, ip_address: str):
        super().__init__(source)
        self.user_id = user_id
        self.ip_address = ip_address

class OrderPlacedEvent(Event):
    def __init__(self, source: str, order_id: str, user_id: str, amount: float):
        super().__init__(source)
        self.order_id = order_id
        self.user_id = user_id
        self.amount = amount

# Observer interface
class EventObserver(ABC):
    @abstractmethod
    def handle_event(self, event: Event) -> None:
        pass
    
    @abstractmethod
    def get_supported_events(self) -> List[Type[Event]]:
        pass

# Event Bus (Subject)
class EventBus:
    def __init__(self):
        self._observers: Dict[Type[Event], List[EventObserver]] = {}
    
    def subscribe(self, observer: EventObserver) -> None:
        """Subscribe observer to events it supports"""
        for event_type in observer.get_supported_events():
            if event_type not in self._observers:
                self._observers[event_type] = []
            if observer not in self._observers[event_type]:
                self._observers[event_type].append(observer)
    
    def unsubscribe(self, observer: EventObserver) -> None:
        """Unsubscribe observer from all events"""
        for event_type in observer.get_supported_events():
            if event_type in self._observers:
                if observer in self._observers[event_type]:
                    self._observers[event_type].remove(observer)
    
    def publish(self, event: Event) -> None:
        """Publish event to all interested observers"""
        event_type = type(event)
        if event_type in self._observers:
            for observer in self._observers[event_type]:
                try:
                    observer.handle_event(event)
                except Exception as e:
                    print(f"Error in observer {observer.__class__.__name__}: {e}")

# Concrete Observers
class EmailNotificationService(EventObserver):
    def get_supported_events(self) -> List[Type[Event]]:
        return [UserRegisteredEvent, OrderPlacedEvent]
    
    def handle_event(self, event: Event) -> None:
        if isinstance(event, UserRegisteredEvent):
            self._send_welcome_email(event)
        elif isinstance(event, OrderPlacedEvent):
            self._send_order_confirmation(event)
    
    def _send_welcome_email(self, event: UserRegisteredEvent) -> None:
        print(f"📧 Sending welcome email to {event.email} for user {event.name}")
    
    def _send_order_confirmation(self, event: OrderPlacedEvent) -> None:
        print(f"📧 Sending order confirmation for order {event.order_id} (${event.amount})")

class SecurityMonitor(EventObserver):
    def __init__(self):
        self.login_attempts: Dict[str, List[datetime]] = {}
    
    def get_supported_events(self) -> List[Type[Event]]:
        return [UserLoginEvent]
    
    def handle_event(self, event: Event) -> None:
        if isinstance(event, UserLoginEvent):
            self._track_login(event)
    
    def _track_login(self, event: UserLoginEvent) -> None:
        user_id = event.user_id
        if user_id not in self.login_attempts:
            self.login_attempts[user_id] = []
        
        self.login_attempts[user_id].append(event.timestamp)
        
        # Check for suspicious activity (more than 5 logins in last minute)
        recent_logins = [
            timestamp for timestamp in self.login_attempts[user_id]
            if (event.timestamp - timestamp).total_seconds() < 60
        ]
        
        if len(recent_logins) > 5:
            print(f"🚨 SECURITY ALERT: Suspicious login activity for user {user_id} from {event.ip_address}")
        else:
            print(f"🔐 Login tracked: User {user_id} from {event.ip_address}")

class AnalyticsService(EventObserver):
    def __init__(self):
        self.metrics = {
            'user_registrations': 0,
            'user_logins': 0,
            'orders_placed': 0,
            'total_revenue': 0.0
        }
    
    def get_supported_events(self) -> List[Type[Event]]:
        return [UserRegisteredEvent, UserLoginEvent, OrderPlacedEvent]
    
    def handle_event(self, event: Event) -> None:
        if isinstance(event, UserRegisteredEvent):
            self.metrics['user_registrations'] += 1
            print(f"📊 New user registration: Total users = {self.metrics['user_registrations']}")
        
        elif isinstance(event, UserLoginEvent):
            self.metrics['user_logins'] += 1
            print(f"📊 User login tracked: Total logins = {self.metrics['user_logins']}")
        
        elif isinstance(event, OrderPlacedEvent):
            self.metrics['orders_placed'] += 1
            self.metrics['total_revenue'] += event.amount
            print(f"📊 Order placed: Total orders = {self.metrics['orders_placed']}, Revenue = ${self.metrics['total_revenue']:.2f}")

class AuditLogger(EventObserver):
    def __init__(self):
        self.audit_log = []
    
    def get_supported_events(self) -> List[Type[Event]]:
        return [UserRegisteredEvent, UserLoginEvent, OrderPlacedEvent]
    
    def handle_event(self, event: Event) -> None:
        log_entry = {
            'timestamp': event.timestamp.isoformat(),
            'event_type': event.__class__.__name__,
            'source': event.source,
            'details': self._extract_details(event)
        }
        self.audit_log.append(log_entry)
        print(f"📝 Audit log: {event.__class__.__name__} at {event.timestamp}")
    
    def _extract_details(self, event: Event) -> Dict[str, Any]:
        if isinstance(event, UserRegisteredEvent):
            return {'user_id': event.user_id, 'email': event.email}
        elif isinstance(event, UserLoginEvent):
            return {'user_id': event.user_id, 'ip_address': event.ip_address}
        elif isinstance(event, OrderPlacedEvent):
            return {'order_id': event.order_id, 'user_id': event.user_id, 'amount': event.amount}
        return {}

# Usage demonstration
def demonstrate_event_system():
    # Create event bus
    event_bus = EventBus()
    
    # Create observers
    email_service = EmailNotificationService()
    security_monitor = SecurityMonitor()
    analytics = AnalyticsService()
    audit_logger = AuditLogger()
    
    # Subscribe observers to event bus
    event_bus.subscribe(email_service)
    event_bus.subscribe(security_monitor)
    event_bus.subscribe(analytics)
    event_bus.subscribe(audit_logger)
    
    print("=== Event System Demonstration ===\n")
    
    # Simulate user registration
    print("1. User Registration:")
    event_bus.publish(UserRegisteredEvent(
        source="web_app",
        user_id="user_123",
        email="john@example.com",
        name="John Doe"
    ))
    print()
    
    # Simulate user logins
    print("2. User Logins:")
    for i in range(3):
        event_bus.publish(UserLoginEvent(
            source="mobile_app",
            user_id="user_123",
            ip_address=f"192.168.1.{10+i}"
        ))
    print()
    
    # Simulate order
    print("3. Order Placement:")
    event_bus.publish(OrderPlacedEvent(
        source="web_app",
        order_id="order_456",
        user_id="user_123",
        amount=99.99
    ))
    print()
    
    # Show final analytics
    print("4. Final Analytics:")
    print(f"Metrics: {analytics.metrics}")

demonstrate_event_system()

Practical Applications

Model-View-Controller (MVC) with Observer

import java.util.*;

// Model (Subject)
class UserModel {
    private String name;
    private String email;
    private int age;
    private List<Observer> observers = new ArrayList<>();
    
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    
    private void notifyObservers(String property, Object oldValue, Object newValue) {
        Map<String, Object> changeData = new HashMap<>();
        changeData.put("property", property);
        changeData.put("oldValue", oldValue);
        changeData.put("newValue", newValue);
        changeData.put("model", this);
        
        for (Observer observer : observers) {
            observer.update(changeData);
        }
    }
    
    // Setters with notification
    public void setName(String name) {
        String oldName = this.name;
        this.name = name;
        notifyObservers("name", oldName, name);
    }
    
    public void setEmail(String email) {
        String oldEmail = this.email;
        this.email = email;
        notifyObservers("email", oldEmail, email);
    }
    
    public void setAge(int age) {
        int oldAge = this.age;
        this.age = age;
        notifyObservers("age", oldAge, age);
    }
    
    // Getters
    public String getName() { return name; }
    public String getEmail() { return email; }
    public int getAge() { return age; }
}

// Views (Observers)
class UserProfileView implements Observer {
    @Override
    public void update(Object data) {
        @SuppressWarnings("unchecked")
        Map<String, Object> changeData = (Map<String, Object>) data;
        UserModel model = (UserModel) changeData.get("model");
        String property = (String) changeData.get("property");
        
        System.out.println("🖥️  Profile View Updated:");
        System.out.printf("   %s changed to: %s%n", property, changeData.get("newValue"));
        System.out.printf("   Current profile: %s (%s, age %d)%n",
                         model.getName(), model.getEmail(), model.getAge());
        System.out.println();
    }
}

class UserListView implements Observer {
    @Override
    public void update(Object data) {
        @SuppressWarnings("unchecked")
        Map<String, Object> changeData = (Map<String, Object>) data;
        UserModel model = (UserModel) changeData.get("model");
        
        System.out.println("📋 List View Updated:");
        System.out.printf("   User: %s (%s)%n", model.getName(), model.getEmail());
        System.out.println();
    }
}

class UserValidationView implements Observer {
    @Override
    public void update(Object data) {
        @SuppressWarnings("unchecked")
        Map<String, Object> changeData = (Map<String, Object>) data;
        UserModel model = (UserModel) changeData.get("model");
        String property = (String) changeData.get("property");
        Object newValue = changeData.get("newValue");
        
        System.out.println("✅ Validation View:");
        
        if ("email".equals(property)) {
            String email = (String) newValue;
            if (email != null && email.contains("@")) {
                System.out.println("   ✓ Email format is valid");
            } else {
                System.out.println("   ❌ Invalid email format");
            }
        } else if ("age".equals(property)) {
            int age = (Integer) newValue;
            if (age >= 18) {
                System.out.println("   ✓ Age is valid for adult account");
            } else {
                System.out.println("   ⚠️  Minor account - parental consent required");
            }
        }
        System.out.println();
    }
}

// Controller
class UserController {
    private UserModel model;
    
    public UserController(UserModel model) {
        this.model = model;
    }
    
    public void updateUserName(String name) {
        if (name != null && !name.trim().isEmpty()) {
            model.setName(name.trim());
        }
    }
    
    public void updateUserEmail(String email) {
        if (email != null && !email.trim().isEmpty()) {
            model.setEmail(email.trim());
        }
    }
    
    public void updateUserAge(int age) {
        if (age > 0 && age < 150) {
            model.setAge(age);
        }
    }
}

Common Misconceptions

  1. Misconception: Observer pattern is the same as event listeners in GUI frameworks Reality: While related, Observer is a more general pattern that can be applied beyond UI

  2. Misconception: Observers must always be notified synchronously Reality: Asynchronous notification is often preferred for performance and decoupling

  3. Misconception: One subject can only have one type of observer Reality: Subjects can have multiple types of observers responding to different aspects

5. Live Coding Demonstration (20-25 minutes)

Example 1: Real-time Chat System

// Event types
const EventTypes = {
    USER_JOINED: 'USER_JOINED',
    USER_LEFT: 'USER_LEFT',
    MESSAGE_SENT: 'MESSAGE_SENT',
    USER_TYPING: 'USER_TYPING'
};

// Event class
class ChatEvent {
    constructor(type, data) {
        this.type = type;
        this.data = data;
        this.timestamp = new Date();
        this.id = Math.random().toString(36).substr(2, 9);
    }
}

// Observable chat room
class ChatRoom {
    constructor(name) {
        this.name = name;
        this.users = new Map();
        this.messages = [];
        this.observers = [];
    }
    
    // Observer management
    subscribe(observer) {
        this.observers.push(observer);
    }
    
    unsubscribe(observer) {
        const index = this.observers.indexOf(observer);
        if (index > -1) {
            this.observers.splice(index, 1);
        }
    }
    
    notify(event) {
        this.observers.forEach(observer => {
            try {
                observer.onEvent(event);
            } catch (error) {
                console.error(`Observer error: ${error.message}`);
            }
        });
    }
    
    // Chat room actions
    addUser(user) {
        if (!this.users.has(user.id)) {
            this.users.set(user.id, user);
            const event = new ChatEvent(EventTypes.USER_JOINED, {
                user: user,
                userCount: this.users.size
            });
            this.notify(event);
        }
    }
    
    removeUser(userId) {
        const user = this.users.get(userId);
        if (user) {
            this.users.delete(userId);
            const event = new ChatEvent(EventTypes.USER_LEFT, {
                user: user,
                userCount: this.users.size
            });
            this.notify(event);
        }
    }
    
    sendMessage(userId, message) {
        const user = this.users.get(userId);
        if (user) {
            const messageObj = {
                id: Math.random().toString(36).substr(2, 9),
                user: user,
                message: message,
                timestamp: new Date()
            };
            
            this.messages.push(messageObj);
            
            const event = new ChatEvent(EventTypes.MESSAGE_SENT, {
                message: messageObj,
                messageCount: this.messages.length
            });
            this.notify(event);
        }
    }
    
    userTyping(userId) {
        const user = this.users.get(userId);
        if (user) {
            const event = new ChatEvent(EventTypes.USER_TYPING, {
                user: user
            });
            this.notify(event);
        }
    }
}

// Observer implementations
class ChatDisplay {
    constructor(name) {
        this.name = name;
    }
    
    onEvent(event) {
        switch (event.type) {
            case EventTypes.USER_JOINED:
                console.log(`[${this.name}] 👋 ${event.data.user.name} joined the chat (${event.data.userCount} users online)`);
                break;
                
            case EventTypes.USER_LEFT:
                console.log(`[${this.name}] 👋 ${event.data.user.name} left the chat (${event.data.userCount} users online)`);
                break;
                
            case EventTypes.MESSAGE_SENT:
                const msg = event.data.message;
                console.log(`[${this.name}] 💬 ${msg.user.name}: ${msg.message}`);
                break;
                
            case EventTypes.USER_TYPING:
                console.log(`[${this.name}] ✍️  ${event.data.user.name} is typing...`);
                break;
        }
    }
}

class ChatLogger {
    constructor() {
        this.logs = [];
    }
    
    onEvent(event) {
        const logEntry = {
            timestamp: event.timestamp,
            type: event.type,
            data: event.data,
            id: event.id
        };
        
        this.logs.push(logEntry);
        console.log(`[Logger] 📝 Event logged: ${event.type} (Total logs: ${this.logs.length})`);
    }
    
    getLogsSince(timestamp) {
        return this.logs.filter(log => log.timestamp >= timestamp);
    }
}

class ModerationBot {
    constructor() {
        this.bannedWords = ['spam', 'badword1', 'badword2'];
        this.warnings = new Map();
    }
    
    onEvent(event) {
        if (event.type === EventTypes.MESSAGE_SENT) {
            const message = event.data.message;
            const hasBannedWord = this.bannedWords.some(word => 
                message.message.toLowerCase().includes(word)
            );
            
            if (hasBannedWord) {
                console.log(`[Moderation] 🚫 Inappropriate message detected from ${message.user.name}`);
                
                const userId = message.user.id;
                const warningCount = (this.warnings.get(userId) || 0) + 1;
                this.warnings.set(userId, warningCount);
                
                if (warningCount >= 3) {
                    console.log(`[Moderation] ⛔ User ${message.user.name} has been flagged for review`);
                }
            }
        }
    }
}

class TypingIndicator {
    constructor() {
        this.typingUsers = new Set();
        this.typingTimers = new Map();
    }
    
    onEvent(event) {
        if (event.type === EventTypes.USER_TYPING) {
            const userId = event.data.user.id;
            const userName = event.data.user.name;
            
            this.typingUsers.add(userId);
            
            // Clear existing timer
            if (this.typingTimers.has(userId)) {
                clearTimeout(this.typingTimers.get(userId));
            }
            
            // Set new timer to remove typing indicator
            const timer = setTimeout(() => {
                this.typingUsers.delete(userId);
                this.typingTimers.delete(userId);
                console.log(`[Typing] ⏰ ${userName} stopped typing (timeout)`);
            }, 3000);
            
            this.typingTimers.set(userId, timer);
        } else if (event.type === EventTypes.MESSAGE_SENT) {
            // User sent message, stop typing indicator
            const userId = event.data.message.user.id;
            if (this.typingUsers.has(userId)) {
                this.typingUsers.delete(userId);
                if (this.typingTimers.has(userId)) {
                    clearTimeout(this.typingTimers.get(userId));
                    this.typingTimers.delete(userId);
                }
            }
        }
    }
}

// Usage demonstration
function demonstrateChatSystem() {
    console.log('=== Real-time Chat System Demo ===\n');
    
    // Create chat room
    const chatRoom = new ChatRoom('General Discussion');
    
    // Create observers
    const mainDisplay = new ChatDisplay('Main Display');
    const mobileDisplay = new ChatDisplay('Mobile App');
    const logger = new ChatLogger();
    const moderationBot = new ModerationBot();
    const typingIndicator = new TypingIndicator();
    
    // Subscribe observers
    chatRoom.subscribe(mainDisplay);
    chatRoom.subscribe(mobileDisplay);
    chatRoom.subscribe(logger);
    chatRoom.subscribe(moderationBot);
    chatRoom.subscribe(typingIndicator);
    
    // Create users
    const alice = { id: 'user1', name: 'Alice' };
    const bob = { id: 'user2', name: 'Bob' };
    const charlie = { id: 'user3', name: 'Charlie' };
    
    // Simulate chat activity
    console.log('1. Users joining:');
    chatRoom.addUser(alice);
    chatRoom.addUser(bob);
    console.log();
    
    console.log('2. Chat messages:');
    chatRoom.sendMessage('user1', 'Hello everyone!');
    chatRoom.sendMessage('user2', 'Hi Alice! How are you?');
    console.log();
    
    console.log('3. Typing indicators:');
    chatRoom.userTyping('user1');
    setTimeout(() => {
        chatRoom.sendMessage('user1', 'I\'m doing great, thanks!');
    }, 1000);
    console.log();
    
    console.log('4. Moderation in action:');
    chatRoom.addUser(charlie);
    chatRoom.sendMessage('user3', 'This is spam content');
    console.log();
    
    console.log('5. User leaving:');
    chatRoom.removeUser('user3');
    console.log();
    
    // Unsubscribe mobile display
    console.log('6. Mobile display disconnected:');
    chatRoom.unsubscribe(mobileDisplay);
    chatRoom.sendMessage('user2', 'Mobile users won\'t see this');
    
    console.log('\n=== Chat Demo Complete ===');
}

// Run demonstration
demonstrateChatSystem();

Example 2: IoT Sensor Monitoring System

import time
import random
from abc import ABC, abstractmethod
from typing import List, Dict, Any
from datetime import datetime, timedelta
from threading import Timer
import json

class SensorReading:
    def __init__(self, sensor_id: str, sensor_type: str, value: float, unit: str):
        self.sensor_id = sensor_id
        self.sensor_type = sensor_type
        self.value = value
        self.unit = unit
        self.timestamp = datetime.now()
    
    def to_dict(self):
        return {
            'sensor_id': self.sensor_id,
            'sensor_type': self.sensor_type,
            'value': self.value,
            'unit': self.unit,
            'timestamp': self.timestamp.isoformat()
        }
    
    def __str__(self):
        return f"{self.sensor_type}[{self.sensor_id}]: {self.value}{self.unit} at {self.timestamp}"

class SensorObserver(ABC):
    @abstractmethod
    def on_sensor_reading(self, reading: SensorReading) -> None:
        pass

class IoTSensor:
    def __init__(self, sensor_id: str, sensor_type: str, unit: str):
        self.sensor_id = sensor_id
        self.sensor_type = sensor_type
        self.unit = unit
        self.observers: List[SensorObserver] = []
        self.is_active = False
        self.reading_interval = 5  # seconds
        self.timer = None
    
    def add_observer(self, observer: SensorObserver) -> None:
        if observer not in self.observers:
            self.observers.append(observer)
    
    def remove_observer(self, observer: SensorObserver) -> None:
        if observer in self.observers:
            self.observers.remove(observer)
    
    def notify_observers(self, reading: SensorReading) -> None:
        for observer in self.observers:
            try:
                observer.on_sensor_reading(reading)
            except Exception as e:
                print(f"Error notifying observer: {e}")
    
    def start_monitoring(self) -> None:
        self.is_active = True
        self._schedule_next_reading()
        print(f"🔴 Sensor {self.sensor_id} started monitoring")
    
    def stop_monitoring(self) -> None:
        self.is_active = False
        if self.timer:
            self.timer.cancel()
        print(f"⚫ Sensor {self.sensor_id} stopped monitoring")
    
    def _schedule_next_reading(self) -> None:
        if self.is_active:
            reading = self._take_reading()
            self.notify_observers(reading)
            
            # Schedule next reading
            self.timer = Timer(self.reading_interval, self._schedule_next_reading)
            self.timer.start()
    
    @abstractmethod
    def _take_reading(self) -> SensorReading:
        pass

class TemperatureSensor(IoTSensor):
    def __init__(self, sensor_id: str, location: str):
        super().__init__(sensor_id, "Temperature", "°C")
        self.location = location
        self.base_temp = 22.0  # Base temperature
    
    def _take_reading(self) -> SensorReading:
        # Simulate temperature reading with some random variation
        variation = random.uniform(-3.0, 3.0)
        temperature = self.base_temp + variation
        return SensorReading(self.sensor_id, "Temperature", round(temperature, 1), self.unit)

class HumiditySensor(IoTSensor):
    def __init__(self, sensor_id: str, location: str):
        super().__init__(sensor_id, "Humidity", "%")
        self.location = location
        self.base_humidity = 45.0  # Base humidity
    
    def _take_reading(self) -> SensorReading:
        # Simulate humidity reading
        variation = random.uniform(-10.0, 15.0)
        humidity = max(0, min(100, self.base_humidity + variation))
        return SensorReading(self.sensor_id, "Humidity", round(humidity, 1), self.unit)

class PressureSensor(IoTSensor):
    def __init__(self, sensor_id: str, location: str):
        super().__init__(sensor_id, "Pressure", "hPa")
        self.location = location
        self.base_pressure = 1013.25  # Sea level pressure
    
    def _take_reading(self) -> SensorReading:
        # Simulate pressure reading
        variation = random.uniform(-5.0, 5.0)
        pressure = self.base_pressure + variation
        return SensorReading(self.sensor_id, "Pressure", round(pressure, 2), self.unit)

# Observer implementations
class DataLogger(SensorObserver):
    def __init__(self, log_file: str):
        self.log_file = log_file
        self.readings = []
    
    def on_sensor_reading(self, reading: SensorReading) -> None:
        self.readings.append(reading)
        print(f"📊 [Data Logger] Logged: {reading}")
        
        # Simulate writing to file
        with open(self.log_file, 'a') as f:
            f.write(json.dumps(reading.to_dict()) + '\n')

class AlertSystem(SensorObserver):
    def __init__(self):
        self.alert_rules = {
            'Temperature': {'min': 18.0, 'max': 26.0},
            'Humidity': {'min': 30.0, 'max': 70.0},
            'Pressure': {'min': 1000.0, 'max': 1030.0}
        }
        self.active_alerts = set()
    
    def on_sensor_reading(self, reading: SensorReading) -> None:
        if reading.sensor_type in self.alert_rules:
            rules = self.alert_rules[reading.sensor_type]
            alert_key = f"{reading.sensor_id}_{reading.sensor_type}"
            
            if reading.value < rules['min'] or reading.value > rules['max']:
                if alert_key not in self.active_alerts:
                    self.active_alerts.add(alert_key)
                    print(f"🚨 [ALERT] {reading.sensor_type} {reading.sensor_id}: {reading.value}{reading.unit} is out of range!")
            else:
                if alert_key in self.active_alerts:
                    self.active_alerts.remove(alert_key)
                    print(f"✅ [RESOLVED] {reading.sensor_type} {reading.sensor_id}: Back to normal range")

class Dashboard(SensorObserver):
    def __init__(self):
        self.latest_readings: Dict[str, SensorReading] = {}
        self.update_count = 0
    
    def on_sensor_reading(self, reading: SensorReading) -> None:
        self.latest_readings[reading.sensor_id] = reading
        self.update_count += 1
        
        if self.update_count % 5 == 0:  # Update display every 5 readings
            self._update_display()
    
    def _update_display(self) -> None:
        print(f"\n📱 [Dashboard Update #{self.update_count // 5}]")
        print("-" * 50)
        for sensor_id, reading in self.latest_readings.items():
            status = self._get_status_emoji(reading)
            print(f"{status} {reading.sensor_type}: {reading.value}{reading.unit} ({sensor_id})")
        print("-" * 50)
    
    def _get_status_emoji(self, reading: SensorReading) -> str:
        # Simple status based on typical ranges
        if reading.sensor_type == "Temperature":
            return "🌡️" if 18 <= reading.value <= 26 else "⚠️"
        elif reading.sensor_type == "Humidity":
            return "💧" if 30 <= reading.value <= 70 else "⚠️"
        elif reading.sensor_type == "Pressure":
            return "🌤️" if 1000 <= reading.value <= 1030 else "⚠️"
        return "📊"

class TrendAnalyzer(SensorObserver):
    def __init__(self, window_size: int = 5):
        self.window_size = window_size
        self.readings_history: Dict[str, List[SensorReading]] = {}
    
    def on_sensor_reading(self, reading: SensorReading) -> None:
        sensor_key = f"{reading.sensor_id}_{reading.sensor_type}"
        
        if sensor_key not in self.readings_history:
            self.readings_history[sensor_key] = []
        
        history = self.readings_history[sensor_key]
        history.append(reading)
        
        # Keep only the last N readings
        if len(history) > self.window_size:
            history.pop(0)
        
        # Analyze trend if we have enough data
        if len(history) >= 3:
            self._analyze_trend(sensor_key, history)
    
    def _analyze_trend(self, sensor_key: str, history: List[SensorReading]) -> None:
        values = [r.value for r in history]
        
        # Simple trend analysis
        if len(values) >= 3:
            recent_trend = values[-3:]
            
            if all(recent_trend[i] < recent_trend[i+1] for i in range(len(recent_trend)-1)):
                print(f"📈 [Trend] {sensor_key}: Rising trend detected")
            elif all(recent_trend[i] > recent_trend[i+1] for i in range(len(recent_trend)-1)):
                print(f"📉 [Trend] {sensor_key}: Falling trend detected")

# Demonstration
def demonstrate_iot_monitoring():
    print("=== IoT Sensor Monitoring System ===\n")
    
    # Create sensors
    temp_sensor = TemperatureSensor("TEMP_001", "Living Room")
    humidity_sensor = HumiditySensor("HUM_001", "Living Room")
    pressure_sensor = PressureSensor("PRESS_001", "Outdoor")
    
    # Set shorter intervals for demo
    temp_sensor.reading_interval = 2
    humidity_sensor.reading_interval = 3
    pressure_sensor.reading_interval = 4
    
    # Create observers
    logger = DataLogger("sensor_readings.log")
    alert_system = AlertSystem()
    dashboard = Dashboard()
    trend_analyzer = TrendAnalyzer(window_size=4)
    
    # Subscribe observers to sensors
    for sensor in [temp_sensor, humidity_sensor, pressure_sensor]:
        sensor.add_observer(logger)
        sensor.add_observer(alert_system)
        sensor.add_observer(dashboard)
        sensor.add_observer(trend_analyzer)
    
    try:
        # Start monitoring
        print("Starting sensor monitoring...")
        temp_sensor.start_monitoring()
        humidity_sensor.start_monitoring()
        pressure_sensor.start_monitoring()
        
        # Let it run for a while
        time.sleep(15)
        
    finally:
        # Stop monitoring
        print("\nStopping sensor monitoring...")
        temp_sensor.stop_monitoring()
        humidity_sensor.stop_monitoring()
        pressure_sensor.stop_monitoring()
        
        print(f"\nTotal readings logged: {len(logger.readings)}")
        print("Demo completed!")

# Uncomment to run the demonstration
# demonstrate_iot_monitoring()

Debugging Demonstration

Show common errors and how to fix them:

// Common mistake: Memory leaks with observers
public class LeakySubject {
    private List<Observer> observers = new ArrayList<>();
    
    public void addObserver(Observer observer) {
        observers.add(observer);
        // Bug: Never removes observers, causing memory leaks
    }
    
    // Missing: removeObserver method and weak references
}

// Fixed version with proper cleanup
public class SafeSubject {
    private List<WeakReference<Observer>> observers = new ArrayList<>();
    
    public void addObserver(Observer observer) {
        observers.add(new WeakReference<>(observer));
    }
    
    public void removeObserver(Observer observer) {
        observers.removeIf(ref -> ref.get() == observer || ref.get() == null);
    }
    
    public void notifyObservers() {
        // Clean up dead references while notifying
        Iterator<WeakReference<Observer>> iterator = observers.iterator();
        while (iterator.hasNext()) {
            WeakReference<Observer> ref = iterator.next();
            Observer observer = ref.get();
            if (observer == null) {
                iterator.remove(); // Remove dead reference
            } else {
                observer.update(this);
            }
        }
    }
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Basic Practice (10 minutes)

Objective: Create a simple Observer pattern for a news subscription system

Instructions:

  1. Create a NewsAgency (Subject) that can publish news articles
  2. Create NewsChannel and OnlinePortal observers that display news differently
  3. Implement subscription/unsubscription functionality
  4. Test with different types of news (Sports, Politics, Technology)

Starter Code:

interface NewsObserver {
    void update(String news, String category);
}

class NewsAgency {
    // TODO: Implement subject with observer management
    
    public void publishNews(String news, String category) {
        // TODO: Notify all observers
    }
}

class NewsChannel implements NewsObserver {
    private String channelName;
    
    // TODO: Implement update method
}

Exercise 2: Intermediate Challenge (15 minutes)

Objective: Build a stock portfolio tracker with multiple notification strategies

Requirements:

  • Create a Stock subject that tracks price, volume, and market cap changes
  • Implement different observer types: EmailAlert, SMSAlert, DesktopNotification
  • Add filtering - observers only get notified for changes above certain thresholds
  • Support different notification frequencies (immediate, batched)

Challenge Features:

  • Add portfolio-level aggregation (total value changes)
  • Implement smart alerts (only notify on significant percentage changes)
  • Create a notification history system

Exercise 3: Creative Application (15 minutes)

Objective: Design a smart home automation system using Observer pattern

Challenge: Create a system where:

  • Sensors (motion, temperature, light) are subjects
  • Smart devices (lights, AC, security system) are observers
  • Rules engine determines which devices should react to which sensors
  • Support for complex scenarios (e.g., “turn on lights when motion detected and it’s dark”)

Extension Ideas:

  • Add learning algorithms that adapt to user preferences
  • Implement energy optimization (coordinate devices to minimize power usage)
  • Create mobile app notifications for remote monitoring
  • Add voice control integration

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Memory leaks from observers not being removed Cause: Objects holding strong references to observers prevent garbage collection Solution: Use weak references, implement proper cleanup, provide unsubscribe methods

  2. Problem: Infinite update loops between observers and subjects Cause: Observer modifying subject state during update, causing recursive notifications Solution: Use flags to prevent re-entry, separate notification from state changes

  3. Problem: Performance issues with large numbers of observers Cause: Synchronous notification blocking the subject thread Solution: Use asynchronous notification, batch updates, or priority-based notification

  4. Problem: Thread safety issues in multithreaded environments Cause: Concurrent access to observer list during iteration and modification Solution: Use concurrent collections, proper synchronization, or copy-on-write patterns

Debugging Tips

  • Log observer registration and unregistration to track subscription lifecycles
  • Monitor notification frequency and patterns to identify performance bottlenecks
  • Use debugging tools to detect memory leaks from unreleased observers
  • Test with multiple threads to ensure thread safety

When NOT to Use Observer Pattern

  • When you need guaranteed delivery (Observer doesn’t ensure observers receive notifications)
  • For complex event routing (use Event Bus or Message Queue instead)
  • When immediate consistency is required between subject and observers
  • In performance-critical systems where notification overhead is significant

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Use weak references to prevent memory leaks in long-running applications
  • Implement proper exception handling in observer notifications to prevent cascade failures
  • Consider asynchronous notification for better performance and responsiveness
  • Document observer contracts clearly, including when and what notifications are sent

Next Steps in Your Learning Journey

  • Study Event-Driven Architecture patterns for distributed systems
  • Learn Reactive Programming (RxJava, RxJS) as modern Observer pattern implementations
  • Explore Message Queues and Event Buses for decoupled communication
  • Investigate CQRS and Event Sourcing patterns for scalable architectures

Modern Alternatives and Frameworks

  • Reactive Streams (RxJava, RxJS, Reactive Extensions)
  • Event Buses (Guava EventBus, MBassador)
  • Message Brokers (RabbitMQ, Apache Kafka)
  • Pub-Sub Systems (Redis Pub/Sub, Google Cloud Pub/Sub)

9. Summary and Review (5 minutes)

Key Takeaways

  1. Observer pattern enables loose coupling between subjects and observers through well-defined interfaces
  2. Choose appropriate notification model (push vs pull) based on data volume and observer needs
  3. Handle memory management carefully to prevent leaks from observer references
  4. Consider modern alternatives like reactive streams for complex event-driven systems

Quick Review Questions

  1. What are the main differences between push and pull models in Observer pattern?
  2. How can you prevent memory leaks when using Observer pattern?
  3. When would you choose reactive programming over traditional Observer pattern?

For Instructors

Teaching Tips

  • Start with concrete examples (news subscriptions, stock prices) before showing code
  • Demonstrate both the problems Observer solves and the problems it can create
  • Use visual diagrams to show the flow of notifications
  • Compare traditional Observer with modern reactive programming approaches

Assessment Ideas

  • Debugging exercise: Find and fix memory leaks in Observer implementations
  • Design challenge: Choose between Observer pattern and alternatives for given scenarios
  • Performance analysis: Compare synchronous vs asynchronous notification strategies

Adaptation Notes

  • Beginners: Focus on basic Observer implementation and common use cases
  • Advanced: Discuss thread safety, performance optimization, and integration with frameworks
  • Framework-specific: Show how Observer pattern is implemented in popular frameworks (Spring Events, Angular Observables)