Astrological Guide to Parenting · CodeAmber

Implementing Strategy and Observer Design Patterns in TypeScript

Implementing the Strategy and Observer design patterns in TypeScript requires the use of interfaces to decouple high-level logic from specific implementations. The Strategy pattern encapsulates interchangeable algorithms within separate classes, while the Observer pattern establishes a one-to-many dependency where multiple objects are notified automatically of state changes in a subject.

Implementing Strategy and Observer Design Patterns in TypeScript

Design patterns are standardized solutions to common software engineering problems. In TypeScript, the strong typing system allows developers to implement these patterns with high precision, ensuring that interchangeable components adhere to strict contracts. By utilizing these patterns, developers can avoid monolithic conditional blocks and tight coupling, which are essential best practices for writing clean, maintainable code.

The Strategy Design Pattern

The Strategy pattern is a behavioral design pattern that enables selecting an algorithm at runtime. Instead of implementing a single class with multiple conditional statements (if/else or switch) to handle different behaviors, the Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

When to Use the Strategy Pattern

The Strategy pattern is the optimal choice when: * A class has multiple versions of a specific behavior. * You need to switch between different algorithms based on user input or environmental configuration. * You want to isolate the business logic of an algorithm from the class that uses it.

Technical Implementation in TypeScript

To implement the Strategy pattern, you must define a common interface that all concrete strategies will implement. The "Context" class then maintains a reference to this interface and delegates the work to the currently active strategy.

1. Define the Strategy Interface

The interface ensures that every strategy provides the same method signature.

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

2. Create Concrete Strategies

Each class implements the interface with a specific logic.

class CreditCardPayment implements PaymentStrategy {
    processPayment(amount: number): void {
        console.log(`Processing credit card payment of $${amount}`);
    }
}

class PayPalPayment implements PaymentStrategy {
    processPayment(amount: number): void {
        console.log(`Processing PayPal payment of $${amount}`);
    }
}

class BitcoinPayment implements PaymentStrategy {
    processPayment(amount: number): void {
        console.log(`Processing Bitcoin payment of $${amount}`);
    }
}

3. Implement the Context Class

The Context class does not know the details of the strategies; it only knows they follow the PaymentStrategy interface.

class ShoppingCart {
    private strategy: PaymentStrategy;

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

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

    checkout(amount: number) {
        this.strategy.processPayment(amount);
    }
}

// Usage
const cart = new ShoppingCart(new CreditCardPayment());
cart.checkout(100); // Output: Processing credit card payment of $100

cart.setStrategy(new PayPalPayment());
cart.checkout(200); // Output: Processing PayPal payment of $200

Benefits of the Strategy Pattern


The Observer Design Pattern

The Observer pattern defines a subscription mechanism to notify multiple objects (observers) about any events that happen to the object they are observing (the subject). This is the foundation of event-driven programming and is heavily utilized in modern frontend frameworks and state management libraries.

When to Use the Observer Pattern

The Observer pattern is necessary when: * A change to one object requires changing others, and the number of objects needing change is unknown or dynamic. * You are building a system where components need to stay in sync without being tightly coupled. * You are implementing a notification system, such as a UI updating when underlying data changes.

Technical Implementation in TypeScript

The implementation involves two primary roles: the Subject (the source of truth) and the Observer (the listener).

1. Define the Observer Interface

The observer must have an update method that the subject can call.

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

2. Implement the Subject

The subject manages the list of observers and provides methods to attach, detach, and notify them.

class NewsAgency {
    private observers: Observer[] = [];

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

    public detach(observer: Observer): void {
        const observerIndex = this.observers.indexOf(observer);
        if (observerIndex === -1) {
            return console.log('Subject: Nonexistent observer.');
        }
        this.observers.splice(observerIndex, 1);
    }

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

    public addNews(news: string): void {
        console.log(`NewsAgency: I've got a new story: ${news}`);
        this.notify(news);
    }
}

3. Create Concrete Observers

These classes define how they react to the notification.

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

    update(data: string): void {
        console.log(`${this.name} is broadcasting: ${data}`);
    }
}

class EmailSubscriber implements Observer {
    update(data: string): void {
        console.log(`Email sent with news: ${data}`);
    }
}

4. Execution

const agency = new NewsAgency();

const bbc = new NewsChannel("BBC");
const cnn = new NewsChannel("CNN");
const user = new EmailSubscriber();

agency.attach(bbc);
agency.attach(cnn);
agency.attach(user);

agency.addNews("TypeScript 5.0 Released!"); 
// All three observers will log the news.

agency.detach(cnn);
agency.addNews("Design Patterns are Essential!"); 
// Only BBC and EmailSubscriber will log the news.

Benefits of the Observer Pattern


Comparing Strategy vs. Observer

While both patterns rely on interfaces to achieve polymorphism, their intent is fundamentally different.

Feature Strategy Pattern Observer Pattern
Intent Change how a task is performed. Notify who needs to know about a change.
Relationship 1:1 (Context to Strategy). 1:N (Subject to many Observers).
Timing Explicitly called by the Context. Automatically triggered by the Subject.
Goal Encapsulation of algorithms. State synchronization and event handling.

Integrating Patterns into Scalable Architecture

In professional software development, these patterns are rarely used in isolation. They are often combined to build a scalable web application where the Observer pattern handles inter-service communication and the Strategy pattern handles varying business rules within a single service.

For instance, a notification service might use the Observer pattern to listen for "Order Completed" events from a database. Once notified, it might use the Strategy pattern to determine whether to send that notification via SMS, Email, or Push Notification based on the user's stored preferences.

Performance Considerations

While design patterns improve maintainability, they can introduce slight overhead due to increased object allocation and indirect function calls. To ensure these patterns do not degrade your system, it is important to understand how to optimize software performance by monitoring memory usage and avoiding unnecessary object creation in high-frequency loops.

Key Takeaways

Original resource: Visit the source site