Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Strategy Pattern! ๐ In this guide, weโll explore how this powerful design pattern can make your code more flexible and maintainable by allowing you to swap algorithms at runtime.
Youโll discover how the Strategy Pattern can transform your TypeScript development experience. Whether youโre building payment systems ๐ณ, sorting algorithms ๐, or game AI ๐ฎ, understanding this pattern is essential for writing robust, extensible code.
By the end of this tutorial, youโll feel confident using the Strategy Pattern in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Strategy Pattern
๐ค What is the Strategy Pattern?
The Strategy Pattern is like having a Swiss Army knife ๐ง where you can swap out different tools based on what you need. Think of it as a restaurant menu ๐ฝ๏ธ where you can choose different cooking methods (grilled, fried, steamed) for the same dish!
In TypeScript terms, the Strategy Pattern lets you define a family of algorithms, encapsulate each one, and make them interchangeable. This means you can:
- โจ Switch algorithms at runtime
- ๐ Add new strategies without changing existing code
- ๐ก๏ธ Keep your code clean and maintainable
๐ก Why Use the Strategy Pattern?
Hereโs why developers love the Strategy Pattern:
- Flexibility ๐: Change behavior without modifying client code
- Open/Closed Principle ๐: Open for extension, closed for modification
- Testability ๐งช: Each strategy can be tested independently
- Code Reusability โป๏ธ: Strategies can be shared across different contexts
Real-world example: Imagine building an e-commerce platform ๐. With the Strategy Pattern, you can easily switch between different shipping methods, payment processors, or discount calculations!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ Hello, Strategy Pattern!
interface PaymentStrategy {
pay(amount: number): void;
getProcessingFee(): number;
}
// ๐ณ Credit card payment strategy
class CreditCardPayment implements PaymentStrategy {
constructor(private cardNumber: string) {}
pay(amount: number): void {
console.log(`๐ณ Paid $${amount} using credit card ending in ${this.cardNumber.slice(-4)}`);
}
getProcessingFee(): number {
return 2.5; // 2.5% fee
}
}
// ๐ฑ PayPal payment strategy
class PayPalPayment implements PaymentStrategy {
constructor(private email: string) {}
pay(amount: number): void {
console.log(`๐ฑ Paid $${amount} using PayPal account ${this.email}`);
}
getProcessingFee(): number {
return 3.0; // 3% fee
}
}
// ๐ Shopping cart that uses strategies
class ShoppingCart {
private paymentStrategy: PaymentStrategy | null = null;
setPaymentStrategy(strategy: PaymentStrategy): void {
this.paymentStrategy = strategy;
console.log("โ
Payment method selected!");
}
checkout(amount: number): void {
if (!this.paymentStrategy) {
console.log("โ Please select a payment method first!");
return;
}
const fee = this.paymentStrategy.getProcessingFee();
const total = amount + (amount * fee / 100);
console.log(`๐ฐ Subtotal: $${amount}, Fee: ${fee}%, Total: $${total.toFixed(2)}`);
this.paymentStrategy.pay(total);
}
}
๐ก Explanation: Notice how we can swap payment methods without changing the ShoppingCart class! The ?
makes properties optional for flexibility.
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Strategy with context
interface SortStrategy<T> {
sort(data: T[]): T[];
getName(): string;
}
class QuickSort<T> implements SortStrategy<T> {
sort(data: T[]): T[] {
// ๐ Quick sort implementation
return [...data].sort();
}
getName(): string {
return "Quick Sort โก";
}
}
// ๐จ Pattern 2: Strategy factory
class StrategyFactory {
private strategies = new Map<string, PaymentStrategy>();
registerStrategy(name: string, strategy: PaymentStrategy): void {
this.strategies.set(name, strategy);
console.log(`โ
Registered ${name} strategy`);
}
getStrategy(name: string): PaymentStrategy | undefined {
return this.strategies.get(name);
}
}
// ๐ Pattern 3: Strategy with state
interface CompressionStrategy {
compress(data: string): string;
decompress(data: string): string;
getCompressionRatio(): number;
}
๐ก Practical Examples
๐ Example 1: Dynamic Pricing Calculator
Letโs build something real:
// ๐ท๏ธ Define our pricing strategy interface
interface PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number;
getDescription(): string;
}
// ๐ฏ Regular pricing
class RegularPricing implements PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity;
}
getDescription(): string {
return "Regular pricing ๐ท๏ธ";
}
}
// ๐ Bulk discount pricing
class BulkDiscountPricing implements PricingStrategy {
constructor(
private minQuantity: number,
private discountPercentage: number
) {}
calculatePrice(basePrice: number, quantity: number): number {
if (quantity >= this.minQuantity) {
const discount = basePrice * quantity * (this.discountPercentage / 100);
return basePrice * quantity - discount;
}
return basePrice * quantity;
}
getDescription(): string {
return `Bulk discount: ${this.discountPercentage}% off for ${this.minQuantity}+ items ๐`;
}
}
// ๐ VIP member pricing
class VIPPricing implements PricingStrategy {
private vipDiscount = 20; // 20% off for VIPs
calculatePrice(basePrice: number, quantity: number): number {
const subtotal = basePrice * quantity;
return subtotal * (1 - this.vipDiscount / 100);
}
getDescription(): string {
return `VIP pricing: ${this.vipDiscount}% off everything! ๐`;
}
}
// ๐๏ธ Product with dynamic pricing
class Product {
constructor(
public name: string,
public basePrice: number,
public emoji: string,
private pricingStrategy: PricingStrategy = new RegularPricing()
) {}
// ๐ Change pricing strategy
setPricingStrategy(strategy: PricingStrategy): void {
this.pricingStrategy = strategy;
console.log(`${this.emoji} ${this.name} now uses: ${strategy.getDescription()}`);
}
// ๐ฐ Calculate final price
getPrice(quantity: number): number {
return this.pricingStrategy.calculatePrice(this.basePrice, quantity);
}
// ๐ Show price breakdown
showPricing(quantity: number): void {
const price = this.getPrice(quantity);
console.log(`${this.emoji} ${this.name} x${quantity}: $${price.toFixed(2)}`);
console.log(` Strategy: ${this.pricingStrategy.getDescription()}`);
}
}
// ๐ฎ Let's use it!
const laptop = new Product("Gaming Laptop", 999.99, "๐ป");
laptop.showPricing(1); // Regular price
// Apply bulk discount
laptop.setPricingStrategy(new BulkDiscountPricing(3, 15));
laptop.showPricing(5); // Bulk discount applied!
// VIP customer arrives
laptop.setPricingStrategy(new VIPPricing());
laptop.showPricing(1); // VIP discount!
๐ฏ Try it yourself: Add a SeasonalPricing
strategy that applies different discounts based on the current season!
๐ฎ Example 2: Game AI Movement Strategies
Letโs make it fun:
// ๐ Movement strategy interface
interface MovementStrategy {
move(currentPosition: { x: number; y: number }): { x: number; y: number };
getMovementType(): string;
getSpeedMultiplier(): number;
}
// ๐ Cautious movement
class CautiousMovement implements MovementStrategy {
move(currentPosition: { x: number; y: number }): { x: number; y: number } {
// Move slowly and carefully
const moveDistance = 1;
return {
x: currentPosition.x + (Math.random() > 0.5 ? moveDistance : -moveDistance),
y: currentPosition.y + (Math.random() > 0.5 ? moveDistance : -moveDistance)
};
}
getMovementType(): string {
return "Cautious ๐";
}
getSpeedMultiplier(): number {
return 0.5;
}
}
// ๐โโ๏ธ Aggressive movement
class AggressiveMovement implements MovementStrategy {
constructor(private targetPosition: { x: number; y: number }) {}
move(currentPosition: { x: number; y: number }): { x: number; y: number } {
// Move directly toward target
const dx = this.targetPosition.x - currentPosition.x;
const dy = this.targetPosition.y - currentPosition.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
const moveDistance = 3;
return {
x: currentPosition.x + (dx / distance) * moveDistance,
y: currentPosition.y + (dy / distance) * moveDistance
};
}
return currentPosition;
}
getMovementType(): string {
return "Aggressive ๐โโ๏ธ";
}
getSpeedMultiplier(): number {
return 1.5;
}
}
// ๐ Random movement
class RandomMovement implements MovementStrategy {
move(currentPosition: { x: number; y: number }): { x: number; y: number } {
const angle = Math.random() * Math.PI * 2;
const distance = 2;
return {
x: currentPosition.x + Math.cos(angle) * distance,
y: currentPosition.y + Math.sin(angle) * distance
};
}
getMovementType(): string {
return "Random ๐";
}
getSpeedMultiplier(): number {
return 1.0;
}
}
// ๐ฎ Game character
class GameCharacter {
private position = { x: 0, y: 0 };
private movementStrategy: MovementStrategy;
constructor(
public name: string,
public emoji: string,
strategy: MovementStrategy = new CautiousMovement()
) {
this.movementStrategy = strategy;
}
// ๐ Change movement behavior
setMovementStrategy(strategy: MovementStrategy): void {
this.movementStrategy = strategy;
console.log(`${this.emoji} ${this.name} switched to ${strategy.getMovementType()} movement!`);
}
// ๐ Move the character
move(): void {
const oldPos = { ...this.position };
this.position = this.movementStrategy.move(this.position);
const speed = this.movementStrategy.getSpeedMultiplier();
console.log(`${this.emoji} moved from (${oldPos.x.toFixed(1)}, ${oldPos.y.toFixed(1)}) to (${this.position.x.toFixed(1)}, ${this.position.y.toFixed(1)}) [Speed: ${speed}x]`);
}
// ๐ Get current position
getPosition(): { x: number; y: number } {
return { ...this.position };
}
}
// ๐ฎ Let's play!
const player = new GameCharacter("Hero", "๐ฆธ");
const enemy = new GameCharacter("Monster", "๐พ");
// Enemy sees player and becomes aggressive!
enemy.setMovementStrategy(new AggressiveMovement(player.getPosition()));
enemy.move();
// Player gets scared and moves randomly
player.setMovementStrategy(new RandomMovement());
player.move();
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Strategy with Type Parameters
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Generic strategy interface
interface DataProcessor<TInput, TOutput> {
process(data: TInput): TOutput;
validate(data: TInput): boolean;
getName(): string;
}
// ๐ช JSON processor
class JSONProcessor implements DataProcessor<string, object> {
process(data: string): object {
try {
return JSON.parse(data);
} catch {
return { error: "Invalid JSON โ" };
}
}
validate(data: string): boolean {
try {
JSON.parse(data);
return true;
} catch {
return false;
}
}
getName(): string {
return "JSON Processor ๐";
}
}
// โจ CSV processor
class CSVProcessor implements DataProcessor<string, string[][]> {
constructor(private delimiter: string = ",") {}
process(data: string): string[][] {
return data.split("\n").map(row => row.split(this.delimiter));
}
validate(data: string): boolean {
return data.includes(this.delimiter);
}
getName(): string {
return `CSV Processor (${this.delimiter}) ๐`;
}
}
// ๐ Strategy context with generics
class DataPipeline<TInput, TOutput> {
constructor(private processor: DataProcessor<TInput, TOutput>) {}
setProcessor(processor: DataProcessor<TInput, TOutput>): void {
this.processor = processor;
console.log(`โจ Switched to ${processor.getName()}`);
}
execute(data: TInput): TOutput | null {
if (!this.processor.validate(data)) {
console.log("โ Validation failed!");
return null;
}
console.log(`๐ Processing with ${this.processor.getName()}`);
return this.processor.process(data);
}
}
๐๏ธ Advanced Topic 2: Composite Strategies
For the brave developers:
// ๐ Composable strategies
interface ValidationStrategy {
validate(value: string): boolean;
getErrorMessage(): string;
}
// ๐ง Email validation
class EmailValidation implements ValidationStrategy {
validate(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
getErrorMessage(): string {
return "Invalid email address ๐ง";
}
}
// ๐ข Length validation
class LengthValidation implements ValidationStrategy {
constructor(private minLength: number, private maxLength: number) {}
validate(value: string): boolean {
return value.length >= this.minLength && value.length <= this.maxLength;
}
getErrorMessage(): string {
return `Length must be between ${this.minLength} and ${this.maxLength} ๐`;
}
}
// ๐ฏ Composite validator
class CompositeValidator implements ValidationStrategy {
private strategies: ValidationStrategy[] = [];
addStrategy(strategy: ValidationStrategy): void {
this.strategies.push(strategy);
}
validate(value: string): boolean {
return this.strategies.every(strategy => strategy.validate(value));
}
getErrorMessage(): string {
const errors = this.strategies
.filter(strategy => !strategy.validate(""))
.map(strategy => strategy.getErrorMessage());
return errors.join(", ");
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Forgetting to Set Strategy
// โ Wrong way - no default strategy!
class PaymentProcessor {
private strategy: PaymentStrategy | undefined;
process(amount: number): void {
this.strategy!.pay(amount); // ๐ฅ Runtime error if strategy not set!
}
}
// โ
Correct way - always have a default!
class PaymentProcessor {
private strategy: PaymentStrategy;
constructor(defaultStrategy: PaymentStrategy = new CreditCardPayment("default")) {
this.strategy = defaultStrategy;
}
process(amount: number): void {
this.strategy.pay(amount); // โ
Always safe!
}
}
๐คฏ Pitfall 2: Strategy State Management
// โ Dangerous - shared mutable state!
class BadStrategy {
private count = 0; // Shared state is dangerous!
execute(): void {
this.count++; // ๐ฅ Multiple contexts will conflict!
}
}
// โ
Safe - stateless or context-specific state!
class GoodStrategy {
execute(context: { count: number }): void {
context.count++; // โ
State belongs to context!
}
}
๐ ๏ธ Best Practices
- ๐ฏ Keep Strategies Focused: Each strategy should do one thing well
- ๐ Use Descriptive Names:
QuickSortStrategy
notQS
- ๐ก๏ธ Make Strategies Stateless: Avoid internal state when possible
- ๐จ Provide Factory Methods: Make strategy creation easy
- โจ Document Strategy Behavior: Clear docs for each algorithm
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Notification System
Create a flexible notification system:
๐ Requirements:
- โ Multiple notification channels (Email, SMS, Push)
- ๐ท๏ธ Priority levels affecting delivery method
- ๐ค User preferences for notification types
- ๐ Time-based strategy selection (quiet hours)
- ๐จ Each notification needs an emoji!
๐ Bonus Points:
- Add retry logic for failed notifications
- Implement notification batching
- Create a notification history tracker
๐ก Solution
๐ Click to see solution
// ๐ฏ Our notification strategy system!
interface NotificationStrategy {
send(message: string, recipient: string): Promise<boolean>;
getChannel(): string;
getPriority(): number;
}
// ๐ง Email notification
class EmailNotification implements NotificationStrategy {
async send(message: string, recipient: string): Promise<boolean> {
console.log(`๐ง Sending email to ${recipient}: ${message}`);
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 1000));
return true;
}
getChannel(): string {
return "Email ๐ง";
}
getPriority(): number {
return 1; // Lowest priority
}
}
// ๐ฑ SMS notification
class SMSNotification implements NotificationStrategy {
async send(message: string, recipient: string): Promise<boolean> {
console.log(`๐ฑ Sending SMS to ${recipient}: ${message}`);
await new Promise(resolve => setTimeout(resolve, 500));
return true;
}
getChannel(): string {
return "SMS ๐ฑ";
}
getPriority(): number {
return 2; // Medium priority
}
}
// ๐ Push notification
class PushNotification implements NotificationStrategy {
async send(message: string, recipient: string): Promise<boolean> {
console.log(`๐ Sending push notification to ${recipient}: ${message}`);
await new Promise(resolve => setTimeout(resolve, 100));
return true;
}
getChannel(): string {
return "Push ๐";
}
getPriority(): number {
return 3; // Highest priority
}
}
// ๐ฏ Notification manager
class NotificationManager {
private strategies = new Map<string, NotificationStrategy>();
private history: Array<{
timestamp: Date;
channel: string;
recipient: string;
success: boolean;
}> = [];
constructor() {
// Register default strategies
this.registerStrategy("email", new EmailNotification());
this.registerStrategy("sms", new SMSNotification());
this.registerStrategy("push", new PushNotification());
}
// ๐ Register a strategy
registerStrategy(name: string, strategy: NotificationStrategy): void {
this.strategies.set(name, strategy);
console.log(`โ
Registered ${strategy.getChannel()} strategy`);
}
// ๐ฏ Smart strategy selection
private selectStrategy(priority: "low" | "medium" | "high"): NotificationStrategy {
const hour = new Date().getHours();
// ๐ Quiet hours (10 PM - 8 AM): only high priority
if (hour >= 22 || hour < 8) {
if (priority !== "high") {
return this.strategies.get("email")!;
}
}
// Select based on priority
switch (priority) {
case "high":
return this.strategies.get("push")!;
case "medium":
return this.strategies.get("sms")!;
default:
return this.strategies.get("email")!;
}
}
// ๐ค Send notification
async notify(
message: string,
recipient: string,
priority: "low" | "medium" | "high" = "medium"
): Promise<void> {
const strategy = this.selectStrategy(priority);
console.log(`๐ฏ Selected ${strategy.getChannel()} for ${priority} priority`);
try {
const success = await strategy.send(message, recipient);
this.history.push({
timestamp: new Date(),
channel: strategy.getChannel(),
recipient,
success
});
if (success) {
console.log(`โ
Notification sent successfully!`);
}
} catch (error) {
console.log(`โ Failed to send notification`);
// Fallback to email
if (strategy.getChannel() !== "Email ๐ง") {
await this.strategies.get("email")!.send(message, recipient);
}
}
}
// ๐ Get statistics
getStats(): void {
console.log("๐ Notification Statistics:");
const channelCounts = new Map<string, number>();
this.history.forEach(entry => {
const count = channelCounts.get(entry.channel) || 0;
channelCounts.set(entry.channel, count + 1);
});
channelCounts.forEach((count, channel) => {
console.log(` ${channel}: ${count} sent`);
});
}
}
// ๐ฎ Test it out!
const notifier = new NotificationManager();
async function testNotifications() {
await notifier.notify("Welcome! ๐", "[email protected]", "low");
await notifier.notify("Payment received ๐ฐ", "[email protected]", "medium");
await notifier.notify("Security alert! ๐จ", "[email protected]", "high");
notifier.getStats();
}
testNotifications();
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create flexible algorithms with the Strategy Pattern ๐ช
- โ Swap behaviors at runtime without changing client code ๐ก๏ธ
- โ Apply SOLID principles in real projects ๐ฏ
- โ Debug strategy-related issues like a pro ๐
- โ Build extensible systems with TypeScript! ๐
Remember: The Strategy Pattern is about giving your code choices. Itโs here to make your applications more flexible and maintainable! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the Strategy Pattern!
Hereโs what to do next:
- ๐ป Practice with the notification system exercise
- ๐๏ธ Refactor existing code to use strategies where appropriate
- ๐ Move on to our next tutorial: Template Method Pattern
- ๐ Share your strategy implementations with others!
Remember: Every design pattern expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