40. 设计模式面试题5:实现一个策略模式,支持运行时切换算法

策略模式,说白了就是「把算法封装起来,让它们可以互相替换」。我面试的时候特别喜欢问这个模式,因为它能很直观地考察一个人对「开闭原则」的理解深度。

你想想看,如果代码里到处都是 if-else 或者 switch-case,每次加一个新算法就要改核心逻辑,那维护起来得多痛苦?策略模式就是来解决这个问题的。

什么是策略模式?

策略模式定义了一系列算法,把它们一个个封装起来,并且使它们可以相互替换。核心思想是:将算法的定义和使用分离

我记得有一次接手一个老项目,里面有个计算折扣的模块,写了十几个 if-else 分支。每次促销活动都要改这个函数,改一次线上出一次 bug。后来我重构成了策略模式,新加折扣只需要新增一个策略类,完全不用动原有代码。

核心三要素:
  • 策略接口(Strategy):定义所有算法的公共接口
  • 具体策略(ConcreteStrategy):实现具体的算法逻辑
  • 上下文(Context):持有策略对象的引用,负责调用策略

运行时切换的关键

为什么能运行时切换?因为上下文里存的是策略接口的指针或引用。你只需要在运行时改变这个指针指向的具体策略对象,行为就立刻变了。

这里有个坑——我曾经见过有人把策略对象直接嵌在上下文里,用 union 或者 std::variant 来切换。嗯,这样做虽然也能工作,但每加一个新策略就要改上下文类的定义,违背了开闭原则。我个人习惯还是用多态,虽然有一点点运行时开销,但扩展性好了不止一个量级。

代码示例:一个支付系统

假设我们要实现一个支付系统,支持支付宝、微信支付和银行卡支付,并且允许用户在运行时切换支付方式。

// 策略接口
class PaymentStrategy {
public:
    virtual ~PaymentStrategy() = default;
    virtual void pay(double amount) = 0;
    virtual std::string getMethodName() const = 0;
};

// 具体策略:支付宝
class AlipayStrategy : public PaymentStrategy {
public:
    void pay(double amount) override {
        std::cout << "使用支付宝支付: " << amount << " 元" << std::endl;
        // 实际的支付宝支付逻辑...
    }
    std::string getMethodName() const override {
        return "支付宝";
    }
};

// 具体策略:微信支付
class WechatPayStrategy : public PaymentStrategy {
public:
    void pay(double amount) override {
        std::cout << "使用微信支付: " << amount << " 元" << std::endl;
        // 实际的微信支付逻辑...
    }
    std::string getMethodName() const override {
        return "微信支付";
    }
};

// 具体策略:银行卡
class CardPayStrategy : public PaymentStrategy {
public:
    void pay(double amount) override {
        std::cout << "使用银行卡支付: " << amount << " 元" << std::endl;
        // 实际的银行卡支付逻辑...
    }
    std::string getMethodName() const override {
        return "银行卡";
    }
};

// 上下文:支付上下文
class PaymentContext {
private:
    std::unique_ptr<PaymentStrategy> strategy_;
public:
    // 运行时切换策略
    void setStrategy(std::unique_ptr<PaymentStrategy> strategy) {
        strategy_ = std::move(strategy);
        std::cout << "已切换至: " << strategy_->getMethodName() << std::endl;
    }

    void executePayment(double amount) {
        if (!strategy_) {
            throw std::runtime_error("请先设置支付方式");
        }
        strategy_->pay(amount);
    }
};

使用示例

int main() {
    PaymentContext context;

    // 用户选择支付宝支付
    context.setStrategy(std::make_unique<AlipayStrategy>());
    context.executePayment(99.9);

    // 用户切换为微信支付
    context.setStrategy(std::make_unique<WechatPayStrategy>());
    context.executePayment(199.9);

    // 用户又切换为银行卡
    context.setStrategy(std::make_unique<CardPayStrategy>());
    context.executePayment(299.9);

    return 0;
}

策略模式的结构图

<<interface>> PaymentStrategy AlipayStrategy WechatPayStrategy CardPayStrategy PaymentContext - strategy: PaymentStrategy* 图例: 继承/实现 依赖/关联

策略模式的优缺点

优点 缺点
  • 符合开闭原则,新增策略无需修改上下文
  • 避免大量的条件判断语句
  • 支持运行时动态切换算法
  • 算法可以独立测试和复用
  • 策略类数量会增加,需要管理多个类
  • 客户端必须了解不同策略的区别
  • 策略对象之间的通信成本
小提示:如果策略类数量不多且相对稳定,可以考虑用函数指针或 std::function 替代多态,减少类数量。但一旦策略数量可能增长,还是老老实实用多态吧。
避坑指南:我曾经见过有人把策略模式用成了「策略反模式」——上下文里存了策略指针,但又在上下文里写 if-else 来判断该调用哪个策略。那还不如直接用 if-else 呢!策略模式的核心是「把判断逻辑交给客户端」,上下文只负责调用,不负责选择。

什么时候用策略模式?

我个人总结了几条经验:

  • 一个系统需要在多种算法中选择一种,且这些算法可以互相替换
  • 算法逻辑复杂,不适合放在一个函数里用 if-else 堆砌
  • 算法可能会扩展,你希望未来的改动不影响现有代码
  • 需要运行时动态改变行为

说白了,如果你发现代码里有一长串 if-else 或者 switch-case,而且每个分支做的事情差异很大,那就是策略模式出场的时候了。

嗯,面试的时候我还会追问一个问题:「策略模式和状态模式有什么区别?」这个问题留给你自己思考。简单提示一下:策略模式是客户端主动选择算法,状态模式是对象内部状态自动驱动行为变化。想清楚这个区别,你对这两个模式的理解就更深一层了。

专注资料整理