Strategy Pattern: Flexible Algorithm Selection in Java

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it, enabling runtime selection of different algorithms.


What is the Strategy Pattern?

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 about which algorithm to use.

Key Benefits:

  • Eliminates conditional statements - No complex if-else or switch cases
  • Open/Closed Principle - Easy to add new strategies without modifying existing code
  • Runtime flexibility - Algorithms can be swapped at runtime
  • Clean separation - Algorithm implementation separated from usage context
  • Testability - Each strategy can be tested independently

Structure

Context ──────> Strategy Interface
↑                ↑
|                |
Strategy Field   Concrete Strategies
- StrategyA
- StrategyB  
- StrategyC

Implementation Examples

Example 1: Payment Processing System

import java.util.*;
// Strategy interface
interface PaymentStrategy {
boolean processPayment(double amount);
String getPaymentMethod();
boolean supportsRefund();
boolean refund(double amount);
}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
private final String expiryDate;
private final String cvv;
private final String cardholderName;
public CreditCardPayment(String cardNumber, String expiryDate, String cvv, String cardholderName) {
this.cardNumber = cardNumber;
this.expiryDate = expiryDate;
this.cvv = cvv;
this.cardholderName = cardholderName;
}
@Override
public boolean processPayment(double amount) {
System.out.printf("Processing credit card payment of $%.2f%n", amount);
System.out.printf("Card: %s****, Expiry: %s%n", 
cardNumber.substring(0, 4), expiryDate);
// Simulate payment processing
boolean success = Math.random() > 0.1; // 90% success rate
if (success) {
System.out.println("✓ Credit card payment successful");
} else {
System.out.println("✗ Credit card payment failed");
}
return success;
}
@Override
public String getPaymentMethod() {
return "Credit Card";
}
@Override
public boolean supportsRefund() {
return true;
}
@Override
public boolean refund(double amount) {
System.out.printf("Refunding $%.2f to credit card %s****%n", 
amount, cardNumber.substring(0, 4));
return true;
}
}
class PayPalPayment implements PaymentStrategy {
private final String email;
private final String password;
public PayPalPayment(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public boolean processPayment(double amount) {
System.out.printf("Processing PayPal payment of $%.2f%n", amount);
System.out.printf("Account: %s%n", email);
// Simulate PayPal API call
boolean success = Math.random() > 0.05; // 95% success rate
if (success) {
System.out.println("✓ PayPal payment successful");
} else {
System.out.println("✗ PayPal payment failed");
}
return success;
}
@Override
public String getPaymentMethod() {
return "PayPal";
}
@Override
public boolean supportsRefund() {
return true;
}
@Override
public boolean refund(double amount) {
System.out.printf("Refunding $%.2f to PayPal account %s%n", amount, email);
return true;
}
}
class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
private final String cryptocurrency;
public CryptoPayment(String walletAddress, String cryptocurrency) {
this.walletAddress = walletAddress;
this.cryptocurrency = cryptocurrency;
}
@Override
public boolean processPayment(double amount) {
System.out.printf("Processing %s payment of $%.2f%n", cryptocurrency, amount);
System.out.printf("Wallet: %s...%n", walletAddress.substring(0, 8));
// Simulate blockchain transaction
boolean success = Math.random() > 0.2; // 80% success rate
if (success) {
System.out.printf("✓ %s payment confirmed%n", cryptocurrency);
} else {
System.out.printf("✗ %s payment failed - network congestion%n", cryptocurrency);
}
return success;
}
@Override
public String getPaymentMethod() {
return cryptocurrency;
}
@Override
public boolean supportsRefund() {
return false; // Crypto payments are typically non-refundable
}
@Override
public boolean refund(double amount) {
System.out.println("✗ Refunds not supported for cryptocurrency payments");
return false;
}
}
class BankTransferPayment implements PaymentStrategy {
private final String accountNumber;
private final String routingNumber;
private final String accountHolder;
public BankTransferPayment(String accountNumber, String routingNumber, String accountHolder) {
this.accountNumber = accountNumber;
this.routingNumber = routingNumber;
this.accountHolder = accountHolder;
}
@Override
public boolean processPayment(double amount) {
System.out.printf("Processing bank transfer of $%.2f%n", amount);
System.out.printf("Account: ***%s, Holder: %s%n", 
accountNumber.substring(accountNumber.length() - 4), accountHolder);
// Simulate bank transfer
boolean success = Math.random() > 0.15; // 85% success rate
if (success) {
System.out.println("✓ Bank transfer initiated successfully");
} else {
System.out.println("✗ Bank transfer failed");
}
return success;
}
@Override
public String getPaymentMethod() {
return "Bank Transfer";
}
@Override
public boolean supportsRefund() {
return true;
}
@Override
public boolean refund(double amount) {
System.out.printf("Refunding $%.2f via bank transfer%n", amount);
return true;
}
}
// Context class
class PaymentProcessor {
private PaymentStrategy paymentStrategy;
private final List<PaymentRecord> paymentHistory;
public PaymentProcessor() {
this.paymentHistory = new ArrayList<>();
}
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
System.out.printf("Payment method changed to: %s%n", paymentStrategy.getPaymentMethod());
this.paymentStrategy = paymentStrategy;
}
public boolean processPayment(double amount, String description) {
if (paymentStrategy == null) {
System.out.println("✗ No payment method selected");
return false;
}
System.out.println("\n--- Processing Payment ---");
System.out.println("Description: " + description);
boolean success = paymentStrategy.processPayment(amount);
// Record payment
PaymentRecord record = new PaymentRecord(
UUID.randomUUID().toString(),
amount,
description,
paymentStrategy.getPaymentMethod(),
success
);
paymentHistory.add(record);
System.out.println("Payment ID: " + record.getPaymentId());
System.out.println("--- Payment Complete ---\n");
return success;
}
public boolean refundPayment(String paymentId, double amount) {
// Find the payment record
PaymentRecord record = paymentHistory.stream()
.filter(r -> r.getPaymentId().equals(paymentId))
.findFirst()
.orElse(null);
if (record == null) {
System.out.println("✗ Payment record not found: " + paymentId);
return false;
}
if (!paymentStrategy.supportsRefund()) {
System.out.println("✗ Refunds not supported for " + paymentStrategy.getPaymentMethod());
return false;
}
System.out.println("\n--- Processing Refund ---");
System.out.printf("Refunding $%.2f for payment: %s%n", amount, paymentId);
boolean success = paymentStrategy.refund(amount);
if (success) {
System.out.println("✓ Refund processed successfully");
}
System.out.println("--- Refund Complete ---\n");
return success;
}
public void displayPaymentHistory() {
System.out.println("\n=== Payment History ===");
if (paymentHistory.isEmpty()) {
System.out.println("No payments processed yet");
return;
}
for (PaymentRecord record : paymentHistory) {
System.out.printf("%s | $%.2f | %s | %s | %s%n",
record.getPaymentId().substring(0, 8),
record.getAmount(),
record.getMethod(),
record.getDescription(),
record.isSuccess() ? "SUCCESS" : "FAILED");
}
}
}
// Payment record class
class PaymentRecord {
private final String paymentId;
private final double amount;
private final String description;
private final String method;
private final boolean success;
public PaymentRecord(String paymentId, double amount, String description, String method, boolean success) {
this.paymentId = paymentId;
this.amount = amount;
this.description = description;
this.method = method;
this.success = success;
}
// Getters
public String getPaymentId() { return paymentId; }
public double getAmount() { return amount; }
public String getDescription() { return description; }
public String getMethod() { return method; }
public boolean isSuccess() { return success; }
}
// Demo
public class PaymentProcessingDemo {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor();
System.out.println("=== Payment Processing Demo ===\n");
// Test different payment methods
System.out.println("1. Credit Card Payment:");
processor.setPaymentStrategy(new CreditCardPayment(
"4111111111111111", "12/25", "123", "John Doe"));
processor.processPayment(99.99, "Online Course");
System.out.println("2. PayPal Payment:");
processor.setPaymentStrategy(new PayPalPayment(
"[email protected]", "password123"));
processor.processPayment(49.99, "E-book Purchase");
System.out.println("3. Cryptocurrency Payment:");
processor.setPaymentStrategy(new CryptoPayment(
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "Bitcoin"));
processor.processPayment(199.99, "Hardware Wallet");
System.out.println("4. Bank Transfer:");
processor.setPaymentStrategy(new BankTransferPayment(
"123456789", "021000021", "John Doe"));
processor.processPayment(299.99, "Business Software");
// Display payment history
processor.displayPaymentHistory();
// Test refund
System.out.println("\n5. Testing Refund:");
processor.setPaymentStrategy(new CreditCardPayment(
"4111111111111111", "12/25", "123", "John Doe"));
processor.refundPayment(processor.getPaymentHistory().get(0).getPaymentId(), 99.99);
// Test crypto refund (should fail)
System.out.println("6. Testing Crypto Refund:");
processor.setPaymentStrategy(new CryptoPayment(
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "Bitcoin"));
processor.refundPayment("some-id", 50.00);
}
// Helper method to get payment history (would be in PaymentProcessor in real implementation)
static List<PaymentRecord> getPaymentHistory() {
return List.of(); // Simplified for demo
}
}

