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:

  1. Clear structure: Each strategy is well-defined and encapsulated in its own class.

  2. 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.

  3. Extensible: Adding a new strategy means simply adding a new class that implements the common interface.

Cons of the Classic Approach:

  1. 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.

  2. More verbose: For simple algorithms, creating entire classes for each strategy can feel unnecessary and heavy.

  3. 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:

  1. Less code: You don’t need to create new classes for each strategy, resulting in less boilerplate.

  2. More concise: Python treats functions as first-class citizens, so passing them around is more natural and concise.

  3. No inheritance needed: There is no need for abstract base classes or interfaces, simplifying the code structure.

  4. Easier to swap strategies: You can dynamically change strategies simply by assigning a new function.

Cons of the First-Class Function Approach:

  1. 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.

  2. 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.

  3. 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?