Strategy Pattern Implementation in TypeScript
The Strategy pattern is a behavioral design pattern that enables selecting an algorithm at runtime. This challenge focuses on implementing this pattern in TypeScript, allowing for interchangeable algorithms within a context. This is useful for creating flexible and extensible systems where different behaviors can be swapped out without modifying the core logic.
Problem Description
Your task is to implement the Strategy pattern in TypeScript. You will create a system that can perform different operations based on a chosen strategy.
What needs to be achieved:
- Define an interface for the strategy.
- Implement concrete strategy classes that adhere to the interface.
- Create a context class that uses a strategy object.
- Allow the context to switch between different strategies at runtime.
Key requirements:
- Strategy Interface: Define a TypeScript interface that declares a common method for all strategies. This method will encapsulate the behavior.
- Concrete Strategies: Create at least two concrete classes that implement the strategy interface, each providing a different implementation of the common behavior.
- Context Class: Create a class that holds a reference to a strategy object. This context class should have a method that delegates the work to the current strategy. It should also have a method to change the strategy.
- Runtime Switching: Demonstrate that the context class can change its strategy dynamically.
Expected behavior: When the context object's method is called, it should execute the logic defined by its currently set strategy. Changing the strategy should result in subsequent calls to the context's method executing the new strategy's logic.
Important edge cases to consider:
- What happens if the context is initialized without a strategy? (For this challenge, assume a strategy will always be provided upon instantiation or set immediately after).
Examples
Example 1: Text Formatting
Imagine a system that needs to format text in different ways (e.g., uppercase, lowercase, title case).
// Strategy Interface
interface TextFormatter {
format(text: string): string;
}
// Concrete Strategy 1
class UppercaseFormatter implements TextFormatter {
format(text: string): string {
return text.toUpperCase();
}
}
// Concrete Strategy 2
class LowercaseFormatter implements TextFormatter {
format(text: string): string {
return text.toLowerCase();
}
}
// Context Class
class TextProcessor {
private formatter: TextFormatter;
constructor(formatter: TextFormatter) {
this.formatter = formatter;
}
setFormatter(formatter: TextFormatter): void {
this.formatter = formatter;
}
process(text: string): string {
return this.formatter.format(text);
}
}
// Usage:
const processor = new TextProcessor(new UppercaseFormatter());
console.log(processor.process("Hello World")); // Expected Output: HELLO WORLD
processor.setFormatter(new LowercaseFormatter());
console.log(processor.process("Hello World")); // Expected Output: hello world
Example 2: Payment Processing
Consider a scenario where an e-commerce application needs to handle payments using different methods (e.g., credit card, PayPal).
// Strategy Interface
interface PaymentStrategy {
pay(amount: number): string;
}
// Concrete Strategy 1
class CreditCardPayment implements PaymentStrategy {
pay(amount: number): string {
return `Paid ${amount} using Credit Card.`;
}
}
// Concrete Strategy 2
class PayPalPayment implements PaymentStrategy {
pay(amount: number): string {
return `Paid ${amount} using PayPal.`;
}
}
// Context Class
class ShoppingCart {
private paymentStrategy: PaymentStrategy;
constructor(paymentStrategy: PaymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
setPaymentStrategy(paymentStrategy: PaymentStrategy): void {
this.paymentStrategy = paymentStrategy;
}
checkout(amount: number): string {
return this.paymentStrategy.pay(amount);
}
}
// Usage:
const cart = new ShoppingCart(new CreditCardPayment());
console.log(cart.checkout(100)); // Expected Output: Paid 100 using Credit Card.
cart.setPaymentStrategy(new PayPalPayment());
console.log(cart.checkout(150)); // Expected Output: Paid 150 using PayPal.
Constraints
- Your solution must be written entirely in TypeScript.
- The solution should demonstrate the core principles of the Strategy pattern.
- The code should be well-organized and readable.
- No external libraries are allowed for the pattern implementation itself.
Notes
- Think about how the strategy interface and concrete implementations interact with the context class.
- Consider how you would add new strategies in the future without altering existing strategy or context code (Open/Closed Principle).
- The examples provided show how to instantiate strategies and pass them to the context, and how to change them later. You should aim for similar flexibility.