Example 2: Sorting Algorithm Strategies

import java.util.*;
// Strategy interface
interface SortStrategy {
void sort(int[] array);
String getAlgorithmName();
long getTimeComplexityWorst();
long getTimeComplexityBest();
String getSpaceComplexity();
}
// Concrete Strategies
class BubbleSortStrategy implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("Sorting using Bubble Sort...");
int n = array.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (array[j] > array[j + 1]) {
// Swap elements
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
@Override
public String getAlgorithmName() {
return "Bubble Sort";
}
@Override
public long getTimeComplexityWorst() {
return (long) Math.pow(10, 2); // O(n²)
}
@Override
public long getTimeComplexityBest() {
return (long) Math.pow(10, 2); // O(n²)
}
@Override
public String getSpaceComplexity() {
return "O(1)";
}
}
class QuickSortStrategy implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("Sorting using Quick Sort...");
quickSort(array, 0, array.length - 1);
}
private void quickSort(int[] array, int low, int high) {
if (low < high) {
int pi = partition(array, low, high);
quickSort(array, low, pi - 1);
quickSort(array, pi + 1, high);
}
}
private int partition(int[] array, int low, int high) {
int pivot = array[high];
int i = (low - 1);
for (int j = low; j < high; j++) {
if (array[j] <= pivot) {
i++;
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
int temp = array[i + 1];
array[i + 1] = array[high];
array[high] = temp;
return i + 1;
}
@Override
public String getAlgorithmName() {
return "Quick Sort";
}
@Override
public long getTimeComplexityWorst() {
return (long) Math.pow(10, 2); // O(n²)
}
@Override
public long getTimeComplexityBest() {
return (long) (10 * Math.log(10)); // O(n log n)
}
@Override
public String getSpaceComplexity() {
return "O(log n)";
}
}
class MergeSortStrategy implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("Sorting using Merge Sort...");
mergeSort(array, 0, array.length - 1);
}
private void mergeSort(int[] array, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(array, left, mid);
mergeSort(array, mid + 1, right);
merge(array, left, mid, right);
}
}
private void merge(int[] array, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] leftArray = new int[n1];
int[] rightArray = new int[n2];
System.arraycopy(array, left, leftArray, 0, n1);
System.arraycopy(array, mid + 1, rightArray, 0, n2);
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArray[i] <= rightArray[j]) {
array[k] = leftArray[i];
i++;
} else {
array[k] = rightArray[j];
j++;
}
k++;
}
while (i < n1) {
array[k] = leftArray[i];
i++;
k++;
}
while (j < n2) {
array[k] = rightArray[j];
j++;
k++;
}
}
@Override
public String getAlgorithmName() {
return "Merge Sort";
}
@Override
public long getTimeComplexityWorst() {
return (long) (10 * Math.log(10)); // O(n log n)
}
@Override
public long getTimeComplexityBest() {
return (long) (10 * Math.log(10)); // O(n log n)
}
@Override
public String getSpaceComplexity() {
return "O(n)";
}
}
class JavaBuiltInSortStrategy implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("Sorting using Java Arrays.sort()...");
Arrays.sort(array);
}
@Override
public String getAlgorithmName() {
return "Java Built-in Sort";
}
@Override
public long getTimeComplexityWorst() {
return (long) (10 * Math.log(10)); // O(n log n)
}
@Override
public long getTimeComplexityBest() {
return (long) (10 * Math.log(10)); // O(n log n)
}
@Override
public String getSpaceComplexity() {
return "O(log n)";
}
}
// Context class
class Sorter {
private SortStrategy strategy;
public void setSortStrategy(SortStrategy strategy) {
System.out.printf("Sort strategy changed to: %s%n", strategy.getAlgorithmName());
this.strategy = strategy;
}
public void sortArray(int[] array) {
if (strategy == null) {
System.out.println("No sort strategy selected. Using default (Quick Sort)");
strategy = new QuickSortStrategy();
}
System.out.println("\n--- Starting Sort ---");
System.out.println("Array size: " + array.length);
System.out.println("Algorithm: " + strategy.getAlgorithmName());
System.out.printf("Time Complexity: O(n²) worst, O(n log n) best%n");
System.out.println("Space Complexity: " + strategy.getSpaceComplexity());
int[] arrayCopy = Arrays.copyOf(array, array.length);
long startTime = System.nanoTime();
strategy.sort(arrayCopy);
long endTime = System.nanoTime();
double durationMs = (endTime - startTime) / 1_000_000.0;
System.out.printf("Sort completed in %.3f ms%n", durationMs);
System.out.println("Sorted array: " + Arrays.toString(Arrays.copyOf(arrayCopy, Math.min(10, arrayCopy.length))) + 
(arrayCopy.length > 10 ? "..." : ""));
System.out.println("--- Sort Complete ---\n");
// Verify sort
if (isSorted(arrayCopy)) {
System.out.println("✓ Array is correctly sorted");
} else {
System.out.println("✗ Array sorting failed");
}
}
private boolean isSorted(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
if (array[i] > array[i + 1]) {
return false;
}
}
return true;
}
public void compareStrategies(int[] array) {
System.out.println("=== Comparing Sort Strategies ===");
System.out.println("Array size: " + array.length);
SortStrategy[] strategies = {
new BubbleSortStrategy(),
new QuickSortStrategy(),
new MergeSortStrategy(),
new JavaBuiltInSortStrategy()
};
for (SortStrategy strategy : strategies) {
setSortStrategy(strategy);
int[] arrayCopy = Arrays.copyOf(array, array.length);
long startTime = System.nanoTime();
strategy.sort(arrayCopy);
long endTime = System.nanoTime();
double durationMs = (endTime - startTime) / 1_000_000.0;
System.out.printf("%-20s: %.3f ms%n", strategy.getAlgorithmName(), durationMs);
}
}
}
// Demo
public class SortingDemo {
public static void main(String[] args) {
Sorter sorter = new Sorter();
System.out.println("=== Sorting Algorithm Strategies Demo ===\n");
// Generate test data
int[] smallArray = generateRandomArray(50);
int[] mediumArray = generateRandomArray(1000);
int[] largeArray = generateRandomArray(10000);
System.out.println("1. Small Array (50 elements):");
sorter.setSortStrategy(new BubbleSortStrategy());
sorter.sortArray(smallArray);
System.out.println("2. Medium Array (1000 elements):");
sorter.setSortStrategy(new QuickSortStrategy());
sorter.sortArray(mediumArray);
System.out.println("3. Large Array (10000 elements):");
sorter.setSortStrategy(new JavaBuiltInSortStrategy());
sorter.sortArray(largeArray);
// Performance comparison
System.out.println("4. Performance Comparison:");
sorter.compareStrategies(mediumArray);
// Runtime strategy switching
System.out.println("\n5. Runtime Strategy Switching:");
int[] testArray = generateRandomArray(500);
sorter.setSortStrategy(new BubbleSortStrategy());
sorter.sortArray(Arrays.copyOf(testArray, testArray.length));
sorter.setSortStrategy(new MergeSortStrategy());
sorter.sortArray(Arrays.copyOf(testArray, testArray.length));
}
private static int[] generateRandomArray(int size) {
Random random = new Random();
int[] array = new int[size];
for (int i = 0; i < size; i++) {
array[i] = random.nextInt(1000);
}
return array;
}
}

