CrackedRuby CrackedRuby

Overview

The Strategy Pattern addresses a common software design problem: when a system needs to perform an operation that can be accomplished in multiple ways, and the choice of approach should remain flexible. Rather than hardcoding conditional logic to select between different algorithms, the pattern extracts each algorithm into separate classes that share a common interface.

The pattern originated from the Gang of Four's seminal "Design Patterns" book (1994) and has become one of the most widely applied behavioral patterns in object-oriented programming. The pattern's name derives from military strategy, where different tactical approaches might be employed to achieve the same objective depending on circumstances.

At its core, the Strategy Pattern consists of three participants: the Context (the class that needs to perform an operation), the Strategy interface (which defines the contract all algorithms must follow), and the Concrete Strategies (individual algorithm implementations). The Context maintains a reference to a Strategy object and delegates the algorithm execution to it, without knowing the specific implementation details.

Consider a payment processing system that must support multiple payment methods:

# Without Strategy Pattern - rigid conditional logic
class PaymentProcessor
  def process(amount, method)
    case method
    when :credit_card
      # Credit card processing logic
    when :paypal
      # PayPal processing logic
    when :cryptocurrency
      # Crypto processing logic
    end
  end
end

This approach creates tight coupling and violates the Open/Closed Principle. Adding new payment methods requires modifying the PaymentProcessor class. The Strategy Pattern eliminates these issues by treating each payment method as an independent, interchangeable strategy.

Key Principles

The Strategy Pattern operates on several foundational principles that distinguish it from other design patterns. Understanding these principles clarifies when and how to apply the pattern.

Algorithm Encapsulation: Each algorithm exists as a self-contained class with clear boundaries. The implementation details remain hidden behind the strategy interface, preventing leakage of algorithm-specific logic into client code. This encapsulation makes strategies easier to test, maintain, and reason about in isolation.

Runtime Flexibility: Unlike template methods or inheritance-based approaches, strategies can be swapped at runtime. A Context object can change its behavior dynamically by switching from one strategy to another without reconstruction. This flexibility proves essential in systems where behavior must adapt to changing conditions or user preferences.

Composition Over Inheritance: The pattern favors object composition (Context has-a Strategy) over inheritance (Context is-a specific implementation). This design avoids the fragility and tight coupling that class hierarchies introduce, particularly when dealing with multiple dimensions of variation.

Open/Closed Principle: The pattern exemplifies this SOLID principle. Systems remain open for extension (new strategies can be added) but closed for modification (existing code using strategies need not change). Adding support for a new algorithm requires creating a new strategy class, not modifying existing classes.

Single Responsibility Principle: Each strategy class has one reason to change: modifications to its specific algorithm. The Context class has a different reason to change: modifications to how strategies are coordinated or when they're invoked. This separation of concerns improves maintainability.

The relationship between these components follows a defined protocol. The Context receives a strategy instance, typically through constructor injection or a setter method. When the Context needs to execute the algorithm, it calls a method on the strategy interface, passing any necessary parameters. The concrete strategy performs the operation and returns results to the Context, which may process them further or pass them to the caller.

# Illustrating the relationship
class Context
  def initialize(strategy)
    @strategy = strategy
  end
  
  def execute_strategy(data)
    # Context delegates to strategy
    result = @strategy.perform(data)
    # Context may post-process result
    log_execution(result)
    result
  end
end

# Strategy interface (Ruby uses duck typing)
# All strategies must respond to 'perform'

class ConcreteStrategyA
  def perform(data)
    # Algorithm A implementation
    data.upcase
  end
end

class ConcreteStrategyB
  def perform(data)
    # Algorithm B implementation
    data.reverse
  end
end

The Context remains agnostic about which specific strategy it holds. It depends only on the interface contract, not the implementation. This inversion of dependency creates loose coupling that facilitates testing, parallel development, and system evolution.

Design Considerations

Selecting the Strategy Pattern involves analyzing trade-offs between flexibility and complexity. The pattern introduces additional classes and indirection, which may be excessive for simple scenarios with stable algorithms.

When to Use Strategy Pattern: The pattern fits when multiple algorithms exist for accomplishing a task, and the choice between them depends on runtime conditions, configuration, or user preference. Systems processing different data formats, implementing multiple sorting algorithms, or supporting various compression methods benefit from this pattern. The pattern also proves valuable when conditional logic for selecting algorithms becomes complex or repetitive across a codebase.

