18、从OCP到策略模式:策略模式如何实现开闭原则,策略的注册与动态切换

开闭原则(OCP)说起来简单——对扩展开放,对修改关闭。但真正落地的时候,很多人就懵了。

我记得刚带团队那会儿,有个同事写了一个支付模块。一开始只支持微信支付,后来要加支付宝,他就往原来的类里塞if-else。再后来要加银联、加花呗……那个类膨胀到两千多行。每次改完都提心吊胆,生怕改崩了哪条支付链路。

这其实就是典型的违反OCP。那怎么解决?策略模式就是答案之一。

什么是策略模式

策略模式,说白了就是把「算法」或者「行为」抽出来,封装成独立的策略类。然后让上下文(Context)去组合这些策略,而不是自己去实现。

举个例子:

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

// 具体策略
class WeChatPay : public PaymentStrategy {
public:
    void pay(double amount) override {
        std::cout << "微信支付:" << amount << "元" << std::endl;
    }
};

class AliPay : public PaymentStrategy {
public:
    void pay(double amount) override {
        std::cout << "支付宝支付:" << amount << "元" << std::endl;
    }
};

// 上下文
class PaymentContext {
private:
    std::unique_ptr<PaymentStrategy> strategy_;
public:
    void setStrategy(std::unique_ptr<PaymentStrategy> strategy) {
        strategy_ = std::move(strategy);
    }
    void executePayment(double amount) {
        if (strategy_) strategy_->pay(amount);
    }
};

你看,现在要加一种新的支付方式,只需要新增一个策略类,完全不用改PaymentContext。这就是对修改关闭,对扩展开放。

策略模式如何实现OCP

我习惯从三个维度来看这个问题:

  • 接口稳定:策略接口一旦定义好,就不应该频繁改动。这是OCP的基石。
  • 实现可替换:新增策略不影响已有策略,也不影响上下文。
  • 组合优于继承:上下文持有策略接口的指针,而不是继承具体策略。这样运行时可以动态切换。

你想想看,如果不用策略模式,每次新增一个支付方式,你都得去改那个两千行的类。改着改着,bug就来了。我在项目中遇到过好几次,就是因为这种「小改动」引发的连锁故障。

策略的注册机制

光有策略模式还不够。实际项目中,策略往往是动态注册的。你不能每次加个策略就去改代码吧?

我常用的做法是搞一个策略注册表:

class PaymentStrategyRegistry {
private:
    std::unordered_map<std::string, std::function<std::unique_ptr<PaymentStrategy>()>> factories_;
public:
    void registerStrategy(const std::string& name, 
                          std::function<std::unique_ptr<PaymentStrategy>()> factory) {
        factories_[name] = factory;
    }
    std::unique_ptr<PaymentStrategy> create(const std::string& name) {
        auto it = factories_.find(name);
        if (it != factories_.end()) {
            return it->second();
        }
        return nullptr;
    }
};

这样,新增策略只需要在初始化阶段注册一下:

registry.registerStrategy("wechat", []() { return std::make_unique<WeChatPay>(); });
registry.registerStrategy("alipay", []() { return std::make_unique<AliPay>(); });

嗯,这里要注意:注册的时机要把握好。我一般放在程序启动阶段,或者模块加载的时候。千万别在运行时动态注册,容易出并发问题。

策略的动态切换

动态切换是策略模式的杀手锏。运行时根据条件换策略,不用重启,不用改代码。

举个例子,电商大促的时候,不同的用户等级享受不同的折扣策略:

class DiscountStrategy {
public:
    virtual ~DiscountStrategy() = default;
    virtual double applyDiscount(double price) = 0;
};

class VipDiscount : public DiscountStrategy {
public:
    double applyDiscount(double price) override {
        return price * 0.8; // 8折
    }
};

class NormalDiscount : public DiscountStrategy {
public:
    double applyDiscount(double price) override {
        return price * 0.95; // 95折
    }
};

// 运行时切换
void checkout(User& user, double price) {
    std::unique_ptr<DiscountStrategy> strategy;
    if (user.isVip()) {
        strategy = std::make_unique<VipDiscount>();
    } else {
        strategy = std::make_unique<NormalDiscount>();
    }
    double finalPrice = strategy->applyDiscount(price);
    // 处理支付...
}

我曾经在一个金融项目中,用策略模式实现了多种计息方式。客户要求能随时切换计息规则,而且不能停机。策略模式配合配置中心,完美解决了这个问题。

核心要点:策略模式让OCP从理论变成了可落地的实践。关键在于把变化点封装成策略接口,然后通过组合和注册机制实现扩展。

避坑指南

策略模式虽好,但也不是银弹。我踩过几个坑,分享给你:

  • 策略过多:如果策略类太多,管理起来很麻烦。可以考虑用模板或者函数对象来减少类数量。
  • 策略间有依赖:有些策略需要共享状态,这时候要小心。我建议用上下文来管理共享数据,而不是让策略之间直接耦合。
  • 性能问题:如果策略切换非常频繁,虚函数调用会有开销。可以用模板策略或者编译期多态来优化。

警告:千万不要为了用模式而用模式。如果只有两三种策略,而且几乎不变,那用if-else反而更清晰。策略模式是为「频繁变化」和「动态扩展」准备的。

策略模式的核心逻辑图

下面这张图展示了策略模式的核心结构,以及它如何支撑OCP:

策略模式与OCP核心逻辑 Context(上下文) Strategy(策略接口) ConcreteStrategyA ConcreteStrategyB ConcreteStrategyC 策略注册表(Registry) 组合 注册 注册 新增策略只需实现接口并注册,无需修改上下文

从图上可以看得很清楚:上下文只依赖策略接口,具体策略通过注册表动态加入。新增策略时,上下文和已有策略都不需要改。这就是OCP的完美体现。

小技巧:在实际项目中,我习惯把策略注册和配置中心结合起来。策略的开关、参数都可以通过配置动态调整,这样连代码都不用重新编译。

好了,关于策略模式和OCP的关系,就聊到这儿。记住一句话:策略模式不是目的,实现OCP才是。模式只是工具,用对了地方才能发挥价值。

专注资料整理