设计模式入门:7. 策略模式详解 C++实现

策略模式详解:把算法"装"进盒子里随意切换,C++完整实现

引言

你每天上班会选择什么出行方式?

  • 时间充裕且天气好:骑自行车,环保又健身
  • 赶时间但预算有限:坐地铁,快速又便宜
  • 下雨或者带了很多东西:打车,舒适又方便
  • 公司有急事:开车,最灵活

如果用代码来实现这个逻辑,你可能会写出这样的代码:

cpp 复制代码
void goToWork(const std::string& way) {
    if (way == "bike") {
        std::cout << "骑自行车去上班" << std::endl;
        // 骑自行车的具体逻辑...
    } else if (way == "subway") {
        std::cout << "坐地铁去上班" << std::endl;
        // 坐地铁的具体逻辑...
    } else if (way == "taxi") {
        std::cout << "打车去上班" << std::endl;
        // 打车的具体逻辑...
    } else if (way == "car") {
        std::cout << "开车去上班" << std::endl;
        // 开车的具体逻辑...
    }
}

这段代码看起来很简单,但存在严重的问题:

  1. 违反开闭原则 :新增一种出行方式(比如共享单车),必须修改goToWork函数
  2. 代码臃肿:所有出行方式的逻辑都挤在一个函数里,难以维护
  3. 无法动态切换:一旦选择了一种方式,中途不能改变
  4. 复用性差:如果其他地方也需要用到打车逻辑,只能复制粘贴

策略模式(Strategy Pattern) 正是为了解决这个问题而生的。它是一种行为型设计模式,定义了一系列算法,将每个算法封装起来,并使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户端。

今天我们就用C++语言,从基础概念到完整实现,彻底搞懂策略模式。


一、策略模式的核心概念

1.1 解决的痛点

策略模式主要解决以下问题:

  • 多重条件判断的噩梦 :大量的if-elseswitch-case语句,代码难以阅读和维护
  • 算法与使用代码耦合:算法的实现和使用代码混在一起,修改算法可能会影响使用代码
  • 无法动态切换算法:算法在编译时就确定了,运行时不能改变
  • 算法复用性差:相同的算法在多个地方使用时,只能复制粘贴

1.2 核心思想

策略模式的核心思想是:"封装变化"。把会变化的部分(算法/行为)从不变的部分(使用算法的代码)中分离出来,封装成独立的类。这些类实现同一个接口,因此可以互相替换。

这样,当需要新增或修改算法时,只需要新增或修改对应的策略类,不需要修改使用算法的代码。客户端可以在运行时动态选择使用哪个策略。

1.3 三个核心角色

策略模式包含三个关键角色:

  1. 抽象策略(Strategy):定义了所有具体策略必须实现的接口,声明了算法的方法
  2. 具体策略(Concrete Strategy):实现了抽象策略接口,包含具体的算法逻辑
  3. 上下文(Context):持有一个策略对象的引用,负责将客户端的请求委托给具体的策略对象执行。它还可以提供一些通用的逻辑,或者在调用策略前后做一些预处理和后处理

二、标准策略模式实现

2.1 UML类图

2.2 C++实现(出行方式例子)

我们就用开头提到的出行方式例子来实现策略模式。

cpp 复制代码
#include <iostream>
#include <string>
#include <memory>

// 抽象策略:出行策略
class TravelStrategy {
public:
    virtual ~TravelStrategy() = default;
    virtual void travel() const = 0; // 出行算法
};

// 具体策略1:骑自行车
class BikeStrategy : public TravelStrategy {
public:
    void travel() const override {
        std::cout << "🚲 骑自行车去上班" << std::endl;
        std::cout << "   优点:环保、健身、不堵车" << std::endl;
        std::cout << "   耗时:约30分钟" << std::endl;
    }
};

// 具体策略2:坐地铁
class SubwayStrategy : public TravelStrategy {
public:
    void travel() const override {
        std::cout << "🚇 坐地铁去上班" << std::endl;
        std::cout << "   优点:快速、便宜、准时" << std::endl;
        std::cout << "   耗时:约20分钟" << std::endl;
    }
};