Applications with user-configurable behavior represent ideal use cases. Consider a data export feature where users can choose JSON, XML, or CSV output. Rather than littering the export code with format-specific conditionals, each format becomes a strategy. Users select their preferred strategy, and the system applies it without modification.

When to Avoid Strategy Pattern: Simple scenarios with two or three stable algorithms may not justify the pattern's overhead. If algorithms rarely change and the selection logic remains straightforward, a simple conditional statement or polymorphic method might suffice. The pattern also adds complexity when strategies require substantial shared code or when the strategy interface becomes unwieldy to accommodate all algorithm variations.

Alternatives to Consider: The Template Method pattern offers an inheritance-based alternative for algorithms sharing a common structure with varying steps. State Pattern handles behavior that changes based on an object's internal state, though it shares structural similarities with Strategy. Command Pattern works when operations need to be queued, logged, or undone. For simple cases, passing functions or lambdas as parameters provides strategy-like flexibility without formal classes.

Design Implications: The Strategy Pattern affects system architecture in several ways. Strategy classes must be instantiated and managed, requiring decisions about their lifecycle. Stateless strategies can be singletons or constants, while stateful strategies need careful instance management. The Context must receive strategy instances through dependency injection, constructor parameters, or factory methods.

The pattern impacts testing by isolating algorithm logic from the Context. Strategies can be tested independently with focused unit tests, while the Context can be tested with mock strategies. This separation improves test coverage and reduces test complexity.

Performance characteristics depend on implementation details. Stateless strategies incur minimal overhead, while strategies with heavy initialization or significant memory footprints require careful management. The indirection of delegating to strategy objects introduces negligible overhead in most applications, but high-frequency algorithm execution in performance-critical code may warrant measurement.

Ruby Implementation

Ruby's dynamic nature and support for first-class functions offers multiple approaches to implementing the Strategy Pattern. The classical object-oriented approach uses classes, while Ruby's functional features enable proc-based strategies.

Class-Based Strategy Implementation: The traditional approach defines a strategy interface through shared method names and implements concrete strategies as classes:

# Strategy interface (implicit in Ruby through duck typing)
class CompressionStrategy
  def compress(data)
    raise NotImplementedError, 'Subclasses must implement compress'
  end
end

class GzipCompression < CompressionStrategy
  def compress(data)
    require 'zlib'
    Zlib::Deflate.deflate(data)
  end
end

class BrotliCompression < CompressionStrategy
  def compress(data)
    require 'brotli'
    Brotli.deflate(data)
  end
end

class NoCompression < CompressionStrategy
  def compress(data)
    data
  end
end

class FileArchiver
  def initialize(compression_strategy)
    @compression_strategy = compression_strategy
  end
  
  def archive(file_path)
    data = File.read(file_path)
    compressed = @compression_strategy.compress(data)
    write_archive(compressed)
  end
  
  def compression_strategy=(strategy)
    @compression_strategy = strategy
  end
  
  private
  
  def write_archive(data)
    # Write compressed data to archive
  end
end

# Usage
archiver = FileArchiver.new(GzipCompression.new)
archiver.archive('document.txt')

# Switch strategies at runtime
archiver.compression_strategy = BrotliCompression.new
archiver.archive('large_file.bin')

Module-Based Strategy Implementation: Ruby modules provide an alternative to inheritance hierarchies. Strategies can be defined as modules and mixed into strategy classes:

module Strategies
  module Sorting
    module QuickSort
      def self.sort(array)
        return array if array.length <= 1
        pivot = array.delete_at(rand(array.length))
        left, right = array.partition { |x| x < pivot }
        sort(left) + [pivot] + sort(right)
      end
    end
    
    module MergeSort
      def self.sort(array)
        return array if array.length <= 1
        mid = array.length / 2
        left = sort(array[0...mid])
        right = sort(array[mid..-1])
        merge(left, right)
      end
      
      def self.merge(left, right)
        result = []
        until left.empty? || right.empty?
          result << (left.first <= right.first ? left.shift : right.shift)
        end
        result + left + right
      end
    end
    
    module BubbleSort
      def self.sort(array)
        arr = array.dup
        n = arr.length
        loop do
          swapped = false
          (n-1).times do |i|
            if arr[i] > arr[i+1]
              arr[i], arr[i+1] = arr[i+1], arr[i]
              swapped = true
            end
          end
          break unless swapped
        end
        arr
      end
    end
  end
