Menu
Coddy logo textTech

Strategy Pattern

Part of the Object Oriented Programming section of Coddy's Dart journey — lesson 93 of 110.

The Strategy pattern lets you define a family of algorithms, encapsulate each one in its own class, and make them interchangeable at runtime. Instead of hardcoding behavior into a class, you inject the behavior through an interface, allowing you to swap strategies without modifying the class that uses them.

Consider a payment system where you want to support different payment methods. Rather than using conditionals to handle each method, you define a strategy interface and create concrete implementations:

abstract class PaymentStrategy {
  void pay(double amount);
}

class CreditCardPayment implements PaymentStrategy {
  @override
  void pay(double amount) => print('Paid \$amount with Credit Card');
}

class PayPalPayment implements PaymentStrategy {
  @override
  void pay(double amount) => print('Paid \$amount with PayPal');
}

class ShoppingCart {
  PaymentStrategy _paymentStrategy;

  ShoppingCart(this._paymentStrategy);

  void setPaymentStrategy(PaymentStrategy strategy) {
    _paymentStrategy = strategy;
  }

  void checkout(double total) {
    _paymentStrategy.pay(total);
  }
}

void main() {
  var cart = ShoppingCart(CreditCardPayment());
  cart.checkout(99.99);  // Paid $99.99 with Credit Card

  cart.setPaymentStrategy(PayPalPayment());
  cart.checkout(49.99);  // Paid $49.99 with PayPal
}

The ShoppingCart doesn't know or care how payment works - it just delegates to whatever strategy is currently set. You can add new payment methods by creating new strategy classes without touching the cart code. This pattern is ideal when you have multiple ways to perform an action and want to select or switch between them dynamically.

challenge icon

Challenge

Easy

Let's build a text formatting system using the Strategy pattern! You'll create a system where different formatting strategies can be applied to text, allowing users to switch between formats dynamically without changing the core text processor.

You'll organize your code into two files:

  • formatting_strategies.dart: Create your strategy system here. Define an abstract class FormattingStrategy with a method String format(String text). Then implement three concrete strategies:
    • UppercaseStrategy - converts text to all uppercase
    • LowercaseStrategy - converts text to all lowercase
    • TitleCaseStrategy - capitalizes the first letter of each word (split by spaces, capitalize first character of each word, rest lowercase)
    Also create a TextProcessor class that holds a FormattingStrategy. It should have a constructor that accepts an initial strategy, a setStrategy() method to change the strategy at runtime, and a process(String text) method that applies the current strategy and returns the formatted result.
  • main.dart: Import your formatting strategies and demonstrate how the Strategy pattern allows dynamic behavior switching. Create a TextProcessor starting with the UppercaseStrategy. Process the text hello world and print the result. Then switch to LowercaseStrategy, process HELLO WORLD, and print. Finally, switch to TitleCaseStrategy, process the quick brown fox, and print the result.

This demonstrates the power of the Strategy pattern - your TextProcessor doesn't need to know the details of how formatting works. It simply delegates to whatever strategy is currently set, making it easy to add new formatting options without modifying the processor!

Expected output:

HELLO WORLD
hello world
The Quick Brown Fox

Cheat sheet

The Strategy pattern defines a family of algorithms, encapsulates each one in its own class, and makes them interchangeable at runtime. Instead of hardcoding behavior, you inject it through an interface.

Define a strategy interface:

abstract class PaymentStrategy {
  void pay(double amount);
}

Create concrete strategy implementations:

class CreditCardPayment implements PaymentStrategy {
  @override
  void pay(double amount) => print('Paid \$amount with Credit Card');
}

class PayPalPayment implements PaymentStrategy {
  @override
  void pay(double amount) => print('Paid \$amount with PayPal');
}

Create a context class that uses the strategy:

class ShoppingCart {
  PaymentStrategy _paymentStrategy;

  ShoppingCart(this._paymentStrategy);

  void setPaymentStrategy(PaymentStrategy strategy) {
    _paymentStrategy = strategy;
  }

  void checkout(double total) {
    _paymentStrategy.pay(total);
  }
}

Use the pattern by injecting and switching strategies:

var cart = ShoppingCart(CreditCardPayment());
cart.checkout(99.99);  // Paid $99.99 with Credit Card

cart.setPaymentStrategy(PayPalPayment());
cart.checkout(49.99);  // Paid $49.99 with PayPal

The context class delegates behavior to the strategy without knowing implementation details. Add new strategies by creating new classes without modifying existing code.

Try it yourself

import 'formatting_strategies.dart';

void main() {
  // TODO: Create a TextProcessor starting with UppercaseStrategy
  
  // TODO: Process the text 'hello world' and print the result
  
  // TODO: Switch to LowercaseStrategy
  
  // TODO: Process 'HELLO WORLD' and print the result
  
  // TODO: Switch to TitleCaseStrategy
  
  // TODO: Process 'the quick brown fox' and print the result
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming