精读C++20设计模式——行为型设计模式:策略模式

前言

​ 我们天天都在用策略模式!标准库的算法在设计上就是一种策略模式!

​ 策略模式的核心想法很简单:把算法/行为从使用它的类中抽离出来,封装成可互换的“策略(Strategy)”,并允许在运行时或编译期替换这些策略,从而实现算法的可扩展、可测试与解耦。


什么是策略模式?

​ 策略模式把一组可以互换的算法封装到独立对象(或类型)中,客户类(Context)通过统一接口使用它们。注意!策略是“可替换的” —— Context 不知道具体实现细节,只依赖策略接口。


动态策略

​ 动态策略通常用虚函数(interface + subclass)或类型擦除(std::function / std::unique_ptr 指向多态对象)实现,优点是运行时可替换,灵活适配配置或用户选择;代价是虚调/间接调用开销和更复杂的生命周期管理。

#include <iostream>
#include <memory>
#include <string>

// 策略接口
struct ICompressor {
    virtual ~ICompressor() = default;
    virtual std::string compress(const std::string& data) = 0;
};

// 具体策略 A
struct GzipCompressor : ICompressor {
    std::string compress(const std::string& data) override {
        return "gzip(" + data + ")";
    }
};

// 具体策略 B
struct NoopCompressor : ICompressor {
    std::string compress(const std::string& data) override {
        return data;
    }
};

// Context
class CompressionContext {
public:
    CompressionContext(std::unique_ptr<ICompressor> s) : strategy_(std::move(s)) {}

    void set_strategy(std::unique_ptr<ICompressor> s) { strategy_ = std::move(s); }

    std::string perform(const std::string& data) {
        return strategy_->compress(data);
    }
private:
    std::unique_ptr<ICompressor> strategy_;
};

int main() {
    CompressionContext ctx(std::make_unique<NoopCompressor>());
    std::cout << ctx.perform("hello") << "\n";            // hello

    ctx.set_strategy(std::make_unique<GzipCompressor>());
    std::cout << ctx.perform("hello") << "\n";            // gzip(hello)
}

​ 如您所见,我们实际上包装了两个压缩策略:这是在外面运行的时候配置的——配置、插件或用户交互决定这一切。策略实现复杂且需要继承/多态;需要在程序运行期间动态替换策略。

注意事项

  • 如果策略是有状态的(内部有成员),要明确其共享/独占语义(shared_ptr vs unique_ptr)。
  • 线程安全:多个线程共享策略实例需要保证策略实现是线程安全或每线程独立实例。
  • 单元测试方便:可以替换为 mock 策略。

静态策略

静态策略利用模板参数或 CRTP 把策略在编译期绑定进 Context。它能带来零虚调用、内联优化和更好性能,但牺牲了运行时切换和二进制大小/可重用性的灵活性。

#include <iostream>
#include <string>

// 策略:UpperCaseFormatter / LowerCaseFormatter
struct UpperCaseFormatter {
    static std::string format(std::string s) {
        for (auto &c : s) c = static_cast<char>(std::toupper(c));
        return s;
    }
};

struct LowerCaseFormatter {
    static std::string format(std::string s) {
        for (auto &c : s) c = static_cast<char>(std::tolower(c));
        return s;
    }
};

// Context 模板化
template<typename FormatterPolicy>
class TextProcessor {
public:
    std::string process(const std::string& s) {
        return FormatterPolicy::format(s);
    }
};

int main() {
    TextProcessor<UpperCaseFormatter> up;
    TextProcessor<LowerCaseFormatter> low;

    std::cout << up.process("Hello") << "\n"; // HELLO
    std::cout << low.process("Hello") << "\n"; // hello
}

​ 当然,有时候既想要运行时灵活性又想要简洁接口,可以考虑使用 std::function 或自定义类型擦除(small-buffer optimization),例如:

#include <functional>
#include <iostream>
#include <string>

class Printer {
public:
    using Strategy = std::function<void(const std::string&)>;
    Printer(Strategy s) : strategy_(std::move(s)) {}
    void set_strategy(Strategy s) { strategy_ = std::move(s); }
    void print(const std::string& s) { strategy_(s); }
private:
    Strategy strategy_;
};

int main() {
    Printer p([](const std::string& s){ std::cout << "A: " << s << "\n"; });
    p.print("x");
    p.set_strategy([](const std::string& s){ std::cout << "B: " << s << "\n"; });
    p.print("y");
}

std::function 的优点是写法短、易用,但它也有类型擦除和内存/调用开销。适合策略实现比较小或不在超高频路径的情况。


总结

动态策略(运行时)

  • 优点:高度灵活、运行时可替换、适合插件/配置驱动。
  • 缺点:虚调用 / 间接调用开销、需要处理生命周期/并发。

静态策略(编译期)

  • 优点:零虚开销、编译器优化、内联、类型安全。
  • 缺点:缺乏运行时切换、编译单元膨胀(可能增加二进制),策略必须在编译期已知。

类型擦除(std::function / 自定义)

  • 优点:编码方便、接口统一、比虚继承更轻量(在小策略场景)。
  • 缺点:隐藏类型信息、可能有分配/性能开销。