// 具体策略3:打车
class TaxiStrategy : public TravelStrategy {
public:
    void travel() const override {
        std::cout << "🚕 打车去上班" << std::endl;
        std::cout << "   优点:舒适、门到门" << std::endl;
        std::cout << "   耗时:约15分钟(不堵车)" << std::endl;
    }
};

// 具体策略4:开车
class CarStrategy : public TravelStrategy {
public:
    void travel() const override {
        std::cout << "🚗 开车去上班" << std::endl;
        std::cout << "   优点:灵活、可以带东西" << std::endl;
        std::cout << "   耗时:约25分钟(视路况而定)" << std::endl;
    }
};

// 上下文:出行上下文
class TravelContext {
private:
    std::unique_ptr<TravelStrategy> strategy_; // 持有策略对象

public:
    // 构造函数可以接收一个默认策略
    explicit TravelContext(std::unique_ptr<TravelStrategy> strategy = nullptr)
        : strategy_(std::move(strategy)) {}

    // 动态设置策略
    void setStrategy(std::unique_ptr<TravelStrategy> strategy) {
        strategy_ = std::move(strategy);
    }

    // 执行出行
    void goToWork() const {
        if (!strategy_) {
            std::cout << "❌ 没有设置出行策略!" << std::endl;
            return;
        }
        std::cout << "准备去上班..." << std::endl;
        strategy_->travel(); // 委托给具体策略执行
        std::cout << "到达公司!" << std::endl;
    }
};

// 客户端代码
int main() {
    TravelContext context;

    std::cout << "=== 周一:天气好,骑自行车 ===" << std::endl;
    context.setStrategy(std::make_unique<BikeStrategy>());
    context.goToWork();

    std::cout << "\n=== 周二:起晚了,打车 ===" << std::endl;
    context.setStrategy(std::make_unique<TaxiStrategy>());
    context.goToWork();

    std::cout << "\n=== 周三:正常上班,坐地铁 ===" << std::endl;
    context.setStrategy(std::make_unique<SubwayStrategy>());
    context.goToWork();

    std::cout << "\n=== 周四:带了很多东西,开车 ===" << std::endl;
    context.setStrategy(std::make_unique<CarStrategy>());
    context.goToWork();

    return 0;
}

2.3 运行结果

复制代码
=== 周一:天气好,骑自行车 ===
准备去上班...
🚲 骑自行车去上班
   优点:环保、健身、不堵车
   耗时:约30分钟
到达公司!

=== 周二:起晚了,打车 ===
准备去上班...
🚕 打车去上班
   优点:舒适、门到门
   耗时:约15分钟(不堵车)
到达公司!

=== 周三:正常上班,坐地铁 ===
准备去上班...
🚇 坐地铁去上班
   优点:快速、便宜、准时
   耗时:约20分钟
到达公司!

=== 周四:带了很多东西,开车 ===
准备去上班...
🚗 开车去上班
   优点:灵活、可以带东西
   耗时:约25分钟(视路况而定)
到达公司!

2.4 代码解析

  • 抽象策略TravelStrategy :定义了所有出行策略必须实现的travel()方法
  • 具体策略BikeStrategySubwayStrategyTaxiStrategyCarStrategy,每个都实现了自己的出行逻辑
  • 上下文TravelContext :持有一个策略对象的引用,提供了setStrategy()方法可以动态更换策略,goToWork()方法将实际的出行逻辑委托给策略对象执行

关键优势

  • 新增一种出行方式,只需要新增一个具体策略类,不需要修改任何现有代码
  • 可以在运行时动态切换策略
  • 每个策略类职责单一,易于维护和测试
  • 策略可以在多个地方复用

三、策略模式的进阶用法

3.1 带参数的策略

有时候策略需要接收一些参数来执行算法。我们可以在策略方法中添加参数:

cpp 复制代码
// 抽象策略:折扣策略
class DiscountStrategy {
public:
    virtual ~DiscountStrategy() = default;
    // 计算折扣后的价格,接收原价作为参数
    virtual double calculate(double originalPrice) const = 0;
};

