Design Principle
Observer Pattern
Learn the Observer pattern: define a one-to-many dependency between objects. When one object changes state, all dependents are notified automatically.
The Observer Pattern defines a one-to-many dependency between objects. When one object changes state, all its dependents are notified and updated automatically.
What is the Observer Pattern?
Observer Pattern provides:
- Loose coupling: Subject doesn't know concrete observers
- Dynamic relationships: Add/remove observers at runtime
- Event notification: Notify multiple observers of changes
- Broadcast communication: One subject, many observers
Use cases:
- Model-View architecture (MVC)
- Event handling systems
- Publish-subscribe systems
- Real-time updates
- UI frameworks (React, Vue)
Structure
Subject (interface)
├─ attach(observer)
├─ detach(observer)
└─ notify()
ConcreteSubject
└─ state
└─ notify() (notifies all observers)
Observer (interface)
└─ update()
ConcreteObserver
└─ update() (reacts to state change)
Examples
Basic Observer Pattern
// Observer interface
interface Observer {
update(data: any): void;
}
// Subject interface
interface Subject {
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
}
// Concrete subject
class NewsAgency implements Subject {
private observers: Observer[] = [];
private news: string = "";
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(): void {
this.observers.forEach(observer => observer.update(this.news));
}
setNews(news: string): void {
this.news = news;
this.notify(); // Notify all observers
}
}
// Concrete observers
class NewsChannel implements Observer {
private name: string;
constructor(name: string) {
this.name = name;
}
update(news: string): void {
console.log(`${this.name} received news: ${news}`);
}
}
// Usage
const agency = new NewsAgency();
const channel1 = new NewsChannel("CNN");
const channel2 = new NewsChannel("BBC");
agency.attach(channel1);
agency.attach(channel2);
agency.setNews("Breaking: Important news!");
// Output:
// CNN received news: Breaking: Important news!
// BBC received news: Breaking: Important news!
Stock Market Observer
// Observer interface
interface StockObserver {
update(stock: Stock): void;
}
// Subject
class StockMarket implements Subject {
private observers: StockObserver[] = [];
private stocks: Map<string, Stock> = new Map();
attach(observer: StockObserver): void {
this.observers.push(observer);
}
detach(observer: StockObserver): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(): void {
this.observers.forEach(observer => {
this.stocks.forEach(stock => observer.update(stock));
});
}
updateStock(symbol: string, price: number): void {
const stock = this.stocks.get(symbol) || { symbol, price };
stock.price = price;
this.stocks.set(symbol, stock);
this.notify();
}
}
// Concrete observers
class StockDisplay implements StockObserver {
update(stock: Stock): void {
console.log(`Stock ${stock.symbol}: $${stock.price}`);
}
}
class PriceAlert implements StockObserver {
private threshold: number;
constructor(threshold: number) {
this.threshold = threshold;
}
update(stock: Stock): void {
if (stock.price > this.threshold) {
console.log(`ALERT: ${stock.symbol} exceeded $${this.threshold}`);
}
}
}
// Usage
const market = new StockMarket();
const display = new StockDisplay();
const alert = new PriceAlert(100);
market.attach(display);
market.attach(alert);
market.updateStock("AAPL", 150);
// Output:
// Stock AAPL: $150
// ALERT: AAPL exceeded $100
Event System
// Event type
type EventType = string;
// Event data
interface EventData {
[key: string]: any;
}
// Observer (event listener)
interface EventListener {
handle(eventType: EventType, data: EventData): void;
}
// Event emitter (subject)
class EventEmitter {
private listeners: Map<EventType, EventListener[]> = new Map();
on(eventType: EventType, listener: EventListener): void {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType)!.push(listener);
}
off(eventType: EventType, listener: EventListener): void {
const listeners = this.listeners.get(eventType);
if (listeners) {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
emit(eventType: EventType, data: EventData): void {
const listeners = this.listeners.get(eventType);
if (listeners) {
listeners.forEach(listener => listener.handle(eventType, data));
}
}
}
// Usage
const emitter = new EventEmitter();
const listener1: EventListener = {
handle(eventType: EventType, data: EventData): void {
console.log(`Listener 1: ${eventType}`, data);
}
};
const listener2: EventListener = {
handle(eventType: EventType, data: EventData): void {
console.log(`Listener 2: ${eventType}`, data);
}
};
emitter.on("user:login", listener1);
emitter.on("user:login", listener2);
emitter.emit("user:login", { userId: 123, username: "john" });
// Output:
// Listener 1: user:login { userId: 123, username: 'john' }
// Listener 2: user:login { userId: 123, username: 'john' }
Common Pitfalls
- Memory leaks: Observers not detached. Fix: Always detach observers
- Update order: Order of updates matters. Fix: Document order, use priority
- Too many observers: Performance issues. Fix: Limit observers, batch updates
- Circular updates: Observer updates subject. Fix: Prevent circular updates
Interview Questions
Beginner
Q: What is the Observer pattern and when would you use it?
A:
Observer Pattern defines one-to-many dependency between objects. When subject changes, all observers are notified.
Key characteristics:
- One-to-many: One subject, many observers
- Loose coupling: Subject doesn't know concrete observers
- Dynamic: Add/remove observers at runtime
- Automatic notification: Observers notified automatically
Example:
class Subject {
private observers: Observer[] = [];
attach(observer: Observer): void {
this.observers.push(observer);
}
notify(): void {
this.observers.forEach(obs => obs.update());
}
}
Use cases:
- MVC architecture: Model notifies views
- Event systems: Event emitters/listeners
- UI frameworks: React, Vue reactivity
- Real-time updates: Stock prices, notifications
Benefits:
- Loose coupling: Subject and observers decoupled
- Flexibility: Add/remove observers dynamically
- Broadcast: Notify multiple observers
Intermediate
Q: Explain how the Observer pattern works. How do you handle observer lifecycle and prevent memory leaks?
A:
Observer Pattern Structure:
1. Subject:
class Subject {
private observers: Observer[] = [];
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(): void {
this.observers.forEach(observer => observer.update());
}
}
2. Observer:
interface Observer {
update(data: any): void;
}
Preventing Memory Leaks:
1. Always Detach:
class Component {
private observer: Observer;
constructor(subject: Subject) {
this.observer = { update: this.handleUpdate.bind(this) };
subject.attach(this.observer);
}
destroy(): void {
// CRITICAL: Detach before destroying
subject.detach(this.observer);
}
}
2. Weak References:
class Subject {
private observers: WeakSet<Observer> = new WeakSet();
// WeakSet allows garbage collection
}
3. Automatic Cleanup:
class AutoDetachingObserver implements Observer {
constructor(
private subject: Subject,
private callback: Function
) {
subject.attach(this);
}
update(data: any): void {
this.callback(data);
}
destroy(): void {
this.subject.detach(this);
}
}
Senior
Q: Design an observer system for a real-time collaborative editor where multiple users can edit a document simultaneously. Handle conflict resolution, network synchronization, and ensure all clients stay in sync.
A:
// Document change event
interface DocumentChange {
type: "insert" | "delete" | "format";
position: number;
content?: string;
timestamp: number;
userId: string;
}
// Observer interface
interface DocumentObserver {
onDocumentChange(change: DocumentChange): void;
onConflict(change: DocumentChange, localChange: DocumentChange): void;
}
// Subject (Document)
class CollaborativeDocument {
private observers: DocumentObserver[] = [];
private content: string = "";
private changes: DocumentChange[] = [];
private version: number = 0;
attach(observer: DocumentObserver): void {
this.observers.push(observer);
}
detach(observer: DocumentObserver): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
private notify(change: DocumentChange): void {
this.observers.forEach(observer => observer.onDocumentChange(change));
}
applyChange(change: DocumentChange): void {
// Check for conflicts
const conflict = this.detectConflict(change);
if (conflict) {
this.notifyConflict(change, conflict);
return;
}
// Apply change
this.applyChangeToContent(change);
this.changes.push(change);
this.version++;
// Notify observers
this.notify(change);
}
private detectConflict(change: DocumentChange): DocumentChange | null {
// Check if change conflicts with recent changes
for (const existingChange of this.changes.slice(-10)) {
if (this.isConflict(change, existingChange)) {
return existingChange;
}
}
return null;
}
private isConflict(change1: DocumentChange, change2: DocumentChange): boolean {
// Conflicts if changes overlap in position
return Math.abs(change1.position - change2.position) < 5 &&
Math.abs(change1.timestamp - change2.timestamp) < 1000;
}
private applyChangeToContent(change: DocumentChange): void {
switch (change.type) {
case "insert":
this.content = this.content.slice(0, change.position) +
(change.content || "") +
this.content.slice(change.position);
break;
case "delete":
this.content = this.content.slice(0, change.position) +
this.content.slice(change.position + 1);
break;
}
}
private notifyConflict(change: DocumentChange, conflict: DocumentChange): void {
this.observers.forEach(observer =>
observer.onConflict(change, conflict)
);
}
getContent(): string {
return this.content;
}
getVersion(): number {
return this.version;
}
}
// Client observer
class ClientObserver implements DocumentObserver {
private userId: string;
private pendingChanges: DocumentChange[] = [];
constructor(userId: string) {
this.userId = userId;
}
onDocumentChange(change: DocumentChange): void {
// Ignore own changes
if (change.userId === this.userId) {
return;
}
// Apply remote change
this.applyRemoteChange(change);
}
onConflict(change: DocumentChange, localChange: DocumentChange): void {
// Resolve conflict (last-write-wins or merge)
this.resolveConflict(change, localChange);
}
private applyRemoteChange(change: DocumentChange): void {
// Update UI with remote change
console.log(`Remote change from ${change.userId}: ${change.type} at ${change.position}`);
}
private resolveConflict(change: DocumentChange, localChange: DocumentChange): void {
// Conflict resolution strategy
// Option 1: Last-write-wins
if (change.timestamp > localChange.timestamp) {
// Accept remote change
this.applyRemoteChange(change);
} else {
// Keep local change, request sync
this.requestSync();
}
}
private requestSync(): void {
// Request full document sync
}
}
// Network sync
class NetworkSync {
private document: CollaborativeDocument;
async syncChange(change: DocumentChange): Promise<void> {
// Send to server
await fetch("/api/document/change", {
method: "POST",
body: JSON.stringify(change)
});
}
async receiveChanges(): Promise<void> {
// Poll or use WebSocket for changes
const changes = await fetch("/api/document/changes").then(r => r.json());
changes.forEach((change: DocumentChange) => {
this.document.applyChange(change);
});
}
}
Features:
- Observer pattern: Notify all clients of changes
- Conflict detection: Detect conflicting changes
- Conflict resolution: Resolve conflicts (last-write-wins, merge)
- Network sync: Sync changes across network
- Version control: Track document version
Key Takeaways
- Observer pattern: One-to-many dependency, automatic notification
- Loose coupling: Subject and observers decoupled
- Dynamic: Add/remove observers at runtime
- Use cases: MVC, event systems, real-time updates
- Memory leaks: Always detach observers
- Best practices: Use weak references, handle lifecycle, prevent circular updates
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.