Observer and Strategy Pattern
Two fundamental behavioral patterns for modern applications. Observer implements event-driven programming with automatic notifications to subscribers, while Strategy allows swapping algorithms at runtime without modifying the client.
🎯 What You Will Learn
- Observer pattern: pub/sub and event-driven architecture
- Implementing reactive systems with Observer
- Strategy pattern: interchangeable algorithms
- Eliminating if/else with Strategy
Observer Pattern
The Observer pattern (also known as pub/sub) defines a one-to-many dependency between objects, so that when one object changes state, all of its dependents are notified automatically.
// Base interfaces
interface Observer {{ '{' }}
update(data: any): void;
{{ '}' }}
interface Subject {{ '{' }}
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
{{ '}' }}
// Concrete Subject: event source
class NewsAgency implements Subject {{ '{' }}
private observers: Observer[] = [];
private latestNews: string = "";
attach(observer: Observer): void {{ '{' }}
const index = this.observers.indexOf(observer);
if (index === -1) {{ '{' }}
this.observers.push(observer);
console.log(`✅ Observer attached (total: ${{ '{' }}this.observers.length{{ '}' }})`);
{{ '}' }}
{{ '}' }}
detach(observer: Observer): void {{ '{' }}
const index = this.observers.indexOf(observer);
if (index !== -1) {{ '{' }}
this.observers.splice(index, 1);
console.log(`❌ Observer detached (total: ${{ '{' }}this.observers.length{{ '}' }})`);
{{ '}' }}
{{ '}' }}
notify(): void {{ '{' }}
console.log(`📢 Notifying ${{ '{' }}this.observers.length{{ '}' }} observers...`);
for (const observer of this.observers) {{ '{' }}
observer.update(this.latestNews);
{{ '}' }}
{{ '}' }}
publishNews(news: string): void {{ '{' }}
console.log(`📰 Publishing: ${{ '{' }}news{{ '}' }}`);
this.latestNews = news;
this.notify();
{{ '}' }}
{{ '}' }}
// Concrete Observers
class MobileApp implements Observer {{ '{' }}
constructor(private name: string) {{ '{' }}{{ '}' }}
update(data: any): void {{ '{' }}
console.log(`📱 [${{ '{' }}this.name{{ '}' }}] Received: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class EmailSubscriber implements Observer {{ '{' }}
constructor(private email: string) {{ '{' }}{{ '}' }}
update(data: any): void {{ '{' }}
console.log(`📧 [${{ '{' }}this.email{{ '}' }}] Sending email: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Usage
const agency = new NewsAgency();
const app1 = new MobileApp("iOS App");
const app2 = new MobileApp("Android App");
const emailer = new EmailSubscriber("user@example.com");
agency.attach(app1);
agency.attach(app2);
agency.attach(emailer);
agency.publishNews("Breaking news: TypeScript 5.0 released!");
// 📰 Publishing: Breaking news: TypeScript 5.0 released!
// 📢 Notifying 3 observers...
// 📱 [iOS App] Received: Breaking news: TypeScript 5.0 released!
// 📱 [Android App] Received: Breaking news: TypeScript 5.0 released!
// 📧 [user@example.com] Sending email: Breaking news: TypeScript 5.0 released!
agency.detach(app2);
agency.publishNews("Update: Bug fixes available");
// ❌ Observer detached (total: 2)
// 📰 Publishing: Update: Bug fixes available
// 📢 Notifying 2 observers...
// 📱 [iOS App] Received: Update: Bug fixes available
// 📧 [user@example.com] Sending email: Update: Bug fixes available
Observer with Push and Pull Models
There are two variants: Push model (Subject sends data) and Pull model (Observer requests data):
// PUSH MODEL: Subject sends all data
interface PushObserver {{ '{' }}
update(temperature: number, humidity: number, pressure: number): void;
{{ '}' }}
// PULL MODEL: Observer requests only what it needs
interface PullObserver {{ '{' }}
update(subject: WeatherStation): void;
{{ '}' }}
class WeatherStation {{ '{' }}
private observers: PullObserver[] = [];
private temperature: number = 0;
private humidity: number = 0;
private pressure: number = 0;
attach(observer: PullObserver): void {{ '{' }}
this.observers.push(observer);
{{ '}' }}
setMeasurements(temp: number, humidity: number, pressure: number): void {{ '{' }}
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
this.notify();
{{ '}' }}
notify(): void {{ '{' }}
for (const observer of this.observers) {{ '{' }}
observer.update(this); // Passes itself
{{ '}' }}
{{ '}' }}
// Getters for Pull model
getTemperature(): number {{ '{' }} return this.temperature; {{ '}' }}
getHumidity(): number {{ '{' }} return this.humidity; {{ '}' }}
getPressure(): number {{ '{' }} return this.pressure; {{ '}' }}
{{ '}' }}
class TemperatureDisplay implements PullObserver {{ '{' }}
update(subject: WeatherStation): void {{ '{' }}
// Requests only temperature (Pull)
const temp = subject.getTemperature();
console.log(`🌡️ Temperature: ${{ '{' }}temp{{ '}' }}°C`);
{{ '}' }}
{{ '}' }}
class FullDisplay implements PullObserver {{ '{' }}
update(subject: WeatherStation): void {{ '{' }}
// Requests all data
console.log(`🌦️ Weather: ${{ '{' }}subject.getTemperature(){{ '}' }}°C, ${{ '{' }}subject.getHumidity(){{ '}' }}%, ${{ '{' }}subject.getPressure(){{ '}' }}hPa`);
{{ '}' }}
{{ '}' }}
// Usage
const station = new WeatherStation();
station.attach(new TemperatureDisplay());
station.attach(new FullDisplay());
station.setMeasurements(25, 65, 1013);
// 🌡️ Temperature: 25°C
// 🌦️ Weather: 25°C, 65%, 1013hPa
Observer Pattern in Angular
Angular uses Observer extensively through RxJS Observables:
import {{ '{' }} Subject {{ '}' }} from 'rxjs';
// Subject = Observable + Observer combined
class MessageService {{ '{' }}
private messageSubject = new Subject<string>();
// Observable for subscribers
message$ = this.messageSubject.asObservable();
// Method for publishing
sendMessage(message: string): void {{ '{' }}
this.messageSubject.next(message);
{{ '}' }}
{{ '}' }}
// Usage in Angular components
class ComponentA {{ '{' }}
constructor(private messageService: MessageService) {{ '{' }}{{ '}' }}
sendNotification(): void {{ '{' }}
this.messageService.sendMessage("Hello from Component A");
{{ '}' }}
{{ '}' }}
class ComponentB {{ '{' }}
constructor(private messageService: MessageService) {{ '{' }}
// Subscribe to receive messages
this.messageService.message$.subscribe(msg => {{ '{' }}
console.log(`ComponentB received: ${{ '{' }}msg{{ '}' }}`);
{{ '}' }});
{{ '}' }}
{{ '}' }}
class ComponentC {{ '{' }}
constructor(private messageService: MessageService) {{ '{' }}
this.messageService.message$.subscribe(msg => {{ '{' }}
console.log(`ComponentC received: ${{ '{' }}msg{{ '}' }}`);
{{ '}' }});
{{ '}' }}
{{ '}' }}
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one in a separate class, and makes them interchangeable. Strategy allows algorithms to vary independently from the clients that use them.
// Strategy Interface
interface PaymentStrategy {{ '{' }}
pay(amount: number): void;
{{ '}' }}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {{ '{' }}
constructor(
private cardNumber: string,
private cvv: string
) {{ '{' }}{{ '}' }}
pay(amount: number): void {{ '{' }}
console.log(`💳 Paid ${{ '{' }}amount{{ '}' }} with Credit Card ending in ${{ '{' }}this.cardNumber.slice(-4){{ '}' }}`);
{{ '}' }}
{{ '}' }}
class PayPalPayment implements PaymentStrategy {{ '{' }}
constructor(private email: string) {{ '{' }}{{ '}' }}
pay(amount: number): void {{ '{' }}
console.log(`🅿️ Paid ${{ '{' }}amount{{ '}' }} via PayPal (${{ '{' }}this.email{{ '}' }})`);
{{ '}' }}
{{ '}' }}
class CryptoPayment implements PaymentStrategy {{ '{' }}
constructor(private walletAddress: string) {{ '{' }}{{ '}' }}
pay(amount: number): void {{ '{' }}
console.log(`₿ Paid ${{ '{' }}amount{{ '}' }} in crypto to ${{ '{' }}this.walletAddress.slice(0, 8){{ '}' }}...`);
{{ '}' }}
{{ '}' }}
// Context: uses the strategy
class ShoppingCart {{ '{' }}
private items: {{ '{' }} name: string; price: number {{ '}' }}[] = [];
private paymentStrategy?: PaymentStrategy;
addItem(name: string, price: number): void {{ '{' }}
this.items.push({{ '{' }} name, price {{ '}' }});
{{ '}' }}
setPaymentStrategy(strategy: PaymentStrategy): void {{ '{' }}
this.paymentStrategy = strategy;
{{ '}' }}
checkout(): void {{ '{' }}
const total = this.items.reduce((sum, item) => sum + item.price, 0);
console.log(`🛒 Total: $${{ '{' }}total{{ '}' }}`);
if (!this.paymentStrategy) {{ '{' }}
console.log("❌ No payment method selected");
return;
{{ '}' }}
this.paymentStrategy.pay(total);
this.items = []; // Clear cart
{{ '}' }}
{{ '}' }}
// Usage
const cart = new ShoppingCart();
cart.addItem("Laptop", 1200);
cart.addItem("Mouse", 25);
// Use different payment strategies
cart.setPaymentStrategy(new CreditCardPayment("1234567890123456", "123"));
cart.checkout();
// 🛒 Total: $1225
// 💳 Paid 1225 with Credit Card ending in 3456
cart.addItem("Keyboard", 75);
cart.setPaymentStrategy(new PayPalPayment("user@paypal.com"));
cart.checkout();
// 🛒 Total: $75
// 🅿️ Paid 75 via PayPal (user@paypal.com)
cart.addItem("Monitor", 350);
cart.setPaymentStrategy(new CryptoPayment("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8"));
cart.checkout();
// 🛒 Total: $350
// ₿ Paid 350 in crypto to 0x742d35...
Strategy for Eliminating If/Else
One of the main uses of Strategy is eliminating long if/else chains:
// ❌ Procedural approach with if/else
class ShippingCalculator {{ '{' }}
calculateCost(weight: number, type: string): number {{ '{' }}
if (type === 'standard') {{ '{' }}
return weight * 1.5;
{{ '}' }} else if (type === 'express') {{ '{' }}
return weight * 3.0;
{{ '}' }} else if (type === 'overnight') {{ '{' }}
return weight * 5.0;
{{ '}' }} else if (type === 'international') {{ '{' }}
return weight * 7.5;
{{ '}' }} else {{ '{' }}
throw new Error("Unknown shipping type");
{{ '}' }}
{{ '}' }}
{{ '}' }}
interface ShippingStrategy {{ '{' }}
calculate(weight: number): number;
{{ '}' }}
class StandardShipping implements ShippingStrategy {{ '{' }}
calculate(weight: number): number {{ '{' }}
return weight * 1.5;
{{ '}' }}
{{ '}' }}
class ExpressShipping implements ShippingStrategy {{ '{' }}
calculate(weight: number): number {{ '{' }}
return weight * 3.0;
{{ '}' }}
{{ '}' }}
class OvernightShipping implements ShippingStrategy {{ '{' }}
calculate(weight: number): number {{ '{' }}
return weight * 5.0;
{{ '}' }}
{{ '}' }}
class InternationalShipping implements ShippingStrategy {{ '{' }}
calculate(weight: number): number {{ '{' }}
return weight * 7.5;
{{ '}' }}
{{ '}' }}
// Context with Strategy Map
class ShippingCalculatorV2 {{ '{' }}
private strategies: Map<string, ShippingStrategy> = new Map([
['standard', new StandardShipping()],
['express', new ExpressShipping()],
['overnight', new OvernightShipping()],
['international', new InternationalShipping()],
]);
calculateCost(weight: number, type: string): number {{ '{' }}
const strategy = this.strategies.get(type);
if (!strategy) {{ '{' }}
throw new Error(`Unknown shipping type: ${{ '{' }}type{{ '}' }}`);
{{ '}' }}
return strategy.calculate(weight);
{{ '}' }}
// Easy to add new strategies
addStrategy(type: string, strategy: ShippingStrategy): void {{ '{' }}
this.strategies.set(type, strategy);
{{ '}' }}
{{ '}' }}
// Usage
const calculator = new ShippingCalculatorV2();
console.log(calculator.calculateCost(10, 'standard')); // 15
console.log(calculator.calculateCost(10, 'overnight')); // 50
// Add custom strategy
class SameDayShipping implements ShippingStrategy {{ '{' }}
calculate(weight: number): number {{ '{' }} return weight * 8.0; {{ '}' }}
{{ '}' }}
calculator.addStrategy('sameday', new SameDayShipping());
console.log(calculator.calculateCost(10, 'sameday')); // 80
Strategy with Dynamic Validation
Practical example: validation system with interchangeable rules:
interface ValidationStrategy {{ '{' }}
validate(value: string): boolean;
getErrorMessage(): string;
{{ '}' }}
class EmailValidator implements ValidationStrategy {{ '{' }}
validate(value: string): boolean {{ '{' }}
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
{{ '}' }}
getErrorMessage(): string {{ '{' }}
return "Invalid email format";
{{ '}' }}
{{ '}' }}
class PasswordValidator implements ValidationStrategy {{ '{' }}
validate(value: string): boolean {{ '{' }}
// Min 8 characters, at least one uppercase, one number
return value.length >= 8 && /[A-Z]/.test(value) && /[0-9]/.test(value);
{{ '}' }}
getErrorMessage(): string {{ '{' }}
return "Password must be 8+ chars with uppercase and number";
{{ '}' }}
{{ '}' }}
class PhoneValidator implements ValidationStrategy {{ '{' }}
validate(value: string): boolean {{ '{' }}
return /^\+?[1-9]\d{{ '{' }}9,14{{ '}' }}$/.test(value.replace(/\s/g, ''));
{{ '}' }}
getErrorMessage(): string {{ '{' }}
return "Invalid phone number format";
{{ '}' }}
{{ '}' }}
class FormField {{ '{' }}
private validator?: ValidationStrategy;
constructor(
private name: string,
private value: string
) {{ '{' }}{{ '}' }}
setValidator(validator: ValidationStrategy): void {{ '{' }}
this.validator = validator;
{{ '}' }}
validate(): {{ '{' }} valid: boolean; error?: string {{ '}' }} {{ '{' }}
if (!this.validator) {{ '{' }}
return {{ '{' }} valid: true {{ '}' }};
{{ '}' }}
const valid = this.validator.validate(this.value);
return {{ '{' }}
valid,
error: valid ? undefined : this.validator.getErrorMessage()
{{ '}' }};
{{ '}' }}
{{ '}' }}
// Usage
const emailField = new FormField("email", "user@example.com");
emailField.setValidator(new EmailValidator());
console.log(emailField.validate()); // {{ '{' }} valid: true {{ '}' }}
const passwordField = new FormField("password", "weak");
passwordField.setValidator(new PasswordValidator());
console.log(passwordField.validate());
// {{ '{' }} valid: false, error: 'Password must be 8+ chars with uppercase and number' {{ '}' }}
const phoneField = new FormField("phone", "+1234567890");
phoneField.setValidator(new PhoneValidator());
console.log(phoneField.validate()); // {{ '{' }} valid: true {{ '}' }}
When to Use Each Pattern
✅ Use Observer when:
- You need to automatically notify multiple objects about state changes
- You want to decouple publisher from subscriber
- Event-driven architecture, real-time updates
- Notification systems, live dashboards, chat
✅ Use Strategy when:
- You have a family of interchangeable algorithms
- You want to avoid long if/else chains
- The algorithm must change at runtime
- Sorting, validation, payment processing, compression
Practical Example: E-Commerce with Both
// STRATEGY: Pricing strategies
interface PricingStrategy {{ '{' }}
calculatePrice(basePrice: number): number;
{{ '}' }}
class RegularPricing implements PricingStrategy {{ '{' }}
calculatePrice(basePrice: number): number {{ '{' }}
return basePrice;
{{ '}' }}
{{ '}' }}
class BlackFridayPricing implements PricingStrategy {{ '{' }}
calculatePrice(basePrice: number): number {{ '{' }}
return basePrice * 0.5; // 50% off
{{ '}' }}
{{ '}' }}
class MemberPricing implements PricingStrategy {{ '{' }}
calculatePrice(basePrice: number): number {{ '{' }}
return basePrice * 0.9; // 10% off
{{ '}' }}
{{ '}' }}
// OBSERVER: Price observers
interface PriceObserver {{ '{' }}
onPriceChange(productName: string, oldPrice: number, newPrice: number): void;
{{ '}' }}
class PriceAlertService implements PriceObserver {{ '{' }}
onPriceChange(productName: string, oldPrice: number, newPrice: number): void {{ '{' }}
console.log(`🔔 Alert: ${{ '{' }}productName{{ '}' }} price changed from $${{ '{' }}oldPrice{{ '}' }} to $${{ '{' }}newPrice{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class AnalyticsService implements PriceObserver {{ '{' }}
onPriceChange(productName: string, oldPrice: number, newPrice: number): void {{ '{' }}
console.log(`📊 Analytics: Price change recorded for ${{ '{' }}productName{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Product with Strategy + Observer
class Product {{ '{' }}
private observers: PriceObserver[] = [];
private pricingStrategy: PricingStrategy = new RegularPricing();
private currentPrice: number;
constructor(
private name: string,
private basePrice: number
) {{ '{' }}
this.currentPrice = this.calculatePrice();
{{ '}' }}
attach(observer: PriceObserver): void {{ '{' }}
this.observers.push(observer);
{{ '}' }}
setPricingStrategy(strategy: PricingStrategy): void {{ '{' }}
this.pricingStrategy = strategy;
this.updatePrice();
{{ '}' }}
private calculatePrice(): number {{ '{' }}
return this.pricingStrategy.calculatePrice(this.basePrice);
{{ '}' }}
private updatePrice(): void {{ '{' }}
const oldPrice = this.currentPrice;
this.currentPrice = this.calculatePrice();
if (oldPrice !== this.currentPrice) {{ '{' }}
this.notifyObservers(oldPrice, this.currentPrice);
{{ '}' }}
{{ '}' }}
private notifyObservers(oldPrice: number, newPrice: number): void {{ '{' }}
for (const observer of this.observers) {{ '{' }}
observer.onPriceChange(this.name, oldPrice, newPrice);
{{ '}' }}
{{ '}' }}
getPrice(): number {{ '{' }}
return this.currentPrice;
{{ '}' }}
{{ '}' }}
// Combined usage
const laptop = new Product("Gaming Laptop", 1500);
laptop.attach(new PriceAlertService());
laptop.attach(new AnalyticsService());
console.log(`Initial price: $${{ '{' }}laptop.getPrice(){{ '}' }}`); // 1500
laptop.setPricingStrategy(new BlackFridayPricing());
// 🔔 Alert: Gaming Laptop price changed from $1500 to $750
// 📊 Analytics: Price change recorded for Gaming Laptop
console.log(`Black Friday price: $${{ '{' }}laptop.getPrice(){{ '}' }}`); // 750
laptop.setPricingStrategy(new MemberPricing());
// 🔔 Alert: Gaming Laptop price changed from $750 to $1350
// 📊 Analytics: Price change recorded for Gaming Laptop
console.log(`Member price: $${{ '{' }}laptop.getPrice(){{ '}' }}`); // 1350
Conclusion
Observer and Strategy are fundamental patterns in modern applications. Observer is at the heart of event-driven programming (RxJS, DOM events, notifications), while Strategy eliminates procedural code by replacing if/else with polymorphism. Combining them creates reactive and flexible systems.
Related deep dive: 06 - Command, Chain of Responsibility e Template Method.
Related deep dive: 1 - Clean Architecture in Angular: Struttura Scalabile.
🎯 Key Takeaways
- Observer = pub/sub, automatic notifications, event-driven
- Push model (Subject sends data) vs Pull model (Observer requests)
- Strategy = interchangeable algorithms, eliminates if/else
- Use Observer for reactive systems and notifications
- Use Strategy for variable runtime behaviors
- RxJS in Angular = enhanced Observer pattern
Frequently asked questions
What is the main difference between the Push model and the Pull model in the Observer pattern?
The Push model involves the Subject sending data to its dependents, while the Pull model involves the Observer requesting data from the Subject.
How does Angular use the Observer pattern?
Angular uses the Observer pattern extensively through RxJS Observables.
What is the primary purpose of the Strategy pattern?
The primary purpose of the Strategy pattern is to define a family of algorithms and make them interchangeable, allowing clients to vary independently from the algorithms used.
How can using the Strategy pattern eliminate procedural code in an application?
Using the Strategy pattern can eliminate procedural code by replacing if/else chains with polymorphism.
Commenti
Caricamento commenti...
Sign in to comment