Observer e Strategy Pattern
Due pattern comportamentali fondamentali per applicazioni moderne. Observer implementa event-driven programming con notifiche automatiche ai subscriber, mentre Strategy permette di cambiare algoritmi runtime senza modificare il client.
🎯 Cosa Imparerai
- Observer pattern: pub/sub e event-driven architecture
- Implementare sistemi reattivi con Observer
- Strategy pattern: algoritmi intercambiabili
- Eliminare if/else con Strategy
Observer Pattern
Il Observer pattern (anche noto come pub/sub) definisce una dipendenza uno-a-molti tra oggetti, così che quando un oggetto cambia stato, tutti i suoi dipendenti vengono notificati automaticamente.
// Interfacce base
interface Observer {{ '{' }}
update(data: any): void;
{{ '}' }}
interface Subject {{ '{' }}
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
{{ '}' }}
// Subject concreto: fonte di eventi
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();
{{ '}' }}
{{ '}' }}
// Observer concreti
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{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Utilizzo
const agency = new NewsAgency();
const app1 = new MobileApp("App iOS");
const app2 = new MobileApp("App Android");
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...
// 📱 [App iOS] Received: Breaking news: TypeScript 5.0 released!
// 📱 [App Android] 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...
// 📱 [App iOS] Received: Update: Bug fixes available
// 📧 [user@example.com] Sending email: Update: Bug fixes available
Observer con Push e Pull Model
Esistono due varianti: Push model (Subject invia dati) e Pull model (Observer richiede dati):
// PUSH MODEL: Subject invia tutti i dati
interface PushObserver {{ '{' }}
update(temperature: number, humidity: number, pressure: number): void;
{{ '}' }}
// PULL MODEL: Observer richiede solo ciò che serve
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); // Passa se stesso
{{ '}' }}
{{ '}' }}
// Getter per 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 {{ '{' }}
// Richiede solo temperatura (Pull)
const temp = subject.getTemperature();
console.log(`🌡️ Temperature: ${{ '{' }}temp{{ '}' }}°C`);
{{ '}' }}
{{ '}' }}
class FullDisplay implements PullObserver {{ '{' }}
update(subject: WeatherStation): void {{ '{' }}
// Richiede tutti i dati
console.log(`🌦️ Weather: ${{ '{' }}subject.getTemperature(){{ '}' }}°C, ${{ '{' }}subject.getHumidity(){{ '}' }}%, ${{ '{' }}subject.getPressure(){{ '}' }}hPa`);
{{ '}' }}
{{ '}' }}
// Utilizzo
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 usa massivamente Observer tramite RxJS Observables:
import {{ '{' }} Subject {{ '}' }} from 'rxjs';
// Subject = Observable + Observer combinati
class MessageService {{ '{' }}
private messageSubject = new Subject<string>();
// Observable per subscribers
message$ = this.messageSubject.asObservable();
// Metodo per pubblicare
sendMessage(message: string): void {{ '{' }}
this.messageSubject.next(message);
{{ '}' }}
{{ '}' }}
// Utilizzo in componenti Angular
class ComponentA {{ '{' }}
constructor(private messageService: MessageService) {{ '{' }}{{ '}' }}
sendNotification(): void {{ '{' }}
this.messageService.sendMessage("Hello from Component A");
{{ '}' }}
{{ '}' }}
class ComponentB {{ '{' }}
constructor(private messageService: MessageService) {{ '{' }}
// Subscribe per ricevere messaggi
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
Il Strategy pattern definisce una famiglia di algoritmi, li incapsula ciascuno in una classe separata, e li rende intercambiabili. Strategy permette agli algoritmi di variare indipendentemente dai client che li usano.
// Interfaccia Strategy
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: usa la 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 = []; // Pulisci carrello
{{ '}' }}
{{ '}' }}
// Utilizzo
const cart = new ShoppingCart();
cart.addItem("Laptop", 1200);
cart.addItem("Mouse", 25);
// Usa diverse strategie di pagamento
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 per Eliminare If/Else
Uno degli usi principali di Strategy è eliminare lunghe catene di if/else:
// ❌ Approccio procedurale con 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 con 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);
{{ '}' }}
// Facile aggiungere nuove strategie
addStrategy(type: string, strategy: ShippingStrategy): void {{ '{' }}
this.strategies.set(type, strategy);
{{ '}' }}
{{ '}' }}
// Utilizzo
const calculator = new ShippingCalculatorV2();
console.log(calculator.calculateCost(10, 'standard')); // 15
console.log(calculator.calculateCost(10, 'overnight')); // 50
// Aggiungi strategia personalizzata
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 con Validazione Dinamica
Esempio pratico: sistema di validazione con regole intercambiabili:
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 caratteri, almeno una maiuscola, un numero
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()
{{ '}' }};
{{ '}' }}
{{ '}' }}
// Utilizzo
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 {{ '}' }}
Quando Usare Ogni Pattern
✅ Usa Observer quando:
- Serve notificare automaticamente più oggetti su cambiamenti di stato
- Vuoi disaccoppiare publisher da subscriber
- Event-driven architecture, real-time updates
- Sistema di notifiche, live dashboards, chat
✅ Usa Strategy quando:
- Hai famiglia di algoritmi intercambiabili
- Vuoi evitare lunghe catene if/else
- Algoritmo deve cambiare runtime
- Sorting, validazione, payment processing, compression
Esempio Pratico: E-Commerce con Entrambi
// 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 con 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;
{{ '}' }}
{{ '}' }}
// Utilizzo combinato
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
Conclusione
Observer e Strategy sono pattern fondamentali nelle applicazioni moderne. Observer è il cuore dell'event-driven programming (RxJS, eventi DOM, notifiche), mentre Strategy elimina codice procedurale sostituendo if/else con polimorfismo. Combinarli crea sistemi reattivi e flessibili.
🎯 Punti Chiave
- Observer = pub/sub, notifiche automatiche, event-driven
- Push model (Subject invia dati) vs Pull model (Observer richiede)
- Strategy = algoritmi intercambiabili, elimina if/else
- Usa Observer per sistemi reattivi e notifiche
- Usa Strategy per comportamenti variabili runtime
- RxJS in Angular = Observer pattern potenziato
Domande frequenti
Qual è il principale vantaggio dell'uso del Pattern Observer?
Il Pattern Observer consente di implementare un event-driven programming con notifiche automatiche ai subscriber, rendendo più facile gestire le dipendenze tra oggetti.
Come funziona il Push model nel contesto del Pattern Observer?
Nel Push model, l'oggetto Subject invia dati agli oggetti Observer interessati.
Qual è lo scopo principale del Pattern Strategy?
Il Pattern Strategy definisce una famiglia di algoritmi incapsulati in classi separate e li rende intercambiabili, permettendo agli algoritmi di variare indipendentemente dai client che li usano.
Come può essere utilizzato il Pattern Strategy per migliorare la codifica?
Il Pattern Strategy può essere utilizzato per eliminare lunghe catene di if/else e sostituirle con polimorfismo, rendendo il codice più flessibile e facile da mantenere.

Commenti
Caricamento commenti...
Accedi per commentare