관찰자와 전략 패턴
최신 애플리케이션을 위한 두 가지 기본 동작 패턴입니다. 관찰자 구독자에게 자동으로 알림을 보내는 이벤트 기반 프로그래밍을 구현합니다. 전략 클라이언트를 수정하지 않고도 런타임 알고리즘을 변경할 수 있습니다.
🎯 무엇을 배울 것인가
- 관찰자 패턴: 게시/구독 및 이벤트 기반 아키텍처
- Observer를 사용하여 반응형 시스템 구현
- 전략 패턴: 상호 교환 가능한 알고리즘
- 전략을 사용하여 if/else 제거
관찰자 패턴
Il 관찰자 패턴 (pub/sub라고도 함)은 중독을 정의합니다. 일대다 객체이므로 객체의 상태가 변경되면 해당 객체의 모든 종속 항목이 자동으로 통보됩니다.
// 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
밀고 당기는 모델을 갖춘 관찰자
두 가지 변형이 있습니다. 푸시 모델 (대상이 데이터를 전송함) 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를 제거하는 전략
전략의 주요 용도 중 하나는 긴 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 {{ '}' }}
각 패턴을 사용하는 경우
✅ 다음과 같은 경우에 Observer를 사용하세요:
- 유용하다 자동으로 알림 상태 변경에 대한 더 많은 객체
- 당신이 원하는 분리하다 구독자의 게시자
- 이벤트 기반 아키텍처, 실시간 업데이트
- 알림 시스템, 라이브 대시보드, 채팅
✅ 다음과 같은 경우에 전략을 사용하세요:
- 당신은 상호 교환 가능한 알고리즘 제품군
- 피하고 싶으신가요? 긴 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
결론
관찰자와 전략은 현대 애플리케이션의 기본 패턴입니다. 관찰자 이벤트 중심 프로그래밍(RxJS, DOM 이벤트, 알림)의 핵심입니다. 전략 if/else를 다형성으로 대체하여 절차적 코드를 제거합니다. 이를 결합하면 반응성이 뛰어나고 유연한 시스템이 만들어집니다.
🎯 핵심 포인트
- 관찰자 = 게시/구독, 자동 알림, 이벤트 기반
- 푸시 모델(주체가 데이터를 보냅니다)과 풀 모델(관찰자 요청)
- 전략 = 상호 교환 가능한 알고리즘, if/else 제거
- 반응형 시스템 및 알림에 Observer 사용
- 런타임 변수 동작에 전략 사용
- Angular의 RxJS = 향상된 패턴 관찰자