end

class DataSorter
  attr_writer :strategy
  
  def initialize(strategy = Strategies::Sorting::QuickSort)
    @strategy = strategy
  end
  
  def sort(data)
    @strategy.sort(data)
  end
end

# Usage
sorter = DataSorter.new
sorter.sort([5, 2, 8, 1, 9])  # Uses QuickSort

sorter.strategy = Strategies::Sorting::MergeSort
sorter.sort([5, 2, 8, 1, 9])  # Uses MergeSort

Proc and Lambda-Based Strategies: Ruby's first-class functions allow strategies to be represented as procs or lambdas, reducing boilerplate for simple algorithms:

class PriceCalculator
  def initialize(discount_strategy = ->(price) { price })
    @discount_strategy = discount_strategy
  end
  
  def calculate(base_price)
    @discount_strategy.call(base_price)
  end
  
  def discount_strategy=(strategy)
    @discount_strategy = strategy
  end
end

# Define strategies as lambdas
no_discount = ->(price) { price }
percentage_discount = ->(percentage) {
  ->(price) { price * (1 - percentage / 100.0) }
}
bulk_discount = ->(threshold, discount) {
  ->(price) { price >= threshold ? price - discount : price }
}

calculator = PriceCalculator.new(percentage_discount.call(10))
calculator.calculate(100)  # => 90.0

calculator.discount_strategy = bulk_discount.call(50, 15)
calculator.calculate(60)  # => 45.0
calculator.calculate(40)  # => 40.0

Strategy Factory Pattern: In larger applications, creating and managing strategy instances benefits from factory methods:

class ValidationStrategy
  def validate(input)
    raise NotImplementedError
  end
end

class EmailValidation < ValidationStrategy
  def validate(input)
    input.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
  end
end

class PhoneValidation < ValidationStrategy
  def validate(input)
    input.match?(/\A\d{3}-\d{3}-\d{4}\z/)
  end
end

class UrlValidation < ValidationStrategy
  def validate(input)
    input.match?(/\Ahttps?:\/\/[\S]+\z/)
  end
end

class ValidationStrategyFactory
  STRATEGIES = {
    email: EmailValidation,
    phone: PhoneValidation,
    url: UrlValidation
  }.freeze
  
  def self.create(type)
    strategy_class = STRATEGIES[type]
    raise ArgumentError, "Unknown validation type: #{type}" unless strategy_class
    strategy_class.new
  end
end

class InputValidator
  def initialize(strategy_type)
    @strategy = ValidationStrategyFactory.create(strategy_type)
  end
  
  def valid?(input)
    @strategy.validate(input)
  end
end

# Usage
validator = InputValidator.new(:email)
validator.valid?('user@example.com')  # => true
validator.valid?('not-an-email')      # => false

Practical Examples

Example 1: Payment Processing System: An e-commerce application supporting multiple payment gateways demonstrates the Strategy Pattern's value in handling third-party integrations:

class PaymentStrategy
  def process(amount, payment_details)
    raise NotImplementedError
  end
  
  def refund(transaction_id, amount)
    raise NotImplementedError
  end
end

class StripePayment < PaymentStrategy
  def initialize(api_key)
    @api_key = api_key
  end
  
  def process(amount, payment_details)
    # Stripe-specific API call
    {
      status: :success,
      transaction_id: "stripe_#{SecureRandom.hex(8)}",
      amount: amount,
      processor: 'stripe'
    }
  end
  
  def refund(transaction_id, amount)
    # Stripe refund logic
    { status: :refunded, refund_id: "ref_#{SecureRandom.hex(6)}" }
  end
end

class PayPalPayment < PaymentStrategy
  def initialize(client_id, secret)
    @client_id = client_id
    @secret = secret
  end
  
  def process(amount, payment_details)
    # PayPal-specific API call
    {
      status: :success,
      transaction_id: "paypal_#{SecureRandom.hex(8)}",
      amount: amount,
      processor: 'paypal'
    }
  end
  
  def refund(transaction_id, amount)
    # PayPal refund logic
    { status: :refunded, refund_id: "pp_ref_#{SecureRandom.hex(6)}" }
  end
end