// 具体策略:无折扣
class NoDiscount : public DiscountStrategy {
public:
    double calculate(double originalPrice) const override {
        return originalPrice;
    }
};

// 具体策略:百分比折扣
class PercentageDiscount : public DiscountStrategy {
private:
    double percentage_; // 折扣百分比,比如0.8表示8折

public:
    explicit PercentageDiscount(double percentage) 
        : percentage_(percentage) {}

    double calculate(double originalPrice) const override {
        return originalPrice * percentage_;
    }
};

// 具体策略:满减折扣
class FullReductionDiscount : public DiscountStrategy {
private:
    double full_; // 满多少
    double reduction_; // 减多少

public:
    FullReductionDiscount(double full, double reduction)
        : full_(full), reduction_(reduction) {}

    double calculate(double originalPrice) const override {
        if (originalPrice >= full_) {
            return originalPrice - reduction_;
        }
        return originalPrice;
    }
};

// 上下文:购物车
class ShoppingCart {
private:
    std::unique_ptr<DiscountStrategy> discount_strategy_;
    double total_ = 0.0;

public:
    void setDiscountStrategy(std::unique_ptr<DiscountStrategy> strategy) {
        discount_strategy_ = std::move(strategy);
    }

    void addItem(double price) {
        total_ += price;
    }

    double checkout() const {
        if (!discount_strategy_) {
            return total_;
        }
        return discount_strategy_->calculate(total_);
    }
};

// 客户端代码
int main() {
    ShoppingCart cart;
    cart.addItem(100.0);
    cart.addItem(200.0);
    cart.addItem(300.0);
    std::cout << "原价: " << cart.checkout() << "元" << std::endl;

    cart.setDiscountStrategy(std::make_unique<PercentageDiscount>(0.8));
    std::cout << "8折后: " << cart.checkout() << "元" << std::endl;

    cart.setDiscountStrategy(std::make_unique<FullReductionDiscount>(500.0, 100.0));
    std::cout << "满500减100后: " << cart.checkout() << "元" << std::endl;

    return 0;
}

3.2 上下文添加通用逻辑

上下文不仅可以委托策略执行算法,还可以在调用策略前后添加一些通用的逻辑,比如日志记录、性能统计、参数校验等:

cpp 复制代码
class TravelContext {
private:
    std::unique_ptr<TravelStrategy> strategy_;

public:
    // ... 其他方法不变

    void goToWork() const {
        if (!strategy_) {
            std::cout << "❌ 没有设置出行策略!" << std::endl;
            return;
        }

        // 预处理:记录开始时间
        auto start = std::chrono::high_resolution_clock::now();
        
        std::cout << "准备去上班..." << std::endl;
        strategy_->travel(); // 委托给具体策略执行
        std::cout << "到达公司!" << std::endl;

        // 后处理:记录耗时
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
        std::cout << "本次出行耗时: " << duration.count() << "ms" << std::endl;
    }
};

这种方式非常优雅,通用逻辑只需要在上下文中写一次,所有策略都能受益。


四、策略模式的优缺点

4.1 优点

  1. 算法可以自由切换:可以在运行时动态更换策略
  2. 避免使用多重条件判断 :消除了大量的if-elseswitch-case语句
  3. 扩展性好:新增策略只需要新增一个类,符合开闭原则
  4. 符合单一职责原则:每个策略类只负责一个算法,易于维护和测试
  5. 算法复用性高:策略可以在多个不同的上下文和客户端中复用
  6. 隔离算法细节:客户端不需要知道算法的具体实现细节,只需要知道策略的接口

4.2 缺点

  1. 策略类数量增多:每个算法都需要一个单独的类,会导致系统中类的数量增加
  2. 客户端需要了解所有策略:客户端必须知道所有的具体策略类,才能选择合适的策略
  3. 所有策略都需要暴露:抽象策略接口必须声明所有策略需要的方法,即使有些策略不需要某些方法
  4. 不适合算法差异过大的情况:如果不同策略的算法差异非常大,抽象策略接口可能会变得臃肿

五、适用场景