Example 3: Compression Strategies

import java.util.*;
import java.util.zip.*;
// Strategy interface
interface CompressionStrategy {
byte[] compress(byte[] data);
byte[] decompress(byte[] compressedData);
String getAlgorithmName();
double getCompressionRatio(byte[] original, byte[] compressed);
}
// Concrete Strategies
class ZipCompressionStrategy implements CompressionStrategy {
@Override
public byte[] compress(byte[] data) {
System.out.println("Compressing using ZIP...");
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) {
gzos.write(data);
}
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException("ZIP compression failed", e);
}
}
@Override
public byte[] decompress(byte[] compressedData) {
System.out.println("Decompressing ZIP data...");
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPInputStream gzis = new GZIPInputStream(new java.io.ByteArrayInputStream(compressedData))) {
byte[] buffer = new byte[1024];
int len;
while ((len = gzis.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
}
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException("ZIP decompression failed", e);
}
}
@Override
public String getAlgorithmName() {
return "ZIP/GZIP";
}
@Override
public double getCompressionRatio(byte[] original, byte[] compressed) {
return (double) compressed.length / original.length;
}
}
class CustomCompressionStrategy implements CompressionStrategy {
@Override
public byte[] compress(byte[] data) {
System.out.println("Compressing using Custom RLE algorithm...");
// Simple Run-Length Encoding implementation
ByteArrayOutputStream result = new ByteArrayOutputStream();
if (data.length == 0) return result.toByteArray();
byte current = data[0];
int count = 1;
for (int i = 1; i < data.length; i++) {
if (data[i] == current && count < 255) {
count++;
} else {
result.write(count);
result.write(current);
current = data[i];
count = 1;
}
}
result.write(count);
result.write(current);
return result.toByteArray();
}
@Override
public byte[] decompress(byte[] compressedData) {
System.out.println("Decompressing Custom RLE data...");
ByteArrayOutputStream result = new ByteArrayOutputStream();
for (int i = 0; i < compressedData.length; i += 2) {
int count = compressedData[i] & 0xFF; // Convert to unsigned
byte value = compressedData[i + 1];
for (int j = 0; j < count; j++) {
result.write(value);
}
}
return result.toByteArray();
}
@Override
public String getAlgorithmName() {
return "Custom RLE";
}
@Override
public double getCompressionRatio(byte[] original, byte[] compressed) {
return (double) compressed.length / original.length;
}
}
class NoCompressionStrategy implements CompressionStrategy {
@Override
public byte[] compress(byte[] data) {
System.out.println("No compression applied...");
return Arrays.copyOf(data, data.length);
}
@Override
public byte[] decompress(byte[] compressedData) {
System.out.println("No decompression needed...");
return Arrays.copyOf(compressedData, compressedData.length);
}
@Override
public String getAlgorithmName() {
return "None";
}
@Override
public double getCompressionRatio(byte[] original, byte[] compressed) {
return 1.0; // No compression
}
}
// Context class
class FileCompressor {
private CompressionStrategy strategy;
public void setCompressionStrategy(CompressionStrategy strategy) {
System.out.printf("Compression strategy changed to: %s%n", strategy.getAlgorithmName());
this.strategy = strategy;
}
public CompressionResult compressFile(byte[] data, String filename) {
if (strategy == null) {
System.out.println("No compression strategy selected. Using ZIP as default.");
strategy = new ZipCompressionStrategy();
}
System.out.println("\n--- Compressing File ---");
System.out.println("Filename: " + filename);
System.out.println("Original size: " + data.length + " bytes");
System.out.println("Algorithm: " + strategy.getAlgorithmName());
long startTime = System.nanoTime();
byte[] compressed = strategy.compress(data);
long endTime = System.nanoTime();
double durationMs = (endTime - startTime) / 1_000_000.0;
double ratio = strategy.getCompressionRatio(data, compressed);
double savings = (1 - ratio) * 100;
System.out.printf("Compressed size: %d bytes%n", compressed.length);
System.out.printf("Compression ratio: %.2f (%.1f%% savings)%n", ratio, savings);
System.out.printf("Compression time: %.3f ms%n", durationMs);
System.out.println("--- Compression Complete ---\n");
return new CompressionResult(data, compressed, strategy.getAlgorithmName(), durationMs, ratio);
}
public byte[] decompressFile(byte[] compressedData, String originalAlgorithm) {
System.out.println("\n--- Decompressing File ---");
System.out.println("Original algorithm: " + originalAlgorithm);
long startTime = System.nanoTime();
byte[] decompressed = strategy.decompress(compressedData);
long endTime = System.nanoTime();
double durationMs = (endTime - startTime) / 1_000_000.0;
System.out.printf("Decompressed size: %d bytes%n", decompressed.length);
System.out.printf("Decompression time: %.3f ms%n", durationMs);
// Verify integrity
System.out.println("✓ Decompression successful");
System.out.println("--- Decompression Complete ---\n");
return decompressed;
}
public void compareStrategies(byte[] testData, String description) {
System.out.println("=== Compression Strategy Comparison ===");
System.out.println("Test data: " + description);
System.out.println("Original size: " + testData.length + " bytes");
CompressionStrategy[] strategies = {
new NoCompressionStrategy(),
new CustomCompressionStrategy(),
new ZipCompressionStrategy()
};
for (CompressionStrategy strategy : strategies) {
setCompressionStrategy(strategy);
CompressionResult result = compressFile(testData, "test.bin");
System.out.printf("%-15s: %d bytes (%.1f%% of original)%n",
strategy.getAlgorithmName(),
result.getCompressedSize(),
result.getCompressionRatio() * 100);
}
}
}
// Result class
class CompressionResult {
private final byte[] originalData;
private final byte[] compressedData;
private final String algorithm;
private final double durationMs;
private final double compressionRatio;
public CompressionResult(byte[] originalData, byte[] compressedData, 
String algorithm, double durationMs, double compressionRatio) {
this.originalData = originalData;
this.compressedData = compressedData;
this.algorithm = algorithm;
this.durationMs = durationMs;
this.compressionRatio = compressionRatio;
}
// Getters
public byte[] getOriginalData() { return originalData; }
public byte[] getCompressedData() { return compressedData; }
public String getAlgorithm() { return algorithm; }
public double getDurationMs() { return durationMs; }
public double getCompressionRatio() { return compressionRatio; }
public int getOriginalSize() { return originalData.length; }
public int getCompressedSize() { return compressedData.length; }
}
// Demo
public class CompressionDemo {
public static void main(String[] args) {
FileCompressor compressor = new FileCompressor();
System.out.println("=== Compression Strategies Demo ===\n");
// Test with different data types
System.out.println("1. Text Data Compression:");
String textData = "Hello World! ".repeat(1000);
byte[] textBytes = textData.getBytes();
compressor.setCompressionStrategy(new ZipCompressionStrategy());
CompressionResult textResult = compressor.compressFile(textBytes, "text.txt");
// Decompress to verify
compressor.decompressFile(textResult.getCompressedData(), textResult.getAlgorithm());
System.out.println("2. Random Data Compression:");
byte[] randomData = generateRandomData(5000);
compressor.setCompressionStrategy(new CustomCompressionStrategy());
CompressionResult randomResult = compressor.compressFile(randomData, "random.bin");
System.out.println("3. No Compression:");
compressor.setCompressionStrategy(new NoCompressionStrategy());
CompressionResult noCompressionResult = compressor.compressFile(textBytes, "text.txt");
// Strategy comparison
System.out.println("4. Strategy Comparison:");
compressor.compareStrategies(textBytes, "Repetitive text data");
// Runtime strategy switching based on data type
System.out.println("5. Smart Strategy Selection:");
byte[] data = generateRandomData(2000);
CompressionStrategy strategy = selectOptimalStrategy(data);
compressor.setCompressionStrategy(strategy);
compressor.compressFile(data, "smart.bin");
}
private static byte[] generateRandomData(int size) {
Random random = new Random();
byte[] data = new byte[size];
random.nextBytes(data);
return data;
}
private static CompressionStrategy selectOptimalStrategy(byte[] data) {
// Simple heuristic: Use custom RLE for repetitive data, ZIP for mixed data
boolean isRepetitive = isDataRepetitive(data);
if (isRepetitive) {
System.out.println("Data appears repetitive, selecting Custom RLE");
return new CustomCompressionStrategy();
} else {
System.out.println("Data appears mixed, selecting ZIP");
return new ZipCompressionStrategy();
}
}
private static boolean isDataRepetitive(byte[] data) {
if (data.length < 10) return false;
// Simple check: see if first few bytes repeat frequently
byte firstByte = data[0];
int sameCount = 0;
for (int i = 1; i < Math.min(100, data.length); i++) {
if (data[i] == firstByte) sameCount++;
}
return sameCount > 20; // More than 20% repetition in first 100 bytes
}
}

