Strategy Pattern Using First-Class Function
The classic Strategy Pattern is typically implemented with classes, where each strategy is encapsulated in its own class, and an interface or abstract class defines a common method that all strategies must implement. In Python, thanks to first-class functions, you can implement the Strategy Pattern in a clean and concise way without needing to create classes for each strategy.
Let’s compare this class-based approach with the first-class functions approach.
1. Classic Approach (Class-based Strategy Pattern)
In the classic approach, you have a common interface and separate classes for each strategy.
Example (Classic Approach):
from abc import ABC, abstractmethod
# Abstract base strategy class
class Strategy(ABC):
@abstractmethod
def execute(self, a, b):
pass
# Concrete strategies
class AddStrategy(Strategy):
def execute(self, a, b):
return a + b
class SubtractStrategy(Strategy):
def execute(self, a, b):
return a - b
class MultiplyStrategy(Strategy):
def execute(self, a, b):
return a * b
# Context class that uses the strategy
class Calculator:
def __init__(self, strategy: Strategy):
self._strategy = strategy
def set_strategy(self, strategy: Strategy):
self._strategy = strategy
def execute(self, a, b):
return self._strategy.execute(a, b)
# Usage of the class-based approach
calc = Calculator(AddStrategy())
print(calc.execute(3, 4)) # Output: 7 (Addition)
calc.set_strategy(SubtractStrategy())
print(calc.execute(10, 5)) # Output: 5 (Subtraction)
calc.set_strategy(MultiplyStrategy())
print(calc.execute(6, 7)) # Output: 42 (Multiplication)
Pros of the Classic Approach:
Clear structure: Each strategy is well-defined and encapsulated in its own class.
Type safety: If you're using a statically typed language (e.g., Java or C++), this approach ensures that all strategies follow a common interface, which is useful for maintaining consistency and enforcing method signatures.
Extensible: Adding a new strategy means simply adding a new class that implements the common interface.
Cons of the Classic Approach:
Boilerplate code: You need to create a new class for every new strategy, which can lead to a lot of boilerplate, especially if strategies are simple.
More verbose: For simple algorithms, creating entire classes for each strategy can feel unnecessary and heavy.
Overhead of inheritance: This approach uses inheritance (abstract base classes), which may add complexity, especially if the hierarchy grows or changes over time.
2. First-Class Functions Approach
In contrast, the first-class function approach simplifies this by treating strategies as functions. There’s no need for interfaces or class hierarchies—just pass the function directly.
Example (First-Class Function Approach):
# Define different strategies as simple functions
def strategy_add(a, b):
return a + b
def strategy_subtract(a, b):
return a - b
def strategy_multiply(a, b):
return a * b
# Context class that uses first-class functions as strategies
class Calculator:
def __init__(self, strategy):
self._strategy = strategy # Assign function as strategy
def set_strategy(self, strategy):
self._strategy = strategy # Dynamically set strategy
def execute(self, a, b):
return self._strategy(a, b)
# Usage of the function-based approach
calc = Calculator(strategy_add)
print(calc.execute(3, 4)) # Output: 7 (Addition)
calc.set_strategy(strategy_subtract)
print(calc.execute(10, 5)) # Output: 5 (Subtraction)
calc.set_strategy(strategy_multiply)
print(calc.execute(6, 7)) # Output: 42 (Multiplication)
Pros of the First-Class Function Approach:
Less code: You don’t need to create new classes for each strategy, resulting in less boilerplate.
More concise: Python treats functions as first-class citizens, so passing them around is more natural and concise.
No inheritance needed: There is no need for abstract base classes or interfaces, simplifying the code structure.
Easier to swap strategies: You can dynamically change strategies simply by assigning a new function.
Cons of the First-Class Function Approach:
Lacks structure for complex cases: For simple strategies, functions work well, but as strategies grow in complexity (e.g., having state or multiple methods), encapsulating them into classes might make more sense.
No explicit interface enforcement: There’s no formal contract like an interface that enforces the function signature, meaning a strategy could be passed that doesn’t follow the expected behavior.
Harder to extend: If strategies become more complex (requiring attributes, state, or more methods), this function-based approach may not scale well.
Comparison Summary:
Aspect
Class-Based Approach
First-Class Functions Approach
Code Complexity
More complex; involves inheritance and classes
Simpler; just functions, no inheritance needed
Encapsulation
High, each strategy is encapsulated in its own class
Lower, but still modular due to standalone functions
Boilerplate
More boilerplate, especially for simple strategies
Minimal code, as functions handle logic
Flexibility
Good, supports complex strategies with state and behavior
Great for simple, stateless strategies
Extensibility
Easy to extend by adding new classes
Easy for simple functions, but may become tricky with complex logic
Type Safety
More formal with abstract base classes
No formal interface, relies on function signature
Dynamic Behavior
Requires setting a new class-based strategy
Easily swap strategies by passing a different function
When to Use Which Approach:
Class-based approach: Best when strategies are complex, involve state, or need multiple methods. Also suitable for larger systems where formal structure and type enforcement are important.
First-class function approach: Ideal when strategies are simple, stateless, and concise. It works well in Python due to the language's support for first-class functions and dynamic typing.
In Python, the function-based approach is often preferred for simplicity, but if your strategies become complex or need more structure, the class-based approach can offer better organization.
Last updated
Was this helpful?