class CryptoPayment < PaymentStrategy
  def initialize(wallet_address)
    @wallet_address = wallet_address
  end
  
  def process(amount, payment_details)
    # Cryptocurrency payment logic
    {
      status: :pending,
      transaction_id: "crypto_#{SecureRandom.hex(8)}",
      amount: amount,
      processor: 'crypto',
      confirmation_required: true
    }
  end
  
  def refund(transaction_id, amount)
    # Crypto refund logic (complex due to blockchain)
    { status: :processing, estimated_completion: Time.now + 3600 }
  end
end

class Order
  attr_reader :total, :payment_strategy
  
  def initialize(total, payment_strategy)
    @total = total
    @payment_strategy = payment_strategy
    @payment_result = nil
  end
  
  def checkout(payment_details)
    @payment_result = @payment_strategy.process(@total, payment_details)
    send_confirmation if @payment_result[:status] == :success
    @payment_result
  end
  
  def refund(amount = @total)
    return { error: 'No payment to refund' } unless @payment_result
    @payment_strategy.refund(@payment_result[:transaction_id], amount)
  end
  
  private
  
  def send_confirmation
    # Send order confirmation
  end
end

# Usage
order = Order.new(99.99, StripePayment.new('sk_test_key'))
result = order.checkout({ card: '4242424242424242' })

# Switch payment method for another order
crypto_order = Order.new(0.005, CryptoPayment.new('0x742d35Cc'))
crypto_result = crypto_order.checkout({ wallet: '0x123...' })

Example 2: Data Export with Multiple Formats: Applications generating reports in various formats benefit from strategy-based format handling:

class ExportStrategy
  def export(data)
    raise NotImplementedError
  end
  
  def content_type
    raise NotImplementedError
  end
end

class JsonExport < ExportStrategy
  def export(data)
    require 'json'
    JSON.pretty_generate(data)
  end
  
  def content_type
    'application/json'
  end
end

class CsvExport < ExportStrategy
  def export(data)
    require 'csv'
    return '' if data.empty?
    
    CSV.generate do |csv|
      csv << data.first.keys
      data.each { |row| csv << row.values }
    end
  end
  
  def content_type
    'text/csv'
  end
end

class XmlExport < ExportStrategy
  def export(data)
    xml = ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<records>"]
    data.each do |record|
      xml << "  <record>"
      record.each { |key, value| xml << "    <#{key}>#{value}</#{key}>" }
      xml << "  </record>"
    end
    xml << "</records>"
    xml.join("\n")
  end
  
  def content_type
    'application/xml'
  end
end

class DataExporter
  def initialize(export_strategy)
    @export_strategy = export_strategy
  end
  
  def export_to_file(data, filename)
    content = @export_strategy.export(data)
    File.write(filename, content)
    {
      filename: filename,
      content_type: @export_strategy.content_type,
      size: content.bytesize
    }
  end
  
  def export_strategy=(strategy)
    @export_strategy = strategy
  end
end

# Usage
data = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
]

exporter = DataExporter.new(JsonExport.new)
exporter.export_to_file(data, 'users.json')

exporter.export_strategy = CsvExport.new
exporter.export_to_file(data, 'users.csv')

Example 3: Route Optimization with Different Algorithms: A delivery routing system selecting algorithms based on priority and constraints:

class RouteStrategy
  def calculate_route(start, destinations)
    raise NotImplementedError
  end
end

class NearestNeighborRoute < RouteStrategy
  def calculate_route(start, destinations)
    route = [start]
    remaining = destinations.dup
    current = start
    
    until remaining.empty?
      nearest = remaining.min_by { |dest| distance(current, dest) }
      route << nearest
      current = nearest
      remaining.delete(nearest)
    end
    
    { route: route, distance: total_distance(route) }
  end
  
  private
  
  def distance(point1, point2)
    Math.sqrt((point1[:x] - point2[:x])**2 + (point1[:y] - point2[:y])**2)
  end
  
  def total_distance(route)
    route.each_cons(2).sum { |p1, p2| distance(p1, p2) }
  end
end

