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 |