Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Ever wondered how your favorite video game ๐ฎ switches between different difficulty levels? Or how a navigation app ๐บ๏ธ can switch between โfastest routeโ, โshortest routeโ, or โavoid tollsโ? Thatโs the Strategy Pattern in action!
The Strategy Pattern is like having a Swiss Army knife ๐ช where each tool (strategy) serves a specific purpose, and you can switch between them based on your needs. In this tutorial, weโll learn how to implement this powerful design pattern in Python to make our code more flexible and maintainable! ๐ช
๐ Understanding Strategy Pattern
Think of the Strategy Pattern as having different game plans for different situations. Just like a football coach ๐ might have different strategies for offense and defense, your code can have different algorithms that can be swapped out depending on the situation.
Hereโs what makes the Strategy Pattern special:
- Interchangeable algorithms: Switch between different approaches seamlessly ๐
- Open/Closed principle: Add new strategies without changing existing code ๐
- Runtime flexibility: Choose the right strategy on the fly โก
๐ง Basic Syntax and Usage
Letโs start with a simple example - a discount calculator for an online store! ๐
from abc import ABC, abstractmethod
# ๐ First, we define our strategy interface
class DiscountStrategy(ABC):
@abstractmethod
def calculate_discount(self, price):
pass
# ๐ฏ Now, let's create some concrete strategies
class RegularCustomerDiscount(DiscountStrategy):
def calculate_discount(self, price):
return price * 0.05 # 5% discount ๐ฐ
class PremiumCustomerDiscount(DiscountStrategy):
def calculate_discount(self, price):
return price * 0.15 # 15% discount ๐
class VIPCustomerDiscount(DiscountStrategy):
def calculate_discount(self, price):
return price * 0.25 # 25% discount ๐
# ๐ Our shopping cart that uses strategies
class ShoppingCart:
def __init__(self, discount_strategy):
self.discount_strategy = discount_strategy
self.items = []
def add_item(self, item, price):
self.items.append({"item": item, "price": price})
def calculate_total(self):
total = sum(item["price"] for item in self.items)
discount = self.discount_strategy.calculate_discount(total)
return total - discount
# ๐ Let's use it!
regular_cart = ShoppingCart(RegularCustomerDiscount())
regular_cart.add_item("Python Book", 29.99)
regular_cart.add_item("Coffee Mug", 15.99)
print(f"Regular customer total: ${regular_cart.calculate_total():.2f}")
# Output: Regular customer total: $43.68
๐ก Practical Examples
Example 1: Payment Processing System ๐ณ
Letโs build a payment processor that can handle different payment methods!
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount):
pass
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number, cvv):
self.card_number = card_number
self.cvv = cvv
def pay(self, amount):
# ๐ณ Processing credit card payment
print(f"Processing ${amount} via Credit Card ending in {self.card_number[-4:]}")
return f"Payment of ${amount} successful! ๐"
class PayPalPayment(PaymentStrategy):
def __init__(self, email):
self.email = email
def pay(self, amount):
# ๐ง Processing PayPal payment
print(f"Redirecting to PayPal for {self.email}...")
return f"PayPal payment of ${amount} completed! โ
"
class CryptoPayment(PaymentStrategy):
def __init__(self, wallet_address):
self.wallet_address = wallet_address
def pay(self, amount):
# ๐ช Processing crypto payment
print(f"Sending ${amount} worth of crypto to wallet: {self.wallet_address[:10]}...")
return f"Crypto transfer successful! ๐"
# ๐๏ธ Online store checkout
class OnlineStore:
def __init__(self):
self.payment_strategy = None
def set_payment_method(self, payment_strategy):
self.payment_strategy = payment_strategy
def checkout(self, amount):
if not self.payment_strategy:
return "Please select a payment method! โ"
return self.payment_strategy.pay(amount)
# ๐ฎ Let's go shopping!
store = OnlineStore()
# Customer chooses credit card
store.set_payment_method(CreditCardPayment("1234-5678-9012-3456", "123"))
print(store.checkout(99.99))
# Another customer prefers PayPal
store.set_payment_method(PayPalPayment("[email protected]"))
print(store.checkout(49.99))
Example 2: Game Character AI ๐ฎ
Create different AI behaviors for game enemies!
import random
class AttackStrategy(ABC):
@abstractmethod
def attack(self, player_health):
pass
class AggressiveAttack(AttackStrategy):
def attack(self, player_health):
# ๐ค Always go for maximum damage!
damage = random.randint(15, 25)
return damage, f"Enemy charges with fury! ๐ฅ -{damage} HP"
class DefensiveAttack(AttackStrategy):
def attack(self, player_health):
# ๐ก๏ธ Play it safe
damage = random.randint(5, 10)
return damage, f"Enemy attacks cautiously. ๐ก๏ธ -{damage} HP"
class SmartAttack(AttackStrategy):
def attack(self, player_health):
# ๐ง Adapt based on player's health
if player_health > 50:
damage = random.randint(10, 20)
return damage, f"Enemy analyzes and strikes! ๐ฏ -{damage} HP"
else:
damage = random.randint(20, 30)
return damage, f"Enemy senses weakness and strikes hard! โ๏ธ -{damage} HP"
class GameEnemy:
def __init__(self, name, attack_strategy):
self.name = name
self.attack_strategy = attack_strategy
self.health = 100
def perform_attack(self, player_health):
damage, message = self.attack_strategy.attack(player_health)
return damage, f"{self.name}: {message}"
def change_strategy(self, new_strategy):
# ๐ Enemies can adapt!
self.attack_strategy = new_strategy
print(f"{self.name} changes tactics! ๐ฒ")
# ๐ฎ Create different enemy types
goblin = GameEnemy("Sneaky Goblin", DefensiveAttack())
orc = GameEnemy("Brutal Orc", AggressiveAttack())
boss = GameEnemy("Dark Wizard", SmartAttack())
# Battle simulation
player_health = 100
print("โ๏ธ Battle begins!")
damage, msg = goblin.perform_attack(player_health)
print(msg)
# Boss adapts mid-battle!
boss.change_strategy(AggressiveAttack())
damage, msg = boss.perform_attack(player_health - damage)
print(msg)
Example 3: Data Compression Utility ๐ฆ
Build a file compressor with different algorithms!
import zlib
import bz2
import lzma
class CompressionStrategy(ABC):
@abstractmethod
def compress(self, data):
pass
@abstractmethod
def decompress(self, data):
pass
class ZipCompression(CompressionStrategy):
def compress(self, data):
# ๐๏ธ Using zlib compression
compressed = zlib.compress(data.encode())
ratio = (1 - len(compressed) / len(data)) * 100
return compressed, f"ZIP compressed: {ratio:.1f}% smaller! ๐"
def decompress(self, data):
return zlib.decompress(data).decode()
class BZ2Compression(CompressionStrategy):
def compress(self, data):
# ๐ฆ Using bz2 compression
compressed = bz2.compress(data.encode())
ratio = (1 - len(compressed) / len(data)) * 100
return compressed, f"BZ2 compressed: {ratio:.1f}% smaller! ๐"
def decompress(self, data):
return bz2.decompress(data).decode()
class LZMACompression(CompressionStrategy):
def compress(self, data):
# ๐ฏ Using LZMA compression (best ratio)
compressed = lzma.compress(data.encode())
ratio = (1 - len(compressed) / len(data)) * 100
return compressed, f"LZMA compressed: {ratio:.1f}% smaller! ๐"
def decompress(self, data):
return lzma.decompress(data).decode()
class FileCompressor:
def __init__(self, strategy):
self.strategy = strategy
def compress_file(self, content):
return self.strategy.compress(content)
def decompress_file(self, compressed_data):
return self.strategy.decompress(compressed_data)
def switch_algorithm(self, new_strategy):
self.strategy = new_strategy
print("Compression algorithm switched! ๐")
# ๐ Let's compress some text!
sample_text = """
The Strategy Pattern is amazing! ๐
It lets us switch algorithms at runtime.
Perfect for compression, encryption, and more!
""" * 10 # Make it longer for better compression
# Try different compression strategies
compressor = FileCompressor(ZipCompression())
# Test ZIP
compressed, message = compressor.compress_file(sample_text)
print(message)
# Switch to BZ2
compressor.switch_algorithm(BZ2Compression())
compressed, message = compressor.compress_file(sample_text)
print(message)
# Switch to LZMA (best compression)
compressor.switch_algorithm(LZMACompression())
compressed, message = compressor.compress_file(sample_text)
print(message)
๐ Advanced Concepts
Strategy Factory Pattern ๐ญ
Combine Strategy with Factory pattern for even more flexibility!
class StrategyFactory:
_strategies = {}
@classmethod
def register_strategy(cls, name, strategy_class):
cls._strategies[name] = strategy_class
@classmethod
def create_strategy(cls, name, *args, **kwargs):
strategy_class = cls._strategies.get(name)
if not strategy_class:
raise ValueError(f"Unknown strategy: {name} ๐")
return strategy_class(*args, **kwargs)
@classmethod
def list_strategies(cls):
return list(cls._strategies.keys())
# ๐ Register our payment strategies
StrategyFactory.register_strategy("credit_card", CreditCardPayment)
StrategyFactory.register_strategy("paypal", PayPalPayment)
StrategyFactory.register_strategy("crypto", CryptoPayment)
# ๐ฏ Use the factory
payment_type = "paypal" # Could come from user input
payment = StrategyFactory.create_strategy(payment_type, "[email protected]")
print(payment.pay(75.00))
print(f"Available payment methods: {StrategyFactory.list_strategies()} ๐ณ")
Context-Aware Strategy Selection ๐ง
Let strategies choose themselves based on context!
class ShippingStrategy(ABC):
@abstractmethod
def calculate_shipping(self, weight, distance):
pass
@abstractmethod
def can_handle(self, weight, distance):
pass
class StandardShipping(ShippingStrategy):
def calculate_shipping(self, weight, distance):
return weight * 0.5 + distance * 0.1
def can_handle(self, weight, distance):
return weight <= 50 and distance <= 1000
class ExpressShipping(ShippingStrategy):
def calculate_shipping(self, weight, distance):
return weight * 1.0 + distance * 0.3
def can_handle(self, weight, distance):
return weight <= 30 and distance <= 500
class FreightShipping(ShippingStrategy):
def calculate_shipping(self, weight, distance):
return weight * 0.3 + distance * 0.05 + 50 # Base fee
def can_handle(self, weight, distance):
return weight > 50 # Heavy items only
class SmartShippingCalculator:
def __init__(self):
self.strategies = [
ExpressShipping(),
StandardShipping(),
FreightShipping()
]
def calculate(self, weight, distance):
# ๐ง Auto-select the best strategy
for strategy in self.strategies:
if strategy.can_handle(weight, distance):
cost = strategy.calculate_shipping(weight, distance)
strategy_name = strategy.__class__.__name__
return cost, f"Using {strategy_name}: ${cost:.2f} ๐ฆ"
return None, "No suitable shipping method found! ๐"
# ๐ Test automatic strategy selection
calculator = SmartShippingCalculator()
# Light package, short distance
cost, method = calculator.calculate(5, 100)
print(f"Small package: {method}")
# Heavy package
cost, method = calculator.calculate(75, 800)
print(f"Heavy package: {method}")
โ ๏ธ Common Pitfalls and Solutions
โ Wrong: Hardcoding Strategy Logic
# โ Don't do this!
class BadPaymentProcessor:
def process_payment(self, method, amount):
if method == "credit_card":
# Credit card logic here
return "Credit card processed"
elif method == "paypal":
# PayPal logic here
return "PayPal processed"
elif method == "crypto":
# Crypto logic here
return "Crypto processed"
# Adding new payment method requires modifying this class! ๐ฑ
โ Right: Using Strategy Pattern
# โ
Do this instead!
class PaymentProcessor:
def __init__(self, strategy):
self.strategy = strategy
def process_payment(self, amount):
return self.strategy.pay(amount)
# Adding new payment methods doesn't require changing this class! ๐
โ Wrong: Tight Coupling
# โ Strategy knows too much about the context
class BadStrategy:
def execute(self, context_object):
# Accessing internal state directly
context_object.internal_state += 1
context_object.private_method()
โ Right: Loose Coupling
# โ
Strategy only gets what it needs
class GoodStrategy:
def execute(self, required_data):
# Work only with provided data
result = self.process(required_data)
return result
๐ ๏ธ Best Practices
-
Keep strategies focused ๐ฏ
- Each strategy should do one thing well
- Donโt mix responsibilities
-
Use meaningful names ๐
AggressiveAttackStrategy
notStrategy1
- Names should describe behavior
-
Consider performance โก
- Strategy switching should be lightweight
- Cache strategies if creation is expensive
-
Provide a default strategy ๐ก๏ธ
- Always have a fallback option
- Prevents null pointer exceptions
-
Document strategy behavior ๐
- Clear documentation for each strategy
- Include when to use each one
๐งช Hands-On Exercise
Create a text formatter that can apply different formatting strategies! ๐
Challenge: Build a TextFormatter
class that can:
- Convert text to uppercase
- Convert text to title case
- Add emoji decorations
- Reverse the text
- Allow switching between strategies at runtime
๐ก Click here for the solution
class FormattingStrategy(ABC):
@abstractmethod
def format(self, text):
pass
class UpperCaseFormatter(FormattingStrategy):
def format(self, text):
return text.upper() + " ๐"
class TitleCaseFormatter(FormattingStrategy):
def format(self, text):
return text.title() + " โจ"
class EmojiFormatter(FormattingStrategy):
def format(self, text):
return f"๐ {text} ๐"
class ReverseFormatter(FormattingStrategy):
def format(self, text):
return text[::-1] + " ๐"
class TextFormatter:
def __init__(self, strategy=None):
self.strategy = strategy or TitleCaseFormatter()
def set_strategy(self, strategy):
self.strategy = strategy
def format_text(self, text):
return self.strategy.format(text)
# ๐ฎ Test your formatter!
formatter = TextFormatter()
sample = "hello strategy pattern"
# Try different formatters
print("Original:", sample)
print("Title:", formatter.format_text(sample))
formatter.set_strategy(UpperCaseFormatter())
print("Upper:", formatter.format_text(sample))
formatter.set_strategy(EmojiFormatter())
print("Emoji:", formatter.format_text(sample))
formatter.set_strategy(ReverseFormatter())
print("Reverse:", formatter.format_text(sample))
# ๐ฏ Bonus: Chain multiple strategies!
class ChainedFormatter(FormattingStrategy):
def __init__(self, *strategies):
self.strategies = strategies
def format(self, text):
result = text
for strategy in self.strategies:
result = strategy.format(result)
return result
# Combine strategies!
chained = ChainedFormatter(
TitleCaseFormatter(),
EmojiFormatter()
)
formatter.set_strategy(chained)
print("Chained:", formatter.format_text(sample))
๐ Key Takeaways
Youโve mastered the Strategy Pattern! Hereโs what you learned:
- Strategy Pattern lets you define a family of algorithms and make them interchangeable ๐
- Encapsulation - Each algorithm is encapsulated in its own class ๐ฆ
- Open/Closed Principle - Add new strategies without modifying existing code ๐
- Runtime flexibility - Switch algorithms on the fly based on conditions โก
- Clean code - Eliminates long if-else chains and switch statements โจ
๐ค Next Steps
Congratulations on mastering the Strategy Pattern! ๐ Youโre becoming a Python design pattern expert!
Hereโs what to explore next:
- Observer Pattern ๐๏ธ - Learn about event-driven programming
- Decorator Pattern ๐จ - Add behaviors to objects dynamically
- Factory Pattern ๐ญ - Create objects without specifying their exact class
Keep practicing with real-world scenarios like:
- Building a multi-format file converter ๐
- Creating a game with different difficulty modes ๐ฎ
- Implementing a flexible pricing system ๐ฐ
Remember, the Strategy Pattern is your Swiss Army knife for algorithm flexibility! Use it whenever you need to switch between different ways of doing something. Happy coding! ๐โจ