策略模式特别适合以下场景:

  1. 多个类只有算法不同的情况:比如不同的排序算法、不同的加密算法、不同的压缩算法
  2. 需要动态切换算法的情况:比如根据不同的用户等级使用不同的折扣策略,根据不同的网络环境使用不同的传输策略
  3. 需要屏蔽算法具体实现细节的情况:让客户端只关心算法的接口,不关心实现
  4. 多重条件判断的情况 :当代码中出现大量的if-elseswitch-case来选择不同的算法时
  5. 算法需要独立于客户端变化的情况:算法的修改不应该影响到使用算法的客户端

经典应用案例

  • 支付系统:支持微信支付、支付宝、银行卡等多种支付方式
  • 电商系统:多种折扣策略(满减、折扣、立减、赠品等)
  • 游戏开发:不同的AI行为策略(进攻、防守、逃跑等)
  • 日志系统:不同的日志输出方式(控制台、文件、数据库、网络等)
  • 排序算法:根据数据量大小选择不同的排序算法

六、与其他模式的对比

很多人容易把策略模式和其他一些设计模式混淆,这里做一个清晰的对比:

模式 核心目的 与策略模式的区别
策略模式 封装算法,使算法可以互相替换 关注行为的替换,算法之间是平等的,可以任意切换
简单工厂模式 创建对象 关注对象的创建,创建后对象的行为是固定的
状态模式 封装状态,根据状态改变行为 状态之间有转换关系,行为的改变是由状态驱动的
装饰器模式 动态给对象添加功能 不改变接口,增强功能,支持多层嵌套
模板方法模式 定义算法的骨架,子类实现具体步骤 算法的骨架是固定的,只有部分步骤可以变化

最容易混淆的是策略模式和状态模式,它们的核心区别在于:

  • 策略模式:客户端主动选择使用哪个策略,策略之间没有依赖关系
  • 状态模式:状态的改变是自动的,由上下文内部的条件触发,状态之间有转换关系

七、现代C++改进建议

在现代C++(C++11及以后)中,我们可以对策略模式进行一些改进,让代码更加简洁和灵活。

7.1 使用std::function和Lambda简化策略

对于只有一个方法的简单策略,我们可以使用std::function和Lambda表达式来代替抽象策略类和具体策略类,代码会非常简洁:

cpp 复制代码
#include <functional>

// 上下文:出行上下文
class TravelContext {
private:
    // 使用std::function作为策略类型
    using TravelStrategy = std::function<void()>;
    TravelStrategy strategy_;

public:
    void setStrategy(TravelStrategy strategy) {
        strategy_ = std::move(strategy);
    }

    void goToWork() const {
        if (!strategy_) {
            std::cout << "❌ 没有设置出行策略!" << std::endl;
            return;
        }
        std::cout << "准备去上班..." << std::endl;
        strategy_(); // 调用函数对象
        std::cout << "到达公司!" << std::endl;
    }
};

// 客户端代码
int main() {
    TravelContext context;

    // 直接使用Lambda表达式作为策略
    context.setStrategy([]() {
        std::cout << "🚲 骑自行车去上班" << std::endl;
        std::cout << "   优点:环保、健身、不堵车" << std::endl;
    });
    context.goToWork();

    context.setStrategy([]() {
        std::cout << "🚇 坐地铁去上班" << std::endl;
        std::cout << "   优点:快速、便宜、准时" << std::endl;
    });
    context.goToWork();

    return 0;
}

这种方式非常适合简单的策略场景,不需要定义任何类,直接使用Lambda表达式即可。

7.2 使用模板实现通用上下文

我们可以使用模板来实现一个通用的上下文类,适用于任何策略接口:

cpp 复制代码
template <typename Strategy>
class Context {
private:
    std::unique_ptr<Strategy> strategy_;

public:
    explicit Context(std::unique_ptr<Strategy> strategy = nullptr)
        : strategy_(std::move(strategy)) {}

    void setStrategy(std::unique_ptr<Strategy> strategy) {
        strategy_ = std::move(strategy);
    }

