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的完美体现。
小技巧:在实际项目中,我习惯把策略注册和配置中心结合起来。策略的开关、参数都可以通过配置动态调整,这样连代码都不用重新编译。
好了,关于策略模式和OCP的关系,就聊到这儿。记住一句话:策略模式不是目的,实现OCP才是。模式只是工具,用对了地方才能发挥价值。