class OptimizedRoute < RouteStrategy
  def calculate_route(start, destinations)
    # More complex optimization algorithm
    all_points = [start] + destinations
    best_route = nil
    best_distance = Float::INFINITY
    
    # Try multiple random permutations
    100.times do
      route = [start] + destinations.shuffle
      distance = total_distance(route)
      if distance < best_distance
        best_distance = distance
        best_route = route
      end
    end
    
    { route: best_route, distance: best_distance }
  end
  
  private
  
  def total_distance(route)
    route.each_cons(2).sum { |p1, p2| distance(p1, p2) }
  end
  
  def distance(point1, point2)
    Math.sqrt((point1[:x] - point2[:x])**2 + (point1[:y] - point2[:y])**2)
  end
end

class DeliveryRouter
  def initialize(route_strategy)
    @route_strategy = route_strategy
  end
  
  def plan_delivery(warehouse, customers)
    result = @route_strategy.calculate_route(warehouse, customers)
    {
      route: result[:route],
      total_distance: result[:distance],
      estimated_time: result[:distance] * 2  # 2 minutes per distance unit
    }
  end
  
  def route_strategy=(strategy)
    @route_strategy = strategy
  end
end

# Usage
warehouse = { x: 0, y: 0, name: 'Warehouse' }
customers = [
  { x: 10, y: 5, name: 'Customer A' },
  { x: 3, y: 8, name: 'Customer B' },
  { x: 7, y: 2, name: 'Customer C' }
]

# Fast delivery - use quick algorithm
fast_router = DeliveryRouter.new(NearestNeighborRoute.new)
fast_plan = fast_router.plan_delivery(warehouse, customers)

# Optimized delivery - use better algorithm
fast_router.route_strategy = OptimizedRoute.new
optimized_plan = fast_router.plan_delivery(warehouse, customers)

Common Patterns

Null Object Strategy: A special strategy that implements the interface but performs no operation, avoiding nil checks:

class LoggingStrategy
  def log(message)
    raise NotImplementedError
  end
end

class FileLogger < LoggingStrategy
  def initialize(filename)
    @file = File.open(filename, 'a')
  end
  
  def log(message)
    @file.puts("[#{Time.now}] #{message}")
    @file.flush
  end
end

class ConsoleLogger < LoggingStrategy
  def log(message)
    puts "[#{Time.now}] #{message}"
  end
end

class NullLogger < LoggingStrategy
  def log(message)
    # Intentionally does nothing
  end
end

class Application
  def initialize(logger = NullLogger.new)
    @logger = logger
  end
  
  def process_data(data)
    @logger.log("Processing started")
    # Process data
    @logger.log("Processing completed")
  end
end

Composite Strategy: Combining multiple strategies to create complex behavior:

class ValidationStrategy
  def validate(input)
    raise NotImplementedError
  end
end

class LengthValidation < ValidationStrategy
  def initialize(min:, max:)
    @min = min
    @max = max
  end
  
  def validate(input)
    length = input.to_s.length
    return { valid: false, error: 'Too short' } if length < @min
    return { valid: false, error: 'Too long' } if length > @max
    { valid: true }
  end
end

class FormatValidation < ValidationStrategy
  def initialize(pattern)
    @pattern = pattern
  end
  
  def validate(input)
    return { valid: true } if input.to_s.match?(@pattern)
    { valid: false, error: 'Invalid format' }
  end
end

class CompositeValidation < ValidationStrategy
  def initialize(strategies)
    @strategies = strategies
  end
  
  def validate(input)
    @strategies.each do |strategy|
      result = strategy.validate(input)
      return result unless result[:valid]
    end
    { valid: true }
  end
end

# Usage
password_validator = CompositeValidation.new([
  LengthValidation.new(min: 8, max: 128),
  FormatValidation.new(/[A-Z]/),
  FormatValidation.new(/[a-z]/),
  FormatValidation.new(/[0-9]/)
])

password_validator.validate('weak')        # => { valid: false, error: 'Too short' }
password_validator.validate('StrongPass1') # => { valid: true }

Strategy Chain: Executing strategies in sequence until one succeeds:

class AuthenticationStrategy
  def authenticate(credentials)
    raise NotImplementedError
  end
end

class DatabaseAuth < AuthenticationStrategy
  def authenticate(credentials)
    # Check database
    return { authenticated: true, method: 'database' } if valid_in_db?(credentials)
    { authenticated: false }
  end
  
  private
  
  def valid_in_db?(credentials)
    # Database check logic
    credentials[:username] == 'admin' && credentials[:password] == 'secret'
  end
end

