Where Can I Learn About My Horoscope · CodeAmber

Implementing Strategy and Observer Patterns in Modern TypeScript

Implementing the Strategy and Observer patterns in TypeScript requires leveraging interfaces and generics to decouple logic from execution. The Strategy pattern replaces conditional branching with interchangeable object compositions, while the Observer pattern creates a one-to-many dependency between objects so that state changes automatically propagate to multiple subscribers.

Implementing Strategy and Observer Patterns in Modern TypeScript

Design patterns are not merely templates but architectural solutions to recurring software problems. In TypeScript, the strong typing system allows developers to implement these patterns with compile-time safety, ensuring that interchangeable components adhere to a strict contract. Mastering these allows engineers to implement common design patterns in modern code that are scalable and easy to test.

The Strategy Pattern: Eliminating Conditional Complexity

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It is the primary solution for removing bloated if/else or switch statements that govern business logic.

The "Bad" Way: Conditional Bloat

In a naive implementation, a developer often handles multiple behaviors within a single class using a conditional block. This violates the Open/Closed Principle: the class must be modified every time a new behavior is added.

class PaymentProcessor {
  processPayment(amount: number, method: 'creditCard' | 'paypal' | 'crypto') {
    if (method === 'creditCard') {
      console.log(`Processing $${amount} via Credit Card...`);
      // Complex CC logic here
    } else if (method === 'paypal') {
      console.log(`Processing $${amount} via PayPal...`);
      // Complex PayPal logic here
    } else if (method === 'crypto') {
      console.log(`Processing $${amount} via Crypto...`);
      // Complex Crypto logic here
    }
  }
}

The "Clean" Way: Strategy Implementation

By defining a PaymentStrategy interface, the PaymentProcessor no longer needs to know how a payment is processed; it only needs to know that the strategy object has a pay method.

interface PaymentStrategy {
  pay(amount: number): void;
}

class CreditCardPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Paid ${amount} using Credit Card.`);
  }
}

class PayPalPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Paid ${amount} using PayPal.`);
  }
}

class PaymentContext {
  private strategy: PaymentStrategy;

  constructor(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }

  executePayment(amount: number) {
    this.strategy.pay(amount);
  }
}

// Usage
const context = new PaymentContext(new CreditCardPayment());
context.executePayment(100);

context.setStrategy(new PayPalPayment());
context.executePayment(200);

Why this is Superior

  1. Open/Closed Principle: You can add a BitcoinPayment class without touching the PaymentContext code.
  2. Testability: Each strategy can be unit-tested in isolation.
  3. Readability: The business logic is separated from the execution mechanism.

The Observer Pattern: Managing State Synchronization

The Observer pattern establishes a subscription mechanism to notify multiple objects about any events that happen to the object they are observing. This is the foundation of reactive programming and event-driven architectures.

The "Bad" Way: Tight Coupling

Tight coupling occurs when the subject (the object being watched) holds direct references to every object it needs to notify and calls their specific methods.

class NewsAgency {
  private subscribers: any[] = [];

  subscribe(user: any) {
    this.subscribers.push(user);
  }

  notify(news: string) {
    this.subscribers.forEach(sub => sub.updateNews(news)); 
    // This assumes every subscriber has an 'updateNews' method, 
    // which is fragile and prone to runtime errors.
  }
}

The "Clean" Way: Interface-Driven Observation

A professional implementation uses a dedicated Observer interface. This ensures that the Subject does not depend on concrete classes, but on an abstraction.

interface Observer {
  update(data: any): void;
}

interface Subject {
  attach(observer: Observer): void;
  detach(observer: Observer): void;
  notify(): void;
}

class NewsPublisher implements Subject {
  private observers: Observer[] = [];
  private latestNews: string = "";

  attach(observer: Observer): void {
    const isExist = this.observers.includes(observer);
    if (isExist) return console.log('Observer already attached.');
    this.observers.push(observer);
  }

  detach(observer: Observer): void {
    const observerIndex = this.observers.indexOf(observer);
    this.observers.splice(observerIndex, 1);
  }

  notify(): void {
    for (const observer of this.observers) {
      observer.update(this.latestNews);
    }
  }

  setNews(news: string): void {
    this.latestNews = news;
    this.notify();
  }
}

class NewsReader implements Observer {
  constructor(private name: string) {}

  update(data: any): void {
    console.log(`${this.name} received news: ${data}`);
  }
}

// Usage
const agency = new NewsPublisher();
const reader1 = new NewsReader("Alice");
const reader2 = new NewsReader("Bob");

agency.attach(reader1);
agency.attach(reader2);

agency.setNews("TypeScript 5.0 Released!"); 
// Output: Alice received news..., Bob received news...

Architectural Advantages

  1. Dynamic Relationships: Observers can be added or removed at runtime without restarting the application.
  2. Broadcast Communication: The subject does not need to know the identity or internal state of the observers.
  3. Scalability: This pattern is essential for building complex UIs where a single data change must update multiple components.

Comparison: Strategy vs. Observer

While both patterns involve composition, their intent is fundamentally different.

Feature Strategy Pattern Observer Pattern
Primary Intent To change how a task is performed. To notify others that a task happened.
Relationship 1:1 (Context to Strategy). 1:N (Subject to Observers).
Coupling Decouples the algorithm from the client. Decouples the event source from the listeners.
Trigger Explicitly called by the context. Automatically triggered by state change.

Integrating Patterns into a Modern Workflow

Implementing these patterns is a step toward best practices for writing clean code. However, the real-world application often requires combining them with other modern TypeScript features.

Using Generics for Type Safety

To avoid using any in the Observer pattern, use generics. This ensures that the data passed from the Subject matches the data expected by the Observer.

interface Observer<T> {
  update(data: T): void;
}

class Subject<T> {
  private observers: Observer<T>[] = [];

  attach(observer: Observer<T>) {
    this.observers.push(observer);
  }

  notify(data: T) {
    this.observers.forEach(obs => obs.update(data));
  }
}

When to avoid these patterns

Over-engineering is a common pitfall. If your application only has two static behaviors that will never change, a simple if/else is more readable than a Strategy pattern. Similarly, if you are working in a framework like React or Vue, the Observer pattern is often already implemented via State Management (Redux, Vuex, or Signals), and creating a custom implementation may introduce unnecessary complexity.

Debugging Pattern-Based Architectures

When moving from linear code to pattern-based code, debugging shifts from tracing a single line to tracing an interaction between objects. If you encounter unexpected behavior in these implementations, apply a systematic debugging framework to isolate whether the failure is in the Strategy implementation (the algorithm) or the Context (the execution).

Common issues include: - Memory Leaks: In the Observer pattern, failing to detach an observer when it is no longer needed can prevent garbage collection. - Circular Dependencies: Ensuring that the Subject and Observer do not depend on each other's concrete implementations.

Key Takeaways

By utilizing these architectural patterns, developers can transition from writing scripts to engineering robust software systems. CodeAmber provides the technical resources necessary to bridge this gap, offering deeper insights into how these patterns integrate with modern frameworks and high-performance environments.

Original resource: Visit the source site