Advanced Strategy Pattern

Example 4: Strategy with Lambda Expressions (Java 8+)

import java.util.*;
import java.util.function.*;
// Functional interface strategies
@FunctionalInterface
interface ValidationStrategy {
boolean validate(String input);
}
@FunctionalInterface
interface TransformationStrategy {
String transform(String input);
}
class TextProcessor {
private ValidationStrategy validationStrategy;
private TransformationStrategy transformationStrategy;
// Constructor with default strategies
public TextProcessor() {
this.validationStrategy = input -> input != null && !input.trim().isEmpty();
this.transformationStrategy = String::toUpperCase;
}
// Set strategies using lambdas
public void setValidationStrategy(ValidationStrategy strategy) {
this.validationStrategy = strategy;
}
public void setTransformationStrategy(TransformationStrategy strategy) {
this.transformationStrategy = strategy;
}
public String process(String input) {
if (!validationStrategy.validate(input)) {
throw new IllegalArgumentException("Invalid input: " + input);
}
return transformationStrategy.transform(input);
}
// Predefined strategy combinations
public static class Strategies {
public static final ValidationStrategy EMAIL_VALIDATION = 
email -> email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
public static final ValidationStrategy PHONE_VALIDATION =
phone -> phone != null && phone.matches("^\\+?[\\d\\s-()]{10,}$");
public static final TransformationStrategy TO_CAMEL_CASE =
input -> {
if (input == null || input.isEmpty()) return input;
String[] words = input.split("\\s+");
return Arrays.stream(words)
.map(word -> word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase())
.reduce("", String::concat);
};
public static final TransformationStrategy REVERSE_STRING =
input -> new StringBuilder(input).reverse().toString();
}
}
// Demo with lambdas
public class LambdaStrategyDemo {
public static void main(String[] args) {
TextProcessor processor = new TextProcessor();
System.out.println("=== Lambda Strategy Demo ===\n");
// Using lambda expressions directly
processor.setValidationStrategy(input -> input != null && input.length() >= 5);
processor.setTransformationStrategy(input -> input.toUpperCase() + "!");
System.out.println("1. Custom validation and transformation:");
System.out.println(processor.process("hello")); // Valid
try {
System.out.println(processor.process("hi")); // Invalid - too short
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
// Using method references
System.out.println("\n2. Using method references:");
processor.setValidationStrategy(Objects::nonNull);
processor.setTransformationStrategy(String::toLowerCase);
System.out.println(processor.process("HELLO WORLD"));
// Using predefined strategies
System.out.println("\n3. Email validation and camel case:");
processor.setValidationStrategy(TextProcessor.Strategies.EMAIL_VALIDATION);
processor.setTransformationStrategy(TextProcessor.Strategies.TO_CAMEL_CASE);
try {
System.out.println(processor.process("[email protected]"));
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
// Runtime strategy composition
System.out.println("\n4. Dynamic strategy composition:");
List<TransformationStrategy> transformations = Arrays.asList(
String::toUpperCase,
s -> s.replace(" ", "_"),
s -> "[" + s + "]"
);
String result = "hello world";
for (TransformationStrategy strategy : transformations) {
result = strategy.transform(result);
}
System.out.println("Final result: " + result);
}
}

Best Practices

  1. Use Strategy Pattern when:
  • Multiple related classes differ only in behavior
  • You need different variants of an algorithm
  • Algorithm uses data clients shouldn't know about
  • Class has many conditional statements
  1. Make Strategies Stateless When Possible
   // Stateless strategy - can be singleton
class QuickSort implements SortStrategy {
private static final QuickSort INSTANCE = new QuickSort();
public static QuickSort getInstance() { return INSTANCE; }
private QuickSort() {}
}
  1. Use Factory Methods for Strategy Creation
   class PaymentStrategyFactory {
public static PaymentStrategy create(String type, Map<String, String> params) {
return switch (type) {
case "credit_card" -> new CreditCardPayment(params.get("cardNumber"), ...);
case "paypal" -> new PayPalPayment(params.get("email"), ...);
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
}
}
  1. Consider Strategy Composition
   class CompositeStrategy implements SortStrategy {
private final List<SortStrategy> strategies;
public void sort(int[] array) {
for (SortStrategy strategy : strategies) {
strategy.preProcess(array);
}
// main sort logic
}
}

When to Use the Strategy Pattern

Use Strategy Pattern when:

  • You have multiple algorithms for a specific task
  • You need to switch algorithms at runtime
  • You want to isolate algorithm implementation details
  • You have complex conditional logic for algorithm selection
  • You want to easily extend with new algorithms

Common Use Cases:

  • Payment processing - Different payment methods
  • Sorting algorithms - Various sort strategies
  • Compression utilities - Different compression algorithms
  • Validation systems - Different validation rules
  • File parsing - Different file format parsers
  • Navigation systems - Different route calculation algorithms

Benefits and Trade-offs

Benefits:

  • Eliminates conditional logic - Clean, focused classes
  • Runtime flexibility - Algorithms can be swapped dynamically
  • Open/Closed Principle - Easy to add new strategies
  • Testability - Each strategy can be tested independently
  • Reusability - Strategies can be reused across different contexts

Trade-offs:

  • Increased number of classes - More classes to manage
  • Client awareness - Clients must understand different strategies
  • Communication overhead - Between strategy and context
  • Over-engineering - For simple cases with few algorithms

Conclusion

The Strategy Pattern is a fundamental pattern for creating flexible, maintainable systems:

Key Benefits:

  • Algorithm Encapsulation - Each algorithm is isolated in its own class
  • Runtime Selection - Algorithms can be changed dynamically
  • Clean Architecture - Separates concerns between algorithm and context
  • Easy Testing - Strategies can be unit tested independently

Implementation Approach:

  1. Identify the varying behavior - What algorithms need to be interchangeable?
  2. Define Strategy Interface - Common interface for all algorithms
  3. Implement Concrete Strategies - Each algorithm in its own class
  4. Compose Strategy in Context - Context holds a reference to strategy
  5. Delegate Behavior - Context delegates work to current strategy

Real-World Impact:

  • Payment systems can easily add new payment methods
  • Sorting utilities can adapt to different data characteristics
  • Validation systems can apply different rules based on context
  • Compression tools can select optimal algorithms for different data types

By using the Strategy Pattern, you create systems that are flexible, maintainable, and ready for future extension without modifying existing code.


Remember: The Strategy Pattern is most valuable when you have multiple algorithms that need to be interchangeable and when the choice of algorithm needs to be made at runtime. For simpler cases with fixed behavior, consider simpler approaches to avoid unnecessary complexity.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper