Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Ever wondered how YouTube knows to update your notification bell when your favorite creator uploads? Or how your weather app updates automatically when conditions change? Thatโs the magic of the Observer Pattern in action! ๐
The Observer Pattern is like having a group chat where everyone gets notified when someone posts something important. Itโs one of the most useful design patterns in TypeScript, and today youโre going to master it! ๐ช
In this tutorial, youโll learn:
- What the Observer Pattern is and why itโs so powerful ๐ฎ
- How to implement it with TypeScriptโs type safety ๐ก๏ธ
- Real-world examples thatโll make you go โAha!โ ๐ก
- Best practices to avoid common pitfalls ๐ฏ
Ready to become an event subscription wizard? Letโs dive in! ๐โโ๏ธ
๐ Understanding Observer Pattern
The Observer Pattern is like subscribing to your favorite newsletter ๐ง. When new content is published (an event occurs), all subscribers (observers) automatically receive it without having to constantly check for updates!
Real-World Analogy ๐
Think of it like a pizza delivery tracking system ๐:
- The Pizza Tracker is the Subject (what youโre observing)
- You and other customers are the Observers (subscribers)
- When the pizza status changes, everyone gets notified! ๐ฑ
Benefits of Observer Pattern ๐
- Loose Coupling: Subjects and observers donโt need to know each otherโs details ๐
- Dynamic Relationships: Add or remove observers at runtime ๐
- Event-Driven Architecture: Perfect for reactive applications โก
- Scalability: Easy to add new observer types without changing existing code ๐
๐ง Basic Syntax and Usage
Letโs start with a simple Observer Pattern implementation in TypeScript:
// ๐ Define the Observer interface
interface Observer<T> {
update(data: T): void;
}
// ๐ข Define the Subject interface
interface Subject<T> {
attach(observer: Observer<T>): void;
detach(observer: Observer<T>): void;
notify(data: T): void;
}
// ๐ฏ Concrete Subject implementation
class EventEmitter<T> implements Subject<T> {
private observers: Observer<T>[] = [];
// โ Add an observer
attach(observer: Observer<T>): void {
console.log('๐ New observer joined!');
this.observers.push(observer);
}
// โ Remove an observer
detach(observer: Observer<T>): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
console.log('๐ Observer left!');
}
}
// ๐ข Notify all observers
notify(data: T): void {
console.log('๐ฃ Broadcasting to all observers...');
this.observers.forEach(observer => observer.update(data));
}
}
// ๐ Concrete Observer
class NewsSubscriber implements Observer<string> {
constructor(private name: string) {}
update(news: string): void {
console.log(`๐ฐ ${this.name} received: ${news}`);
}
}
// ๐ Let's see it in action!
const newsChannel = new EventEmitter<string>();
const alice = new NewsSubscriber('Alice');
const bob = new NewsSubscriber('Bob');
newsChannel.attach(alice);
newsChannel.attach(bob);
newsChannel.notify('Breaking: TypeScript 5.0 Released! ๐');
// Output:
// ๐ New observer joined!
// ๐ New observer joined!
// ๐ฃ Broadcasting to all observers...
// ๐ฐ Alice received: Breaking: TypeScript 5.0 Released! ๐
// ๐ฐ Bob received: Breaking: TypeScript 5.0 Released! ๐
๐ก Practical Examples
Example 1: Stock Price Tracker ๐
Letโs build a stock price monitoring system where investors get notified of price changes:
// ๐ฐ Stock price update interface
interface StockUpdate {
symbol: string;
price: number;
change: number;
timestamp: Date;
}
// ๐ Stock ticker subject
class StockTicker implements Subject<StockUpdate> {
private observers: Observer<StockUpdate>[] = [];
private stocks: Map<string, number> = new Map();
attach(observer: Observer<StockUpdate>): void {
this.observers.push(observer);
console.log('๐ New investor subscribed!');
}
detach(observer: Observer<StockUpdate>): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(data: StockUpdate): void {
this.observers.forEach(observer => observer.update(data));
}
// ๐น Update stock price
updateStock(symbol: string, newPrice: number): void {
const oldPrice = this.stocks.get(symbol) || newPrice;
this.stocks.set(symbol, newPrice);
const change = ((newPrice - oldPrice) / oldPrice) * 100;
const update: StockUpdate = {
symbol,
price: newPrice,
change,
timestamp: new Date()
};
console.log(`๐ ${symbol}: $${newPrice} (${change > 0 ? '+' : ''}${change.toFixed(2)}%)`);
this.notify(update);
}
}
// ๐ผ Different types of investors
class DayTrader implements Observer<StockUpdate> {
constructor(private name: string) {}
update(data: StockUpdate): void {
if (Math.abs(data.change) > 2) {
console.log(`๐ ${this.name}: Quick trade on ${data.symbol}! Big move: ${data.change.toFixed(2)}%`);
}
}
}
class LongTermInvestor implements Observer<StockUpdate> {
constructor(private name: string) {}
update(data: StockUpdate): void {
if (data.change < -10) {
console.log(`๐ ${this.name}: Buying the dip on ${data.symbol}! Down ${data.change.toFixed(2)}%`);
}
}
}
// ๐ฎ Let's simulate the market!
const nasdaq = new StockTicker();
const dayTrader = new DayTrader('FastMoney Mike');
const investor = new LongTermInvestor('Patient Patricia');
nasdaq.attach(dayTrader);
nasdaq.attach(investor);
// ๐ Market simulation
nasdaq.updateStock('TSLA', 250.00);
nasdaq.updateStock('TSLA', 260.00); // +4% triggers day trader
nasdaq.updateStock('AAPL', 180.00);
nasdaq.updateStock('AAPL', 160.00); // -11% triggers long-term investor
Example 2: Game Achievement System ๐ฎ
Create an achievement notification system for a game:
// ๐ Achievement data
interface Achievement {
id: string;
name: string;
description: string;
points: number;
icon: string;
}
// ๐ฎ Player progress data
interface ProgressUpdate {
playerId: string;
metric: string;
value: number;
}
// ๐
Achievement manager
class AchievementManager implements Subject<Achievement> {
private observers: Observer<Achievement>[] = [];
private playerProgress: Map<string, Map<string, number>> = new Map();
private achievements: Achievement[] = [
{ id: 'first_kill', name: 'First Blood', description: 'Defeat your first enemy', points: 10, icon: 'โ๏ธ' },
{ id: 'speed_demon', name: 'Speed Demon', description: 'Complete level in under 60 seconds', points: 25, icon: 'โก' },
{ id: 'collector', name: 'Collector', description: 'Collect 100 coins', points: 50, icon: '๐ฐ' }
];
attach(observer: Observer<Achievement>): void {
this.observers.push(observer);
}
detach(observer: Observer<Achievement>): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(achievement: Achievement): void {
this.observers.forEach(observer => observer.update(achievement));
}
// ๐ Update player progress
updateProgress(update: ProgressUpdate): void {
if (!this.playerProgress.has(update.playerId)) {
this.playerProgress.set(update.playerId, new Map());
}
const progress = this.playerProgress.get(update.playerId)!;
progress.set(update.metric, update.value);
// ๐ฏ Check for achievements
this.checkAchievements(update.playerId, update.metric, update.value);
}
private checkAchievements(playerId: string, metric: string, value: number): void {
// ๐ Check each achievement condition
if (metric === 'enemies_defeated' && value === 1) {
this.unlockAchievement('first_kill');
} else if (metric === 'level_time' && value < 60) {
this.unlockAchievement('speed_demon');
} else if (metric === 'coins_collected' && value >= 100) {
this.unlockAchievement('collector');
}
}
private unlockAchievement(achievementId: string): void {
const achievement = this.achievements.find(a => a.id === achievementId);
if (achievement) {
console.log(`๐ Achievement Unlocked: ${achievement.icon} ${achievement.name}!`);
this.notify(achievement);
}
}
}
// ๐ฑ UI notification system
class NotificationUI implements Observer<Achievement> {
update(achievement: Achievement): void {
console.log(`๐ฏ UI Popup: ${achievement.icon} ${achievement.name} - ${achievement.points} points!`);
}
}
// ๐ Stats tracker
class PlayerStats implements Observer<Achievement> {
private totalPoints = 0;
update(achievement: Achievement): void {
this.totalPoints += achievement.points;
console.log(`๐ Stats Updated: Total points = ${this.totalPoints}`);
}
}
// ๐ฎ Game in action!
const gameAchievements = new AchievementManager();
const ui = new NotificationUI();
const stats = new PlayerStats();
gameAchievements.attach(ui);
gameAchievements.attach(stats);
// ๐ฏ Player actions
gameAchievements.updateProgress({ playerId: 'player1', metric: 'enemies_defeated', value: 1 });
gameAchievements.updateProgress({ playerId: 'player1', metric: 'level_time', value: 45 });
gameAchievements.updateProgress({ playerId: 'player1', metric: 'coins_collected', value: 100 });
Example 3: Shopping Cart with Real-time Updates ๐
// ๐๏ธ Shopping cart events
type CartEvent = 'item_added' | 'item_removed' | 'cart_cleared';
interface CartUpdate {
event: CartEvent;
item?: Product;
totalItems: number;
totalPrice: number;
}
interface Product {
id: string;
name: string;
price: number;
emoji: string;
}
// ๐ Shopping cart subject
class ShoppingCart implements Subject<CartUpdate> {
private observers: Observer<CartUpdate>[] = [];
private items: Map<string, { product: Product; quantity: number }> = new Map();
attach(observer: Observer<CartUpdate>): void {
this.observers.push(observer);
}
detach(observer: Observer<CartUpdate>): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(update: CartUpdate): void {
this.observers.forEach(observer => observer.update(update));
}
// โ Add item to cart
addItem(product: Product, quantity: number = 1): void {
const existing = this.items.get(product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.items.set(product.id, { product, quantity });
}
console.log(`${product.emoji} Added ${product.name} to cart!`);
this.notify({
event: 'item_added',
item: product,
totalItems: this.getTotalItems(),
totalPrice: this.getTotalPrice()
});
}
// โ Remove item from cart
removeItem(productId: string): void {
const item = this.items.get(productId);
if (item) {
this.items.delete(productId);
console.log(`${item.product.emoji} Removed ${item.product.name} from cart!`);
this.notify({
event: 'item_removed',
item: item.product,
totalItems: this.getTotalItems(),
totalPrice: this.getTotalPrice()
});
}
}
private getTotalItems(): number {
return Array.from(this.items.values()).reduce((sum, item) => sum + item.quantity, 0);
}
private getTotalPrice(): number {
return Array.from(this.items.values()).reduce(
(sum, item) => sum + (item.product.price * item.quantity),
0
);
}
}
// ๐ฐ Price display observer
class PriceDisplay implements Observer<CartUpdate> {
update(data: CartUpdate): void {
console.log(`๐ฐ Total: $${data.totalPrice.toFixed(2)} (${data.totalItems} items)`);
}
}
// ๐ง Email notification observer
class EmailNotifier implements Observer<CartUpdate> {
update(data: CartUpdate): void {
if (data.event === 'item_added' && data.totalPrice > 100) {
console.log(`๐ง Email: You qualify for free shipping! ๐`);
}
}
}
// ๐ Analytics observer
class Analytics implements Observer<CartUpdate> {
private events: CartEvent[] = [];
update(data: CartUpdate): void {
this.events.push(data.event);
console.log(`๐ Analytics: ${data.event} tracked (Total events: ${this.events.length})`);
}
}
// ๐๏ธ Shopping time!
const cart = new ShoppingCart();
const priceDisplay = new PriceDisplay();
const emailer = new EmailNotifier();
const analytics = new Analytics();
cart.attach(priceDisplay);
cart.attach(emailer);
cart.attach(analytics);
// ๐ Add some products
const products: Product[] = [
{ id: '1', name: 'Gaming Keyboard', price: 79.99, emoji: 'โจ๏ธ' },
{ id: '2', name: 'Gaming Mouse', price: 49.99, emoji: '๐ฑ๏ธ' },
{ id: '3', name: 'Monitor', price: 299.99, emoji: '๐ฅ๏ธ' }
];
cart.addItem(products[0]);
cart.addItem(products[1]);
cart.addItem(products[2]); // Triggers free shipping email!
๐ Advanced Concepts
Type-Safe Event Emitter with Generics ๐งฌ
Letโs create a more advanced, type-safe event system:
// ๐ฏ Event map for strong typing
interface GameEvents {
'player:levelUp': { playerId: string; newLevel: number; };
'player:scoreUpdate': { playerId: string; score: number; highScore: boolean; };
'game:over': { winner: string; finalScore: number; };
}
// ๐ฅ Advanced type-safe event emitter
class TypedEventEmitter<TEvents extends Record<string, any>> {
private listeners: {
[K in keyof TEvents]?: Array<(data: TEvents[K]) => void>;
} = {};
// ๐ Type-safe event listener
on<K extends keyof TEvents>(event: K, listener: (data: TEvents[K]) => void): () => void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
// ๐ Return unsubscribe function
return () => this.off(event, listener);
}
// ๐ Remove listener
off<K extends keyof TEvents>(event: K, listener: (data: TEvents[K]) => void): void {
const listeners = this.listeners[event];
if (listeners) {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
// ๐ข Emit type-safe event
emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
const listeners = this.listeners[event];
if (listeners) {
listeners.forEach(listener => listener(data));
}
}
// ๐ฏ Once listener (auto-unsubscribe after first call)
once<K extends keyof TEvents>(event: K, listener: (data: TEvents[K]) => void): void {
const unsubscribe = this.on(event, (data) => {
listener(data);
unsubscribe();
});
}
}
// ๐ฎ Game system using typed events
class GameSystem {
private events = new TypedEventEmitter<GameEvents>();
// Expose event methods
on = this.events.on.bind(this.events);
emit = this.events.emit.bind(this.events);
// ๐ฏ Game actions
levelUpPlayer(playerId: string, newLevel: number): void {
console.log(`โฌ๏ธ Player ${playerId} reached level ${newLevel}!`);
this.emit('player:levelUp', { playerId, newLevel });
}
updateScore(playerId: string, score: number, highScore: boolean): void {
console.log(`๐ Player ${playerId} score: ${score}${highScore ? ' ๐ NEW HIGH SCORE!' : ''}`);
this.emit('player:scoreUpdate', { playerId, score, highScore });
}
endGame(winner: string, finalScore: number): void {
console.log(`๐ Game Over! Winner: ${winner} with ${finalScore} points!`);
this.emit('game:over', { winner, finalScore });
}
}
// ๐ฏ Usage with full type safety!
const game = new GameSystem();
// โ
TypeScript knows the exact shape of each event
game.on('player:levelUp', ({ playerId, newLevel }) => {
console.log(`๐ Celebration animation for ${playerId} reaching level ${newLevel}!`);
});
game.on('player:scoreUpdate', ({ score, highScore }) => {
if (highScore) {
console.log(`๐ Fireworks! New high score: ${score}!`);
}
});
// ๐ฎ Simulate game
game.levelUpPlayer('Alice', 5);
game.updateScore('Alice', 5000, true);
game.endGame('Alice', 5000);
Async Observer Pattern ๐
Handle asynchronous operations in observers:
// ๐ Async observer interface
interface AsyncObserver<T> {
update(data: T): Promise<void>;
}
// ๐ก Async subject
class AsyncEventEmitter<T> implements Subject<AsyncObserver<T>> {
private observers: AsyncObserver<T>[] = [];
attach(observer: AsyncObserver<T>): void {
this.observers.push(observer);
}
detach(observer: AsyncObserver<T>): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
// ๐ Notify all observers asynchronously
async notify(data: T): Promise<void> {
console.log('๐ก Broadcasting async event...');
// Option 1: Parallel execution
await Promise.all(
this.observers.map(observer => observer.update(data))
);
// Option 2: Sequential execution (uncomment if needed)
// for (const observer of this.observers) {
// await observer.update(data);
// }
}
}
// ๐พ Database logger (async)
class DatabaseLogger implements AsyncObserver<string> {
async update(message: string): Promise<void> {
console.log('๐พ Saving to database...');
// Simulate async database operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`โ
Logged: "${message}" to database`);
}
}
// ๐ง Email sender (async)
class EmailSender implements AsyncObserver<string> {
async update(message: string): Promise<void> {
console.log('๐ง Sending email...');
// Simulate async email sending
await new Promise(resolve => setTimeout(resolve, 200));
console.log(`โ
Email sent: "${message}"`);
}
}
// ๐ Test async observers
async function testAsyncObservers() {
const notifier = new AsyncEventEmitter<string>();
notifier.attach(new DatabaseLogger());
notifier.attach(new EmailSender());
await notifier.notify('Important system event occurred! ๐จ');
console.log('๐ฏ All observers notified!');
}
// Run the test
testAsyncObservers();
โ ๏ธ Common Pitfalls and Solutions
โ Wrong: Memory Leaks from Forgotten Observers
// โ BAD: Observers never get removed
class LeakyComponent {
private eventEmitter: EventEmitter<string>;
constructor(emitter: EventEmitter<string>) {
this.eventEmitter = emitter;
// Observer attached but never detached!
this.eventEmitter.attach({
update: (data) => console.log(data)
});
}
// No cleanup method! ๐ฑ
}
โ Correct: Proper Observer Cleanup
// โ
GOOD: Always clean up observers
class CleanComponent {
private eventEmitter: EventEmitter<string>;
private observer: Observer<string>;
constructor(emitter: EventEmitter<string>) {
this.eventEmitter = emitter;
this.observer = {
update: (data) => console.log(data)
};
this.eventEmitter.attach(this.observer);
}
// ๐งน Cleanup method
destroy(): void {
this.eventEmitter.detach(this.observer);
console.log('๐งน Observer cleaned up!');
}
}
โ Wrong: Modifying Observer List During Notification
// โ BAD: Causes unexpected behavior
class BadSubject<T> implements Subject<T> {
private observers: Observer<T>[] = [];
notify(data: T): void {
// โ Direct iteration while list might change
this.observers.forEach(observer => {
observer.update(data);
// What if update() calls detach()? ๐ฅ
});
}
}
โ Correct: Safe Notification with Copy
// โ
GOOD: Iterate over a copy
class SafeSubject<T> implements Subject<T> {
private observers: Observer<T>[] = [];
notify(data: T): void {
// โ
Create a copy to iterate safely
const observersCopy = [...this.observers];
observersCopy.forEach(observer => {
observer.update(data);
});
}
}
โ Wrong: Tight Coupling with Concrete Types
// โ BAD: Tightly coupled to specific implementation
class NewsChannel {
private subscribers: EmailSubscriber[] = []; // โ Too specific!
addSubscriber(subscriber: EmailSubscriber): void {
this.subscribers.push(subscriber);
}
}
โ Correct: Program to Interfaces
// โ
GOOD: Loosely coupled with interfaces
class NewsChannel {
private subscribers: Observer<string>[] = []; // โ
Uses interface!
addSubscriber(subscriber: Observer<string>): void {
this.subscribers.push(subscriber);
}
}
๐ ๏ธ Best Practices
-
๐ Use WeakMap for Private Observer Storage
const observersMap = new WeakMap<Subject<any>, Observer<any>[]>();
-
๐ฏ Implement Unsubscribe Tokens
const unsubscribe = emitter.on('event', handler); // Later: unsubscribe();
-
๐ Use Event Types for Better Organization
type EventMap = { click: MouseEvent; change: string; };
-
๐ก๏ธ Add Error Handling in Observers
try { observer.update(data); } catch (error) { console.error('Observer error:', error); }
-
๐ Consider Using Existing Libraries
- RxJS for complex reactive programming
- Node.js EventEmitter for simple cases
- MobX for state management with observers
๐งช Hands-On Exercise
Ready to put your skills to the test? Letโs build a Chat Room system! ๐ฌ
Requirements:
- Create a
ChatRoom
class that acts as the subject - Create different types of observers:
User
- receives and displays messagesMessageLogger
- logs all messagesProfanityFilter
- alerts on inappropriate content
- Messages should include: sender, content, timestamp
- Users should be able to join/leave the chat
- Bonus: Add private messaging between users! ๐คซ
๐ก Click here for the solution
// ๐ฌ Message interface
interface ChatMessage {
id: string;
sender: string;
content: string;
timestamp: Date;
isPrivate: boolean;
recipient?: string;
}
// ๐ Chat room subject
class ChatRoom implements Subject<ChatMessage> {
private observers: Map<string, Observer<ChatMessage>> = new Map();
private messageHistory: ChatMessage[] = [];
private messageId = 0;
// ๐ User joins chat
join(username: string, observer: Observer<ChatMessage>): void {
this.observers.set(username, observer);
console.log(`๐ ${username} joined the chat!`);
// Send join notification
this.broadcast('System', `${username} has entered the chat! ๐`);
}
// ๐ User leaves chat
leave(username: string): void {
if (this.observers.delete(username)) {
console.log(`๐ ${username} left the chat!`);
this.broadcast('System', `${username} has left the chat ๐ข`);
}
}
// ๐ข Public message
sendMessage(sender: string, content: string): void {
const message: ChatMessage = {
id: `msg-${++this.messageId}`,
sender,
content,
timestamp: new Date(),
isPrivate: false
};
this.messageHistory.push(message);
this.notify(message);
}
// ๐คซ Private message
sendPrivateMessage(sender: string, recipient: string, content: string): void {
const message: ChatMessage = {
id: `msg-${++this.messageId}`,
sender,
content,
timestamp: new Date(),
isPrivate: true,
recipient
};
this.messageHistory.push(message);
// Only notify sender and recipient
const senderObserver = this.observers.get(sender);
const recipientObserver = this.observers.get(recipient);
if (senderObserver) senderObserver.update(message);
if (recipientObserver) recipientObserver.update(message);
console.log(`๐คซ Private message from ${sender} to ${recipient}`);
}
// ๐ฃ System broadcast
private broadcast(sender: string, content: string): void {
const message: ChatMessage = {
id: `msg-${++this.messageId}`,
sender,
content,
timestamp: new Date(),
isPrivate: false
};
this.notify(message);
}
// Required Subject methods
attach(observer: Observer<ChatMessage>): void {
// Not used directly - use join() instead
}
detach(observer: Observer<ChatMessage>): void {
// Not used directly - use leave() instead
}
notify(message: ChatMessage): void {
if (message.isPrivate) return; // Private messages handled separately
this.observers.forEach((observer, username) => {
observer.update(message);
});
}
}
// ๐ค Chat user
class User implements Observer<ChatMessage> {
constructor(private username: string) {}
update(message: ChatMessage): void {
const time = message.timestamp.toLocaleTimeString();
if (message.isPrivate) {
if (message.sender === this.username) {
console.log(`๐ค [${time}] You โ ${message.recipient}: ${message.content}`);
} else {
console.log(`๐จ [${time}] ${message.sender} โ You (private): ${message.content}`);
}
} else {
const prefix = message.sender === 'System' ? '๐ข' : '๐ฌ';
console.log(`${prefix} [${time}] ${message.sender}: ${message.content}`);
}
}
}
// ๐ Message logger
class MessageLogger implements Observer<ChatMessage> {
private logFile: ChatMessage[] = [];
update(message: ChatMessage): void {
this.logFile.push(message);
console.log(`๐ Logged message #${this.logFile.length}`);
}
}
// ๐ซ Profanity filter
class ProfanityFilter implements Observer<ChatMessage> {
private badWords = ['spam', 'scam', 'fake'];
update(message: ChatMessage): void {
const hasProfanity = this.badWords.some(word =>
message.content.toLowerCase().includes(word)
);
if (hasProfanity) {
console.log(`๐จ Warning: Inappropriate content detected from ${message.sender}!`);
}
}
}
// ๐ฎ Let's chat!
const chatRoom = new ChatRoom();
const logger = new MessageLogger();
const filter = new ProfanityFilter();
// Set up system observers
chatRoom.attach(logger);
chatRoom.attach(filter);
// Users join
const alice = new User('Alice');
const bob = new User('Bob');
const charlie = new User('Charlie');
chatRoom.join('Alice', alice);
chatRoom.join('Bob', bob);
chatRoom.join('Charlie', charlie);
// Chat simulation
chatRoom.sendMessage('Alice', 'Hey everyone! ๐');
chatRoom.sendMessage('Bob', 'Hi Alice! How are you?');
chatRoom.sendPrivateMessage('Alice', 'Bob', "I'm great! Want to grab coffee later? โ");
chatRoom.sendMessage('Charlie', 'This chat is spam!'); // Triggers profanity filter
chatRoom.leave('Charlie');
๐ Key Takeaways
Youโve mastered the Observer Pattern! Hereโs what youโve learned:
โ
Observer Pattern Basics: Subjects notify observers of state changes automatically
โ
Type-Safe Implementation: Use TypeScript interfaces and generics for robust code
โ
Real-World Applications: Stock tickers, game systems, shopping carts, and more
โ
Memory Management: Always detach observers to prevent memory leaks
โ
Advanced Patterns: Async observers, typed event emitters, and error handling
Remember: The Observer Pattern is perfect when you need loose coupling between objects that need to communicate! ๐
๐ค Next Steps
Congratulations on mastering the Observer Pattern! ๐ Youโre becoming a true TypeScript design pattern expert!
Hereโs what to explore next:
- ๐ Try implementing Observer Pattern with RxJS for reactive programming
- ๐ Combine Observer with other patterns like Command or Mediator
- ๐๏ธ Build a real-time notification system for your next project
- ๐ Check out the next tutorial: Strategy Pattern: Flexible Algorithms
The Observer Pattern is everywhere in modern applications - from Reactโs state management to Node.js EventEmitters. Now you have the power to use it effectively!
Keep building amazing event-driven systems! Youโre doing fantastic! ๐ชโจ