class LdapAuth < AuthenticationStrategy
  def authenticate(credentials)
    # Check LDAP
    return { authenticated: true, method: 'ldap' } if valid_in_ldap?(credentials)
    { authenticated: false }
  end
  
  private
  
  def valid_in_ldap?(credentials)
    # LDAP check logic
    false
  end
end

class OAuthAuth < AuthenticationStrategy
  def authenticate(credentials)
    # Check OAuth
    return { authenticated: true, method: 'oauth' } if valid_oauth?(credentials)
    { authenticated: false }
  end
  
  private
  
  def valid_oauth?(credentials)
    # OAuth validation logic
    credentials[:token]&.start_with?('oauth_')
  end
end

class AuthenticationChain
  def initialize(strategies)
    @strategies = strategies
  end
  
  def authenticate(credentials)
    @strategies.each do |strategy|
      result = strategy.authenticate(credentials)
      return result if result[:authenticated]
    end
    { authenticated: false, error: 'All authentication methods failed' }
  end
end

# Usage
auth_chain = AuthenticationChain.new([
  DatabaseAuth.new,
  LdapAuth.new,
  OAuthAuth.new
])

auth_chain.authenticate(username: 'admin', password: 'secret')
# => { authenticated: true, method: 'database' }

auth_chain.authenticate(token: 'oauth_abc123')
# => { authenticated: true, method: 'oauth' }

Common Pitfalls

Overuse for Simple Cases: Applying the Strategy Pattern to trivial decisions adds unnecessary complexity. A simple if-else statement or ternary operator suffices when only two options exist and they're unlikely to expand:

# Overkill - Strategy Pattern for simple choice
class GreetingStrategy
  def greet(name)
    raise NotImplementedError
  end
end

class FormalGreeting < GreetingStrategy
  def greet(name)
    "Good day, #{name}"
  end
end

class CasualGreeting < GreetingStrategy
  def greet(name)
    "Hey #{name}"
  end
end

# Better - Simple conditional
def greet(name, formal: false)
  formal ? "Good day, #{name}" : "Hey #{name}"
end

Inadequate Strategy Interface: Designing a strategy interface that forces all concrete strategies to accept the same parameters, even when some strategies don't need all parameters, creates awkward implementations:

# Problematic - forced parameter consistency
class DiscountStrategy
  def calculate(base_price, customer_type, quantity, day_of_week)
    raise NotImplementedError
  end
end

class BulkDiscount < DiscountStrategy
  def calculate(base_price, customer_type, quantity, day_of_week)
    # Only uses quantity, others ignored
    quantity > 10 ? base_price * 0.9 : base_price
  end
end

class HappyHourDiscount < DiscountStrategy
  def calculate(base_price, customer_type, quantity, day_of_week)
    # Only uses day_of_week, others ignored
    ['saturday', 'sunday'].include?(day_of_week) ? base_price * 0.85 : base_price
  end
end

# Better - use context object or strategy-specific parameters
class DiscountContext
  attr_accessor :base_price, :customer_type, :quantity, :day_of_week
  
  def initialize(base_price:, customer_type: nil, quantity: 1, day_of_week: nil)
    @base_price = base_price
    @customer_type = customer_type
    @quantity = quantity
    @day_of_week = day_of_week
  end
end

class BulkDiscount
  def calculate(context)
    context.quantity > 10 ? context.base_price * 0.9 : context.base_price
  end
end

Strategy State Management Issues: Strategies that maintain state between invocations can cause subtle bugs when strategies are shared across contexts:

# Problematic - stateful strategy shared between contexts
class CountingStrategy
  def initialize
    @call_count = 0
  end
  
  def execute(data)
    @call_count += 1
    "Processed #{data} (call ##{@call_count})"
  end
end

shared_strategy = CountingStrategy.new
context1 = Context.new(shared_strategy)
context2 = Context.new(shared_strategy)

context1.execute('A')  # call #1
context2.execute('B')  # call #2 - unexpected!

# Better - keep strategies stateless or create new instances
class StatelessStrategy
  def execute(data, call_count)
    "Processed #{data} (call ##{call_count})"
  end
end

class Context
  def initialize(strategy)
    @strategy = strategy
    @call_count = 0
  end
  
  def execute(data)
    @call_count += 1
    @strategy.execute(data, @call_count)
  end
end

