Strategy Pattern


  • Description: Define a family of interchangeable algorithms, encapsulate each, and let the client pick or swap one at runtime via composition — algorithm varies independently of the code that uses it.
  • My Notion Note ID: K2C-2-20
  • Created: 2026-05-22
  • Updated: 2026-05-22
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Core Idea

  • Replace switch (algorithmKind) or hardcoded algorithm with a pluggable object.
  • Context delegates the algorithm step to a strategy object held via composition (pointer, reference, std::function, template param).
  • Algorithm choice = data, not control flow.
  • Canonical examples: comparator for sort, compression codec, validation rule, retry policy, route planner, payment processor.

2. Structure

  • Context — holds a reference/handle to a Strategy. Calls it at the variable step.
  • Strategy (interface) — declares the operation. Often a single method.
  • ConcreteStrategyA, B, ... — implementations.
  • Client — instantiates the concrete strategy and injects it into context (constructor or setter).

Composition over inheritance — context doesn't subclass per algorithm.


3. Classic C++ Example

#include <memory>
#include <vector>
#include <iostream>
#include <numeric>

struct Compressor {
    virtual ~Compressor() = default;
    virtual std::vector<std::byte> compress(const std::vector<std::byte>& in) = 0;
};

struct Zstd  : Compressor { std::vector<std::byte> compress(const std::vector<std::byte>& in) override { /* zstd */  return in; } };
struct Gzip  : Compressor { std::vector<std::byte> compress(const std::vector<std::byte>& in) override { /* gzip */  return in; } };
struct Noop  : Compressor { std::vector<std::byte> compress(const std::vector<std::byte>& in) override { return in; } };

class Pipeline {
    std::unique_ptr<Compressor> codec_;
public:
    explicit Pipeline(std::unique_ptr<Compressor> c) : codec_(std::move(c)) {}
    void setCodec(std::unique_ptr<Compressor> c) { codec_ = std::move(c); }
    std::vector<std::byte> run(const std::vector<std::byte>& data) {
        return codec_->compress(data);
    }
};

int main() {
    Pipeline p{std::make_unique<Zstd>()};
    auto out = p.run({});
    p.setCodec(std::make_unique<Gzip>());
}

4. Modern std::function Form

Many strategies are single-method → lift to std::function. No vtables, no class hierarchy:

#include <functional>
#include <vector>
#include <algorithm>
#include <string>

using Comparator = std::function<bool(const std::string&, const std::string&)>;

class Sorter {
    Comparator cmp_;
public:
    explicit Sorter(Comparator c) : cmp_(std::move(c)) {}
    void sort(std::vector<std::string>& v) const { std::sort(v.begin(), v.end(), cmp_); }
};

int main() {
    std::vector<std::string> v{"banana", "apple", "Cherry"};

    Sorter byLex{[](auto& a, auto& b) { return a < b; }};
    byLex.sort(v);

    Sorter byLen{[](auto& a, auto& b) { return a.size() < b.size(); }};
    byLen.sort(v);

    Sorter caseInsensitive{[](auto& a, auto& b) {
        return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(),
            [](char x, char y) { return std::tolower(x) < std::tolower(y); });
    }};
    caseInsensitive.sort(v);
}

Compile-time variant — template parameter; algorithm picked at instantiation:

template <typename Cmp>
class StaticSorter {
    Cmp cmp_;
public:
    explicit StaticSorter(Cmp c = {}) : cmp_(std::move(c)) {}
    void sort(std::vector<std::string>& v) const { std::sort(v.begin(), v.end(), cmp_); }
};

StaticSorter<std::less<>>    s1;
StaticSorter<std::greater<>> s2;

This is the form std::sort, std::priority_queue, std::set already use — comparator + allocator as template parameters.


5. When to Use / When Not To

Use when:

  • Multiple algorithms exist for the same task; choice varies per call site or per config.
  • You want to add new algorithms without modifying the context (OCP).
  • Algorithm needs to be configurable, mockable, hot-swappable.

Avoid when:

  • Only one algorithm exists today and the change is purely speculative — premature abstraction.
  • Algorithm is trivial (1–2 lines) and rarely changes — inline it.
  • Differences between algorithms are deeper than a single hook (different inputs/outputs) — Strategy fits poorly.

6. Variants and Pitfalls

  • std::function allocates for non-trivial captures (small buffer optimization is implementation-defined). Hot paths: prefer template parameter or function_ref (proposed) for non-owning.
  • Null strategy — always construct with a default (e.g. Noop codec) rather than nullable handle; spares every call site a null check.
  • Strategy explosion — many tiny strategies. Group via factory; expose enum/string identifier mapping to concrete strategy.
  • Stateful strategies — usually a smell; strategies should be referentially transparent or scoped per call. If state grows, the strategy is becoming its own object.
  • Lifetime — context shouldn't outlive its injected strategy. Pass unique_ptr or by value; raw pointers without ownership story bite later.

7. Related Patterns

  • Strategy vs State — identical UML, opposite intent. Strategy: client chooses; algorithm doesn't change itself. State: the state object swaps itself based on internal transitions. Strategy = "how", State = "what mode am I in".
  • Strategy vs Template Method — both define a varying step. Strategy uses composition + runtime swap. Template Method uses inheritance + compile-time hook in a subclass override. Choose Strategy when the variation needs to switch per call or be configured externally; Template Method when the variation is fixed per subclass.
  • Policy-Based Design — compile-time Strategy via template parameters. See Policy Based Design. Zero overhead, but algorithm baked in at instantiation.
  • CRTP — another compile-time form (CRTP) when policy classes derive from a common base.
  • Command — strategy + invocation metadata (parameters, undo). Strategy is "how"; Command is "what to do + when".
  • Decorator — adds behavior; Strategy replaces behavior.

8. References