オブザーバーと戦略パターン
最新のアプリケーションの 2 つの基本的な動作パターン。 オブザーバー サブスクライバーへの自動通知を備えたイベント駆動型プログラミングを実装します。 戦略 クライアントを変更せずにランタイム アルゴリズムを変更できます。
🎯 何を学ぶか
- オブザーバー パターン: pub/sub およびイベント駆動型のアーキテクチャ
- Observer を使用してリアクティブ システムを実装する
- 戦略パターン: 交換可能なアルゴリズム
- 戦略で if/else を排除する
オブザーバーパターン
Il オブザーバーパターン (パブ/サブとも呼ばれます) 依存症を定義します 1 対多のオブジェクト。オブジェクトの状態が変化すると、そのオブジェクトに依存するすべてのオブジェクトが変化します。 自動的に通知されます。
// 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
プッシュ アンド プル モデルを使用したオブザーバー
次の 2 つのバリエーションがあります。 プッシュモデル (被験者がデータを送信) e プルモデル (オブザーバーがデータを要求):
// 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
Angular のオブザーバー パターン
Angular は Observer を次の方法で広範囲に使用します。 RxJS オブザーバブル:
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{{ '}' }}`);
{{ '}' }});
{{ '}' }}
{{ '}' }}
戦略パターン
Il 戦略パターン アルゴリズムのファミリーを定義し、それらをカプセル化します それぞれが別のクラスに属しており、交換可能になっています。戦略が可能にする アルゴリズムは、それを使用するクライアントに関係なく異なります。
// 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...
If/Else を排除する戦略
Strategy の主な用途の 1 つは、長い 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
動的検証を使用した戦略
実用的な例: 交換可能なルールを使用した検証システム:
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 {{ '}' }}
各パターンをいつ使用するか
✅ 次の場合にオブザーバーを使用します。
- 便利です 自動的に通知する 状態変更時のオブジェクトの増加
- あなたが欲しいのは 切り離す パブリッシャーからサブスクライバー
- イベント駆動型アーキテクチャ、リアルタイム更新
- 通知システム、ライブダッシュボード、チャット
✅ 次の場合に戦略を使用します。
- あなたが持っている 交換可能なアルゴリズムのファミリー
- 避けたいのは 長い if/else チェーン
- アルゴリズムはランタイムを変更する必要がある
- ソート、検証、支払い処理、圧縮
実践例: 両方を使用した電子商取引
// 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
結論
Observer と Strategy は、最新のアプリケーションの基本的なパターンです。 オブザーバー イベント駆動型プログラミング (RxJS、DOM イベント、通知) の核心ですが、 戦略 if/else をポリモーフィズムに置き換えることにより、手続き型コードを排除します。 それらを組み合わせることで、応答性の高い柔軟なシステムが作成されます。
🎯 重要なポイント
- オブザーバー = pub/sub、自動通知、イベント駆動型
- プッシュ モデル (サブジェクトがデータを送信) vs プル モデル (オブザーバーのリクエスト)
- 戦略 = 交換可能なアルゴリズム、if/else を排除
- リアクティブなシステムと通知にはオブザーバーを使用する
- ランタイム変数の動作にストラテジーを使用する
- Angular の RxJS = 強化されたパターン オブザーバー