    template <typename... Args>
    auto execute(Args&&... args) const {
        if (!strategy_) {
            throw std::runtime_error("No strategy set");
        }
        return strategy_->execute(std::forward<Args>(args)...);
    }
};

// 使用
class MyStrategy {
public:
    virtual int execute(int a, int b) const = 0;
};

class AddStrategy : public MyStrategy {
public:
    int execute(int a, int b) const override {
        return a + b;
    }
};

int main() {
    Context<MyStrategy> context;
    context.setStrategy(std::make_unique<AddStrategy>());
    std::cout << context.execute(1, 2) << std::endl; // 输出3
    return 0;
}

7.3 结合工厂模式创建策略

为了解决客户端需要知道所有具体策略类的问题,我们可以结合工厂模式来创建策略对象。客户端只需要传入一个策略标识,工厂就会创建对应的策略对象:

cpp 复制代码
// 策略工厂
class StrategyFactory {
public:
    static std::unique_ptr<TravelStrategy> createStrategy(const std::string& type) {
        if (type == "bike") {
            return std::make_unique<BikeStrategy>();
        } else if (type == "subway") {
            return std::make_unique<SubwayStrategy>();
        } else if (type == "taxi") {
            return std::make_unique<TaxiStrategy>();
        } else if (type == "car") {
            return std::make_unique<CarStrategy>();
        } else {
            return nullptr;
        }
    }
};

// 客户端代码
int main() {
    TravelContext context;
    context.setStrategy(StrategyFactory::createStrategy("bike"));
    context.goToWork();

    context.setStrategy(StrategyFactory::createStrategy("taxi"));
    context.goToWork();

    return 0;
}

这样客户端就不需要知道任何具体的策略类,只需要知道策略的标识即可。


八、实际应用中的注意事项

  1. 策略数量不宜过多:如果策略数量超过5-6个,可能会导致系统复杂度增加。这时候可以考虑使用享元模式来共享策略对象,或者使用其他模式
  2. 策略应该是无状态的:策略对象不应该持有自己的状态,这样可以在多个上下文之间共享同一个策略对象,提高性能
  3. 正确处理空策略:上下文应该处理策略为空的情况,避免空指针异常
  4. 考虑策略的优先级:如果多个策略可以同时适用,需要考虑策略的优先级问题
  5. 避免过度使用:如果算法非常简单且不会变化,就没有必要使用策略模式,直接写在代码里即可

九、总结

策略模式是一种非常实用的设计模式,它的核心思想是**"封装变化,委托执行"**。通过将算法封装成独立的策略类,我们可以实现算法的自由切换,消除多重条件判断,提高代码的扩展性和可维护性。

在实际开发中,当你发现代码中有大量的if-elseswitch-case语句来选择不同的算法时,就应该考虑使用策略模式来重构。结合工厂模式和现代C++的std::function、Lambda表达式,可以让策略模式的实现更加简洁和优雅。

记住,设计模式不是银弹。只有当算法确实会变化,并且需要动态切换时,才应该使用策略模式。如果算法是固定的,不会变化,那么直接写在代码里会更简单。

希望这篇文章能帮助你彻底理解策略模式,并在实际项目中正确地使用它。

相关推荐
Je1lyfish1 小时前
CMU15-445 (2025 Fall/2026 Spring) Project#4 - Concurrency Control
开发语言·数据库·c++·笔记·后端·算法·系统架构
mjhcsp2 小时前
C++ 单位根反演(Roots of Unity Filter)全解析
开发语言·c++
thisiszdy2 小时前
<设计模式> 生产者-消费者模式
设计模式
cany10002 小时前
C++进阶 -- std::deque‌ 和 ‌std::list
开发语言·c++
2301_789015622 小时前
Lnux权限
linux·开发语言·c++·权限
刀法如飞11 小时前
AI时代:DDD领域驱动建模与Ontology语义建模的区别
java·设计模式·架构
feng_you_ying_li16 小时前
C++复习二,继承与多态
c++
小小de风呀16 小时前
de风——【从零开始学C++】(十一):list的基本使用和模拟实现
开发语言·c++·list
陌路2016 小时前
C++高级进阶--夯实进阶基础(1)
开发语言·c++