Strategy Pattern
The Strategy pattern lets you swap algorithms at runtime without changing the code that uses them. Define a family of interchangeable behaviors, encapsulate each one, and let the client choose which to use—like choosing a route to work (drive, bike, bus) without changing your destination.
Quick Reference
Use When
- You have multiple algorithms for the same task and want to swap them at runtime
- You want to eliminate if/else or switch statements for selecting algorithm variants
- Different users, contexts, or configurations need different behaviors
- You're A/B testing different approaches (pricing, validation, formatting)
- Game entities need swappable behaviors (movement, attack, AI)
Avoid When
- You only have one algorithm that will never change (just use a function)
- The algorithms share significant code (consider Template Method instead)
- Behavior changes based on internal state transitions (use State pattern)
- You need to queue, undo, or log operations (use Command pattern)
- The overhead of extra classes/functions outweighs the flexibility benefit
The Analogy
Choosing a route to work: you can drive, bike, or take the bus. Each is a different strategy for the same goal (getting to work). You can switch strategies based on traffic, weather, or mood without changing your destination.
The Problem
You have multiple algorithms that accomplish the same task, and you want to switch between them at runtime. Hardcoding the choice with if/else or switch statements makes the code rigid—adding a new algorithm requires modifying existing code. The algorithms may be used in different combinations across your codebase, and you don't want to duplicate the selection logic everywhere.
The Solution
Define a family of algorithms, encapsulate each one, and make them interchangeable. The context delegates to a strategy object (or function) rather than implementing the algorithm itself. New strategies can be added without modifying existing code. In modern languages, strategies are often just functions—you don't always need classes.
Structure
Shows the static relationships between classes in the pattern.
Pattern Variants
Class-Based Strategy (GoF)
Traditional approach: define a Strategy interface, implement concrete strategy classes. Best for: complex strategies with multiple methods or internal state.
Function-Based Strategy
In languages with first-class functions (Python, JS), pass functions directly instead of classes. Best for: simple, stateless strategies—avoids class explosion.
Dictionary/Map Strategy
Store strategies in a dictionary keyed by name/config. Best for: when strategy selection comes from configuration, user input, or feature flags.
Resource-Based Strategy (Godot)
In Godot, extend Resource instead of RefCounted. Best for: strategies that should be editable in the Inspector and saveable as .tres files.
Implementations
Copy-paste examples in Python, JavaScript, and GDScript. Each includes validation and Director patterns.
from typing import Protocol, Callable
from dataclasses import dataclass
# ===== Modern Python: Protocol + Callable Strategies =====
# For complex strategies with multiple methods, use Protocol
class Exporter(Protocol):
"""Structural typing - no inheritance needed."""
def export(self, data: dict) -> str: ...
def file_extension(self) -> str: ...
class JsonExporter:
"""Implements Exporter protocol implicitly (duck typing)."""
def export(self, data: dict) -> str:
import json
return json.dumps(data, indent=2)
def file_extension(self) -> str:
return ".json"
class CsvExporter:
def export(self, data: dict) -> str:
if not data:
return ""
headers = ",".join(data.keys())
values = ",".join(str(v) for v in data.values())
return f"{headers}\n{values}"
def file_extension(self) -> str:
return ".csv"
# ===== For simple strategies, just use functions =====
# Type alias for pricing strategy
PricingStrategy = Callable[[float, dict], float]
def percentage_discount(total: float, context: dict) -> float:
"""10% off for members."""
return total * 0.9 if context.get("is_member") else total
def flat_discount(total: float, context: dict) -> float:
"""$5 off orders over $50."""
return total - 5 if total > 50 else total
def loyalty_discount(total: float, context: dict) -> float:
"""Discount based on loyalty tier."""
tiers = {"bronze": 0.95, "silver": 0.90, "gold": 0.85}
multiplier = tiers.get(context.get("tier", ""), 1.0)
return total * multiplier
# ===== Strategy selection via dictionary (avoids if/else chains) =====
PRICING_STRATEGIES: dict[str, PricingStrategy] = {
"percentage": percentage_discount,
"flat": flat_discount,
"loyalty": loyalty_discount,
}
@dataclass
class Order:
total: float
pricing_strategy: PricingStrategy = percentage_discount
def calculate_total(self, context: dict) -> float:
return self.pricing_strategy(self.total, context)
# Usage
order = Order(total=100.0, pricing_strategy=loyalty_discount)
final = order.calculate_total({"tier": "gold"}) # 85.0
# Or select strategy from config
strategy_name = "loyalty" # from config/user preference
strategy = PRICING_STRATEGIES[strategy_name]
order.pricing_strategy = strategyModern Python using Protocol for structural typing (no inheritance needed) and simple functions for stateless strategies. Uses a dictionary to select strategies by name—eliminates if/else chains. Protocol is preferred over ABC for strategy interfaces since strategies just need to match the signature.
// ===== Modern JS: Functions + Map for Strategy Selection =====
/**
* For simple strategies, use functions directly.
* No need for class hierarchies in JavaScript.
*
* @typedef {(text: string) => { valid: boolean, message?: string }} ValidationStrategy
*/
// Validation strategies as functions
const validators = {
email: (text) => ({
valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text),
message: "Enter a valid email address",
}),
phone: (text) => ({
valid: /^\d{10,}$/.test(text.replace(/\D/g, "")),
message: "Enter a valid phone number",
}),
required: (text) => ({
valid: text.trim().length > 0,
message: "This field is required",
}),
minLength: (min) => (text) => ({
valid: text.length >= min,
message: `Must be at least ${min} characters`,
}),
};
// ===== Context that uses strategies =====
class FormField {
#value = "";
#validators = [];
constructor(validators = []) {
this.#validators = validators;
}
setValue(value) {
this.#value = value;
}
validate() {
for (const validator of this.#validators) {
const result = validator(this.#value);
if (!result.valid) {
return result;
}
}
return { valid: true };
}
// Swap strategies at runtime
setValidators(validators) {
this.#validators = validators;
}
}
// ===== Usage =====
// Compose validators for different field types
const emailField = new FormField([validators.required, validators.email]);
const passwordField = new FormField([
validators.required,
validators.minLength(8),
]);
emailField.setValue("user@example.com");
console.log(emailField.validate()); // { valid: true }
emailField.setValue("invalid");
console.log(emailField.validate()); // { valid: false, message: "Enter a valid email" }
// ===== TypeScript version for type safety =====
/*
interface ValidationResult {
valid: boolean;
message?: string;
}
type ValidationStrategy = (text: string) => ValidationResult;
const validators: Record<string, ValidationStrategy | ((arg: any) => ValidationStrategy)> = {
email: (text) => ({ valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text) }),
// ...
};
*/
// ===== Shipping cost example (classic Strategy use case) =====
const shippingStrategies = {
standard: (weight) => weight * 0.5 + 5,
express: (weight) => weight * 1.0 + 15,
overnight: (weight) => weight * 2.0 + 30,
pickup: () => 0,
};
function calculateShipping(weight, method = "standard") {
const strategy = shippingStrategies[method];
if (!strategy) {
throw new Error(`Unknown shipping method: ${method}`);
}
return strategy(weight);
}
// Select from user preference or config
const cost = calculateShipping(10, "express"); // 25Modern JavaScript using functions and object maps instead of class hierarchies. This is more idiomatic—JS developers rarely create abstract base classes for simple strategy patterns. Shows validation strategies (composable), shipping strategies (from config), and includes a TypeScript type annotation example for type safety.
# ===== Godot: Resource-Based Strategies (Inspector-Editable) =====
#
# Use Resource instead of RefCounted so strategies can be:
# - Edited in the Inspector (drag-and-drop)
# - Saved as .tres files (reusable across scenes)
# - Configured by designers without code changes
# attack_strategy.gd
class_name AttackStrategy
extends Resource
## Base class for attack behaviors. Extend this to create new attacks.
@export var damage: int = 10
@export var cooldown: float = 1.0
@export var attack_name: String = "Attack"
func execute(attacker: Node2D, target: Node2D) -> void:
push_error("execute() must be overridden")
func can_use(attacker: Node2D) -> bool:
return true # Override for conditional attacks
# melee_attack.gd
class_name MeleeAttack
extends AttackStrategy
@export var range: float = 50.0
@export var knockback_force: float = 200.0
func execute(attacker: Node2D, target: Node2D) -> void:
var distance = attacker.global_position.distance_to(target.global_position)
if distance <= range:
if target.has_method("take_damage"):
target.take_damage(damage)
if target.has_method("apply_knockback"):
var direction = (target.global_position - attacker.global_position).normalized()
target.apply_knockback(direction * knockback_force)
func can_use(attacker: Node2D) -> bool:
# Could check stamina, animation state, etc.
return true
# ranged_attack.gd
class_name RangedAttack
extends AttackStrategy
@export var projectile_scene: PackedScene
@export var projectile_speed: float = 400.0
func execute(attacker: Node2D, target: Node2D) -> void:
if projectile_scene:
var projectile = projectile_scene.instantiate()
projectile.global_position = attacker.global_position
projectile.damage = damage
projectile.speed = projectile_speed
var direction = (target.global_position - attacker.global_position).normalized()
projectile.direction = direction
attacker.get_tree().current_scene.add_child(projectile)
# ===== Character using strategies =====
# character.gd
class_name Character
extends CharacterBody2D
## Attack strategy - assign in Inspector or swap at runtime
@export var attack_strategy: AttackStrategy
## Movement strategy - can also be a Resource
@export var movement_strategy: MovementStrategy
var current_target: Node2D
func attack() -> void:
if attack_strategy and current_target and attack_strategy.can_use(self):
attack_strategy.execute(self, current_target)
func set_attack(new_strategy: AttackStrategy) -> void:
attack_strategy = new_strategy
func _physics_process(delta: float) -> void:
if movement_strategy:
movement_strategy.move(self, delta)
# ===== Power-up that grants a new attack =====
# power_up.gd
extends Area2D
@export var granted_attack: AttackStrategy # Assign FireBall.tres in Inspector
func _on_body_entered(body: Node2D) -> void:
if body.has_method("set_attack"):
body.set_attack(granted_attack)
queue_free()
# ===== Creating strategy resources =====
# 1. Right-click in FileSystem → New Resource
# 2. Select "MeleeAttack" or "RangedAttack"
# 3. Configure in Inspector: damage=25, range=75, etc.
# 4. Save as "sword_attack.tres" or "fireball_attack.tres"
# 5. Drag onto Character's attack_strategy slotGDScript using Resource instead of RefCounted—the key upgrade for Godot. Resources can be edited in the Inspector, saved as .tres files, and drag-and-dropped onto nodes. Designers can create "SwordAttack.tres" and "FireballAttack.tres" without touching code. The @export annotations expose properties in the editor. This is idiomatic Godot and much more powerful than RefCounted strategies.
Strategy Pattern vs State
| Aspect | Strategy Pattern | State |
|---|---|---|
| Who chooses behavior | Client/config selects the strategy | Object transitions internally based on state |
| Awareness | Strategies don't know about each other | States often know about other states (to trigger transitions) |
| Lifetime | Strategy typically set once or swapped occasionally | State changes frequently during object lifetime |
| Purpose | HOW to do something (algorithm selection) | WHAT the object can do (behavior changes with state) |
| Example | Shipping calculator: FedEx vs UPS vs USPS | Document: Draft → Review → Published (different allowed actions) |
Real-World Examples
- Shipping cost calculation: FedEx, UPS, USPS, or local delivery strategies
- Form validation: email, phone, credit card, or custom validation rules
- Data export: JSON, CSV, XML, or PDF formatting strategies
- Pricing/discounts: percentage off, fixed amount, buy-one-get-one, loyalty pricing
- Game AI: aggressive, defensive, or patrol movement strategies
- Authentication: OAuth, JWT, API key, or session-based strategies
- Compression: gzip, bzip2, zstd, or no compression based on content type
Common Mistakes
Creating a class wrapper for a single function
# Overkill for a simple strategy
class UppercaseFormatter:
def format(self, text: str) -> str:
return text.upper()
class LowercaseFormatter:
def format(self, text: str) -> str:
return text.lower()
# Each class just wraps str.upper() or str.lower()!Fix: Use functions directly: formatters = {"upper": str.upper, "lower": str.lower}. Reserve classes for strategies with multiple methods or internal state.
Strategy selection with growing if/else chains
// The if/else just moved, not eliminated
function getShippingStrategy(method) {
if (method === "standard") return standardShipping;
if (method === "express") return expressShipping;
if (method === "overnight") return overnightShipping;
// Adding new method = modify this function
}Fix: Use a map/dictionary: const strategies = { standard: standardShipping, express: expressShipping }; return strategies[method]. Adding new strategies = adding a key, not modifying selection logic.
Strategy tightly coupled to Context internals
class DiscountStrategy:
def apply(self, context):
# Strategy reaches into context internals
total = context._cart._items_total # Private access!
user = context._user_service.get_current() # External dependency!
return total * 0.9 if user.is_premium else totalFix: Pass only what the strategy needs: def apply(self, total: float, is_premium: bool) -> float. The Context extracts values and passes them as parameters.
Confusing Strategy with State pattern
# This is actually State, not Strategy
class DocumentStrategy:
def publish(self, doc):
if doc.state == "draft":
doc.state = "review" # State transition!
elif doc.state == "review":
doc.state = "published" # Another transition!
# Strategies shouldn't manage state transitionsFix: Use State pattern when behavior depends on and changes internal state. Strategy is for algorithm selection by the client, not internal state machines.
When to Use Strategy Pattern
- When you have multiple algorithms for a specific task and need to swap them at runtime
- When you want to eliminate if/else or switch statements for selecting behavior
- When different users, contexts, or configurations need different algorithms
- When you're A/B testing different approaches (pricing, validation, formatting)
- When algorithm variations should be independent of the code that uses them
- When adding new algorithms should not require modifying existing code (Open/Closed)
Pitfalls to Avoid
- Over-engineering: Using Strategy for a single algorithm that will never change. FIX: Only use Strategy when you actually have (or expect) multiple interchangeable behaviors.
- Class explosion: Creating a class for every trivial strategy. FIX: Use functions for simple, stateless strategies—classes are for complex strategies with multiple methods or internal state.
- Selection logic sprawl: Moving the if/else chain to strategy selection. FIX: Use a dictionary/map keyed by configuration values, not a growing conditional.
- Tight coupling: Strategy knows too much about the Context. FIX: Pass only necessary data as parameters to execute(), not the entire Context object.
- Shared state complexity: Strategies that need to share or mutate state. FIX: Keep strategies stateless; pass state in parameters; or keep mutable state in the Context.
- Interface bloat: Forcing all strategies to implement methods they do not need. FIX: Split strategy interfaces by capability (ISP), or use separate Contexts for different strategy families.
Frequently Asked Questions
What is the difference between Strategy and State patterns?
Strategy: client/config chooses which algorithm to use. Strategies are independent and unaware of each other. State: behavior changes based on internal state transitions. States often know about other states to trigger transitions. Use Strategy for "which algorithm?" (shipping method, validation rules). Use State for "what can this object do now?" (document workflow, game character states).
What is the difference between Strategy and Template Method?
Strategy uses composition: you swap the entire algorithm by injecting a different strategy object. Template Method uses inheritance: you define a skeleton algorithm in a base class and override specific steps in subclasses. Strategy is more flexible (swap at runtime) but requires more objects. Template Method is simpler when subclasses only vary in specific steps.
What is the difference between Strategy and Command?
Strategy defines HOW to do something (algorithm selection). Command defines WHAT to do and packages it for later execution (request as object). Commands can be queued, logged, undone. Strategies are typically stateless algorithms. Use Command when you need undo/redo, queuing, or macro recording. Use Strategy when you need interchangeable algorithms.
Can I use functions instead of Strategy classes?
Yes, and you often should. In Python, JavaScript, and GDScript, functions are first-class citizens. For simple, stateless strategies (validation, formatting, calculation), pass functions directly. Reserve classes for complex strategies with multiple methods, internal state, or when you need Protocol/interface type checking.
How do I choose which strategy to use at runtime?
Best practice: use a dictionary/map keyed by configuration value, user preference, or feature flag. Avoid if/else chains for strategy selection because they just move the conditional instead of eliminating it. Example: strategies = {"fedex": fedex_shipping, "ups": ups_shipping}; selected = strategies[user_preference].
Should strategies be stateless?
Preferably yes. Stateless strategies can be shared, reused, and are easier to test. If a strategy needs data, pass it as parameters to execute(). If strategies must maintain state, keep it minimal and consider whether you actually need the State pattern instead.
How do I implement Strategy in Godot/GDScript?
Extend Resource instead of RefCounted. This lets you edit strategy properties in the Inspector, save strategies as .tres files, and drag-and-drop them onto nodes. Designers can configure "SwordAttack.tres" with damage=25 without touching code. Use @export for Inspector-editable properties.
When should I NOT use Strategy pattern?
Skip Strategy when: (1) You only have one algorithm that will never change. (2) The algorithms share significant code (use Template Method). (3) Behavior changes based on internal state (use State). (4) The overhead of extra classes/functions outweighs the flexibility. (5) A simple conditional is clearer for 2-3 options that rarely change.