Forgetting to Handle Strategy Absence: Contexts that allow nil strategies without providing default behavior or proper validation cause runtime errors:

# Problematic - nil strategy not handled
class Processor
  attr_writer :strategy
  
  def process(data)
    @strategy.transform(data)  # NoMethodError if @strategy is nil
  end
end

# Better - provide default or validate
class Processor
  def initialize(strategy = DefaultStrategy.new)
    @strategy = strategy
  end
  
  def strategy=(new_strategy)
    raise ArgumentError, 'Strategy cannot be nil' if new_strategy.nil?
    @strategy = new_strategy
  end
  
  def process(data)
    @strategy.transform(data)
  end
end

Tight Coupling Between Strategies and Context: When strategies access internal state of the Context directly, changing either strategies or the Context becomes difficult:

# Problematic - strategy depends on Context internals
class ReportStrategy
  def generate(context)
    # Accesses context's internal structure
    data = context.instance_variable_get(:@raw_data)
    format = context.instance_variable_get(:@format_settings)
    # Generate report
  end
end

# Better - Context provides clean interface
class ReportStrategy
  def generate(data, settings)
    # Works with provided data
  end
end

class ReportGenerator
  def generate
    data = prepare_data
    settings = get_settings
    @strategy.generate(data, settings)
  end
  
  private
  
  def prepare_data
    # Prepare and return data
  end
  
  def get_settings
    # Return settings
  end
end

Reference

Strategy Pattern Components

Component Role Responsibility
Context Client class that uses strategies Maintains reference to strategy, delegates algorithm execution, manages strategy lifecycle
Strategy Interface Defines contract for algorithms Declares method signature that all concrete strategies must implement
Concrete Strategy Implements specific algorithm Contains algorithm implementation, encapsulates variation point
Client Code Configures and uses Context Creates strategy instances, injects strategies into Context, invokes Context operations

Implementation Checklist

Step Action Consideration
1 Identify algorithm variation points Find code with multiple conditional branches for different algorithms
2 Define strategy interface Create method signature that all algorithms will implement
3 Extract algorithms to concrete strategies Move each algorithm variant into separate class implementing the interface
4 Modify Context to use strategies Replace conditional logic with delegation to strategy object
5 Implement strategy injection Add constructor parameter or setter method for strategy
6 Handle strategy lifecycle Decide if strategies are stateless singletons or require new instances
7 Consider factory for strategy creation Add factory if strategy selection logic is complex

Common Method Names

Method Name Purpose Usage
execute General algorithm execution Generic contexts where algorithm purpose varies
process Data transformation or processing Data processing pipelines
calculate Computation or calculation Mathematical or financial calculations
validate Input validation Form or data validation systems
format Output formatting Data export or display formatting
compress Data compression File or data compression systems
encrypt Data encryption Security and cryptography contexts
sort Collection sorting Sorting algorithms
route Path or route calculation Navigation and routing systems

Strategy Selection Criteria

Consideration Use Strategy When Avoid Strategy When
Algorithm Count Three or more algorithm variants One or two simple variants
Change Frequency Algorithms added/modified regularly Algorithms stable and rarely change
Algorithm Complexity Each algorithm complex enough to warrant isolation Algorithms are simple one-liners
Runtime Selection Algorithm chosen at runtime based on conditions Algorithm known at compile time
Code Reuse Algorithms reused in multiple contexts Algorithm used in single location
Testing Isolation Algorithms need independent unit tests Simple logic easily tested inline

Ruby-Specific Considerations

Approach When to Use Trade-offs
Class-based strategies Complex algorithms with state or multiple methods More verbose, better for object-oriented designs
Module-based strategies Stateless algorithms needing namespace organization Clean organization, but less object-oriented
Proc/Lambda strategies Simple, stateless algorithms Compact syntax, harder to test and organize
Method objects Single-method strategies with parameters Callable objects, good for simple cases

Design Pattern Relationships

Pattern Relationship Key Difference
State Structural similarity, both use composition State changes behavior based on internal state, Strategy changes based on client choice
Template Method Both handle algorithm variation Template Method uses inheritance, Strategy uses composition
Command Both encapsulate operations Command encapsulates request as object with undo/redo, Strategy encapsulates algorithm
Factory Method Often used together Factory creates strategy instances, Strategy implements algorithms
Decorator Both use composition Decorator adds behavior, Strategy replaces behavior