Strategy Pattern | Deep Dive In Design Patterns

Narendra Singh Rathore
4 min readAug 15, 2024

--

The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm’s behavior at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions on which in a family of algorithms to use.

This pattern promotes the idea of composition over inheritance, allowing behavior to be encapsulated in separate classes.

Advantages

  1. Open/Closed Principle: New strategies can be added without modifying existing code.
  2. Reusability: Strategies can be reused across different contexts.
  3. Simplified Code Maintenance: Encapsulating algorithms makes the code easier to understand and maintain.
  4. Runtime Flexibility: Allows changing algorithms dynamically at runtime based on context.

Disadvantages

  1. Increased Number of Classes: Each strategy requires a separate class, which can lead to a proliferation of classes.
  2. Communication Overhead: Context must be aware of different strategies, which can increase complexity.
  3. Client Awareness: Clients need to understand the differences between strategies to select the appropriate one.

TypeScript Implementation

Let’s consider an example where we have different payment methods (Credit Card, PayPal, Bitcoin) for processing payments. We’ll use the Strategy Pattern to encapsulate each payment method.

// Strategy Interface

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

// Concrete Strategies

class CreditCardPayment implements PaymentStrategy {

constructor(private name: string,
private cardNumber: string,
private cvv: string,
private dateOfExpiry: string)
{}

pay(amount: number): void {
console.log(`${amount} paid with credit/debit card.`);
}
}

class PayPalPayment implements PaymentStrategy {

constructor(private email: string, private password: string) {}

pay(amount: number): void {
console.log(`${amount} paid using PayPal.`);
}
}

class BitcoinPayment implements PaymentStrategy {

constructor(private walletAddress: string) {}

pay(amount: number): void {
console.log(`${amount} paid using Bitcoin.`);
}
}

// Context
class ShoppingCart {
private items: { name: string; price: number }[] = [];
private paymentStrategy: PaymentStrategy;

addItem(item: { name: string; price: number }): void {
this.items.push(item);
}

calculateTotal(): number {
return this.items.reduce((acc, item) => acc + item.price, 0);
}

setPaymentStrategy(strategy: PaymentStrategy): void {
this.paymentStrategy = strategy;
}

checkout(): void {
const amount = this.calculateTotal();
this.paymentStrategy.pay(amount);
}
}

// Usage
const cart = new ShoppingCart();
cart.addItem({ name: 'Book', price: 10 });
cart.addItem({ name: 'Pen', price: 2 });

// Pay using PayPal
cart.setPaymentStrategy(
new PayPalPayment('user@example.com', 'securePassword')
);
cart.checkout(); // Output: 12 paid using PayPal.

// Pay using Credit Card
cart.setPaymentStrategy(
new CreditCardPayment('John Doe', '1234567890123456', '123', '12/23')
);
cart.checkout(); // Output: 12 paid with credit/debit card.

// Pay using Bitcoin
cart.setPaymentStrategy(
new BitcoinPayment('1BoatSLRHtKNngkdXEeobR76b53LETtpyT')
);
cart.checkout(); // Output: 12 paid using Bitcoin.

How the Strategy Pattern Improves Code

  1. Flexibility: The ShoppingCart class doesn't need to know the details of payment processing. It delegates that responsibility to the strategy objects.
  2. Extensibility: Adding a new payment method is straightforward. Simply implement the PaymentStrategy interface and pass it to the ShoppingCart.
  3. Maintenance: Each payment method’s logic is encapsulated in its own class, making it easier to manage and debug.
  4. Testing: Strategies can be tested in isolation, ensuring that each payment method works as expected without interference from other parts of the system.

By applying the Strategy Pattern, the code becomes more organized, maintainable, and scalable, adhering to solid design principles.

Let’s use the Strategy Pattern in a context where different sorting algorithms are used for sorting lists. This example will illustrate how the Strategy Pattern can improve code organization, flexibility, and maintainability.

Use Case: Sorting Different Data Types

