196
behavioral design patterns

Strategy

Reference Wikipedia ↗
Strategy — class diagram
Plate 196 class diagram

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently from the clients that use it. This pattern avoids conditional complexity and promotes code reusability by defining a consistent interface for various algorithms.

This is particularly useful when you have multiple ways to accomplish a task, and you want to be able to select the appropriate algorithm at runtime, or when you need to be able to switch between algorithms easily. It promotes loose coupling between the client and the algorithm’s implementation.

Usage

The Strategy pattern is common in scenarios where you need flexible algorithms. Some examples include:

  • Payment Processing: Different payment methods (credit card, PayPal, bank transfer) can be implemented as separate strategies, allowing a shopping cart to support multiple payment options.
  • Sorting Algorithms: A sorting class can accept different sorting strategies (bubble sort, quicksort, merge sort) to sort data in various ways.
  • Compression Algorithms: A file archiver can use different compression algorithms (ZIP, GZIP, BZIP2) based on user preference or file type.
  • Validation Rules: Applying different validation rules to input data, such as email format, password strength, or data type.

Examples

  1. Java 8 Streams API: The Comparator interface in Java 8’s Streams API exemplifies the Strategy pattern. You can define different comparison strategies (e.g., comparing by name, by age, by date) and pass them to the sorted() method of a stream. The stream processing logic remains the same, but the sorting behavior changes based on the chosen comparator.

  2. Spring Data JPA: Spring Data JPA allows customizing query derivation by providing different JpaEntityMappings or implementing your own QuerydslPredicateExecutor. Each strategy determines how Spring Data JPA translates method names into database queries. This allows developers to tailor query creation without affecting core Spring Data functionalities.

  3. Log4j 2: Log4j 2 uses strategies for different aspects of logging. For example, the Layout interface defines a strategy for formatting log messages, allowing you to choose between plain text, JSON, XML, or other formats. Similarly, different Filter implementations act as strategies to determine which log messages are processed.

Specimens

15 implementations
Specimen 196.01 Dart View specimen ↗

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows an algorithm to vary independently from the clients that use it. This example demonstrates the Strategy pattern by defining different shipping cost calculation strategies (Standard, Express, Overnight). A ShippingContext class uses a ShippingStrategy interface to determine the cost, promoting loose coupling and flexibility. The Dart implementation leverages abstract classes and interfaces (protocols) which are core tenets of Dart’s type system and allow for clear contract definition between the context and strategies, fitting Dart’s emphasis on type safety and code organization.

// Define the Strategy interface
abstract class ShippingStrategy {
  double calculateCost(double weight, String destination);
}

// Concrete Strategies
class StandardShipping implements ShippingStrategy {
  @override
  double calculateCost(double weight, String destination) {
    // Some complex logic for standard shipping
    double baseCost = 5.0;
    double distanceCost = 0.1 * double.parse(destination.length); //simplified distance
    return baseCost + (weight * 2) + distanceCost;
  }
}

class ExpressShipping implements ShippingStrategy {
  @override
  double calculateCost(double weight, String destination) {
    // Some complex logic for express shipping
    double baseCost = 10.0;
    double distanceCost = 0.25 * double.parse(destination.length); //simplified distance
    return baseCost + (weight * 3) + distanceCost;
  }
}

class OvernightShipping implements ShippingStrategy {
  @override
  double calculateCost(double weight, String destination) {
    // Some complex logic for overnight shipping
    double baseCost = 20.0;
    double distanceCost = 0.5 * double.parse(destination.length); //simplified distance
    return baseCost + (weight * 5) + distanceCost;
  }
}

// Context
class ShippingContext {
  final ShippingStrategy strategy;

  ShippingContext(this.strategy);

  double calculateShippingCost(double weight, String destination) {
    return strategy.calculateCost(weight, destination);
  }
}

void main() {
  final standardShipping = StandardShipping();
  final expressShipping = ExpressShipping();
  final overnightShipping = OvernightShipping();

  final context1 = ShippingContext(standardShipping);
  final cost1 = context1.calculateShippingCost(2.0, "New York");
  print("Standard Shipping Cost: \$${cost1.toStringAsFixed(2)}");

  final context2 = ShippingContext(expressShipping);
  final cost2 = context2.calculateShippingCost(2.0, "Los Angeles");
  print("Express Shipping Cost: \$${cost2.toStringAsFixed(2)}");

  final context3 = ShippingContext(overnightShipping);
  final cost3 = context3.calculateShippingCost(2.0, "Miami");
  print("Overnight Shipping Cost: \$${cost3.toStringAsFixed(2)}");
}