Observer Pattern
The Observer pattern defines a one-to-many dependency between objects, so that when one object changes state, all its dependents are automatically notified and updated. This pattern is fundamental for building event-driven systems and is particularly useful when modernizing legacy systems to add new features without modifying existing code.
Video Summary
The video presentation covers the Observer pattern comprehensively:
- Core concepts of publish-subscribe architecture
- When to use observers vs. direct method calls
- Implementing observers in TypeScript
- Real-world applications in legacy modernization
- Avoiding common pitfalls: memory leaks, notification storms
- Comparison with related patterns: Event Emitters, Message Queues
Key Concepts
1. Subject and Observers
Subject (Observable): The object being watched Observers: Objects that want to be notified of changes
interface Observer<T> {
update(data: T): void;
}
class Subject<T> {
private observers: Observer<T>[] = [];
subscribe(observer: Observer<T>): void {
this.observers.push(observer);
}
unsubscribe(observer: Observer<T>): void {
this.observers = this.observers.filter((o) => o !== observer);
}
notify(data: T): void {
this.observers.forEach((observer) => observer.update(data));
}
}
2. Decoupling
Subject doesn't need to know about concrete observer classes:
// Subject doesn't know what observers do with the data
class UserModel extends Subject<User> {
updateUser(user: User): void {
// Update user
this.notify(user); // All observers automatically notified
}
}
// Observers can be added without modifying subject
class EmailObserver implements Observer<User> {
update(user: User): void {
sendEmail(user.email, 'Profile updated');
}
}
class AuditObserver implements Observer<User> {
update(user: User): void {
logToAudit({ event: 'user_updated', userId: user.id });
}
}
class CacheObserver implements Observer<User> {
update(user: User): void {
cache.invalidate(`user:${user.id}`);
}
}
Real-World Applications
Example 1: Adding Features to Legacy Systems
// Legacy order processing code we can't modify
class LegacyOrderProcessor {
private observers: Array<(order: Order) => void> = [];
subscribe(callback: (order: Order) => void): void {
this.observers.push(callback);
}
processOrder(order: Order): OrderResult {
// Legacy processing logic
const result = this.legacyProcess(order);
// Notify observers (added as seam)
this.observers.forEach((callback) => callback(order));
return result;
}
private legacyProcess(order: Order): OrderResult {
// Complex legacy code we don't want to touch
}
}
// New features added via observers
class InventoryObserver {
constructor(private inventoryService: InventoryService) {}
observe(orderProcessor: LegacyOrderProcessor): void {
orderProcessor.subscribe((order) => {
order.items.forEach((item) => {
this.inventoryService.decrementStock(item.productId, item.quantity);
});
});
}
}
class EmailNotificationObserver {
constructor(private emailService: EmailService) {}
observe(orderProcessor: LegacyOrderProcessor): void {
orderProcessor.subscribe((order) => {
this.emailService.send({
to: order.customerEmail,
subject: 'Order Confirmation',
body: `Your order #${order.id} has been received`,
});
});
}
}
class AnalyticsObserver {
constructor(private analytics: AnalyticsService) {}
observe(orderProcessor: LegacyOrderProcessor): void {
orderProcessor.subscribe((order) => {
this.analytics.track('order_placed', {
orderId: order.id,
total: order.total,
itemCount: order.items.length,
});
});
}
}
// Wire up observers
const orderProcessor = new LegacyOrderProcessor();
new InventoryObserver(inventoryService).observe(orderProcessor);
new EmailNotificationObserver(emailService).observe(orderProcessor);
new AnalyticsObserver(analyticsService).observe(orderProcessor);
Example 2: Event-Driven Architecture
type EventType = 'USER_CREATED' | 'USER_UPDATED' | 'USER_DELETED';
interface DomainEvent<T = any> {
type: EventType;
timestamp: Date;
data: T;
}
class EventBus {
private subscribers = new Map<EventType, Array<(event: DomainEvent) => void>>();
subscribe(eventType: EventType, callback: (event: DomainEvent) => void): void {
if (!this.subscribers.has(eventType)) {
this.subscribers.set(eventType, []);
}
this.subscribers.get(eventType)!.push(callback);
}
publish(event: DomainEvent): void {
const callbacks = this.subscribers.get(event.type) || [];
callbacks.forEach((callback) => {
try {
callback(event);
} catch (error) {
console.error('Observer error:', error);
}
});
}
}
// Domain service publishes events
class UserService {
constructor(private eventBus: EventBus) {}
async createUser(user: User): Promise<User> {
const createdUser = await database.insert('users', user);
this.eventBus.publish({
type: 'USER_CREATED',
timestamp: new Date(),
data: createdUser,
});
return createdUser;
}
}
// Multiple systems react to events
class WelcomeEmailHandler {
constructor(eventBus: EventBus) {
eventBus.subscribe('USER_CREATED', (event) => {
const user = event.data;
emailService.send({
to: user.email,
template: 'welcome',
data: { name: user.name },
});
});
}
}
class UserAnalyticsHandler {
constructor(eventBus: EventBus) {
eventBus.subscribe('USER_CREATED', (event) => {
analytics.track('user_signup', { userId: event.data.id });
});
}
}
class CRMSyncHandler {
constructor(eventBus: EventBus) {
eventBus.subscribe('USER_CREATED', (event) => {
crmAPI.createContact(event.data);
});
}
}
Example 3: Reactive State Management
class Observable<T> {
private value: T;
private observers: Array<(value: T) => void> = [];
constructor(initialValue: T) {
this.value = initialValue;
}
get(): T {
return this.value;
}
set(newValue: T): void {
if (this.value !== newValue) {
this.value = newValue;
this.notify();
}
}
subscribe(callback: (value: T) => void): () => void {
this.observers.push(callback);
// Return unsubscribe function
return () => {
this.observers = this.observers.filter((cb) => cb !== callback);
};
}
private notify(): void {
this.observers.forEach((callback) => callback(this.value));
}
}
// Usage
const userCount = new Observable(0);
// UI subscribes to updates
const unsubscribe = userCount.subscribe((count) => {
document.getElementById('user-count').textContent = String(count);
});
// Update triggers UI refresh
userCount.set(42); // UI automatically updates
// Clean up
unsubscribe();
Common Pitfalls
1. Memory Leaks from Forgotten Unsubscribe
Problem: Observers never unsubscribe, preventing garbage collection.
// Bad: Creates memory leak
function setupObserver() {
const observer = new MyObserver();
subject.subscribe(observer);
// observer never unsubscribed!
}
Solution: Always clean up subscriptions:
// Good: Unsubscribe when done
class Component {
private unsubscribe: () => void;
mount() {
this.unsubscribe = subject.subscribe(this.handleUpdate);
}
unmount() {
this.unsubscribe();
}
handleUpdate = (data: Data) => {
// Handle update
};
}
2. Notification Storms
Problem: Updates trigger observers that trigger more updates.
Solution: Batch updates or prevent recursion:
class Subject<T> {
private isNotifying = false;
notify(data: T): void {
if (this.isNotifying) {
console.warn('Recursive notification prevented');
return;
}
this.isNotifying = true;
try {
this.observers.forEach((observer) => observer.update(data));
} finally {
this.isNotifying = false;
}
}
}
3. Observer Execution Order Dependency
Problem: Observers depend on execution order.
Solution: Make observers independent or use priority system:
interface PrioritizedObserver<T> extends Observer<T> {
priority: number;
}
class Subject<T> {
private observers: PrioritizedObserver<T>[] = [];
subscribe(observer: PrioritizedObserver<T>): void {
this.observers.push(observer);
this.observers.sort((a, b) => b.priority - a.priority);
}
}
Key Takeaways
- Observer pattern enables loose coupling between subjects and observers
- Perfect for adding new functionality to legacy systems without modification
- Forms the basis of event-driven architectures
- Watch for memory leaks—always unsubscribe observers
- Prevent notification storms with guards against recursion
- Observers should be independent, not rely on execution order
- Consider async observers for long-running operations
Further Reading
- Design Patterns - Gang of Four
- Reactive Programming - Event-driven systems
- EventEmitter Pattern - Node.js implementation
- RxJS - Powerful observable library