Imagine we have a sorting utility that needs to handle different types of data, such as arrays of numbers and arrays of strings. We want to allow switching between different sorting algorithms like Bubble Sort, Quick Sort, and Merge Sort.

Scenario Without Strategy Pattern

Without the Strategy Pattern, you might have a single Sorter class with different methods for each sorting algorithm. This can lead to a bloated class with complex conditional logic.

class Sorter {
sortNumbers(numbers: number[], algorithm: string): void {
if (algorithm === 'bubble') {
this.bubbleSort(numbers);
} else if (algorithm === 'quick') {
this.quickSort(numbers);
} else if (algorithm === 'merge') {
this.mergeSort(numbers);
}
console.log(`Sorted numbers: ${numbers}`);
}

sortStrings(strings: string[], algorithm: string): void {
if (algorithm === 'bubble') {
this.bubbleSort(strings);
} else if (algorithm === 'quick') {
this.quickSort(strings);
} else if (algorithm === 'merge') {
this.mergeSort(strings);
}
console.log(`Sorted strings: ${strings}`);
}
private bubbleSort(arr: any[]): void {
// Bubble sort implementation
}
private quickSort(arr: any[]): void {
// Quick sort implementation
}
private mergeSort(arr: any[]): void {
// Merge sort implementation
}
}

Scenario With Strategy Pattern

Using the Strategy Pattern, we separate the sorting algorithms into distinct classes and delegate the sorting task to these strategy classes. This approach improves code organization and makes it easier to add or change algorithms.

Strategy Interface

// Strategy Interface
interface SortStrategy {
sort(arr: any[]): void;
}

Concrete Strategies

// Bubble Sort Strategy
class BubbleSort implements SortStrategy {
sort(arr: any[]): void {
console.log('Sorting using Bubble Sort.');
// Bubble sort implementation
}
}

// Quick Sort Strategy
class QuickSort implements SortStrategy {
sort(arr: any[]): void {
console.log('Sorting using Quick Sort.');
// Quick sort implementation
}
}

// Merge Sort Strategy
class MergeSort implements SortStrategy {
sort(arr: any[]): void {
console.log('Sorting using Merge Sort.');
// Merge sort implementation
}
}

Context

// Context
class Sorter {
private sortStrategy: SortStrategy;

setSortStrategy(strategy: SortStrategy): void {
this.sortStrategy = strategy;
}
sort(arr: any[]): void {
this.sortStrategy.sort(arr);
console.log(`Sorted array: ${arr}`);
}
}

Usage

// Example usage
const numberArray = [5, 3, 8, 1, 2];
const stringArray = ['apple', 'banana', 'cherry'];

// Create a Sorter instance
const sorter = new Sorter();
// Use Bubble Sort for numbers
sorter.setSortStrategy(new BubbleSort());
sorter.sort(numberArray);
// Output: Sorting using Bubble Sort.
// Sorted array: 5,3,8,1,2
// Use Quick Sort for strings
sorter.setSortStrategy(new QuickSort());
sorter.sort(stringArray);
// Output: Sorting using Quick Sort.
// Sorted array: apple,banana,cherry

How the Strategy Pattern Improves Code

  1. Separation of Concerns: Each sorting algorithm is encapsulated in its own class, making it easier to manage and test.
  2. Flexibility: The Sorter class can switch between different sorting strategies at runtime without any modification to the sorting logic.
  3. Code Organization: The strategy pattern helps keep the Sorter class clean and focused on coordinating sorting operations rather than implementing specific algorithms.
  4. Extensibility: Adding a new sorting algorithm involves creating a new strategy class that implements the SortStrategy interface, without changing existing code.
  5. Maintainability: The sorting algorithms are isolated, so updates or bug fixes in one algorithm do not affect the others.

By applying the Strategy Pattern, the sorting logic becomes modular and easily extendable, leading to cleaner and more maintainable code.

Thanks for reading.

--

--

No responses yet