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
- Open/Closed Principle: You can introduce new strategies without modifying the existing Context class.
- Elimination of Conditionals: It replaces complex
switchorif-elseblocks with polymorphic calls. - Testability: Each strategy can be unit-tested in isolation.
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
- Loose Coupling: The subject does not need to know the internal logic of the observers.
- Dynamic Relationships: Observers can be added or removed at runtime.
- Broadcast Communication: It allows a single event to trigger multiple disparate actions across a system.
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
- Strategy Pattern: Use it to swap algorithms at runtime. It relies on a common interface to ensure that the Context class remains agnostic of the specific implementation.
- Observer Pattern: Use it to create a subscription model. It allows a Subject to notify multiple Observers of state changes without tight coupling.
- TypeScript Advantage: Interfaces and access modifiers (
private,public) make these patterns more robust by preventing unauthorized access to the observer list or the strategy instance. - Maintainability: Both patterns adhere to the Open/Closed Principle, allowing for system expansion without modifying existing, tested code.
- Practical Application: CodeAmber recommends using these patterns to replace deep nesting of conditional logic and to decouple event-driven components in complex TypeScript applications.