24.策略模式实现
02.策略模式实现
策略模式的优缺点:
优点:
-
避免多重的条件判断
-
易扩展
缺点:
-
代码量增多
-
策略实现需要对外暴露
可以先看看如果没有使用策略模式,那我们在遇到扩展时是怎么处理的。就拿支付来举例吧。伪代码如下:
std::string doPay(const PayParam& payParam) { // 先验 preCheck(payParam); std::string result; if (payParam.getPayChannelEnum() == PayChannelEnum::ALI) { // 支付宝支付的一些校验和处理 result = doAli(payParam); } else if (payParam.getPayChannelEnum() == PayChannelEnum::WECHAT) { // 微信支付的一些校验和处理 result = doWechat(payParam); } else { // 其他情况的处理 result.clear(); } // 结果处理 result = resultHandle(result); return result;}这个时候如果又加上了银联支付,那么就得在这个if else的分支上进行修改,再加一个分支,如果我们的处理不只是这么简单的话,甚至还涉及到代码结构的调整,先验和结果处理调整,这不符合代码封装的开闭原则。而且也会改得很令人头痛。
1.策略实现
使用策略模式实现也是有好几种方法的,下面我就来讲讲我在真实场景中使用过的策略实现吧。
2.第一种
这种方式非常简单,简直就是上面的if else套了个封装的壳子而已。定义一个接口,然后接口有几个实现,在根据if else判断需要构造哪个实现。然后调用执行方法。
// 策略接口类,用于抽象支付的行为class PayStrategy {public: virtual ~PayStrategy() = default; virtual std::string doPay(const PayParam& payParam) = 0;};
// 具体支付宝支付的实现class AliPayStrategyImpl : public PayStrategy {public: std::string doPay(const PayParam& payParam) override { return "alipay"; }};
// 具体微信支付的实现class WechatPayStrategyImpl : public PayStrategy {public: std::string doPay(const PayParam& payParam) override { return "wechatpay"; }};
// 调用支付的入口class OrderPay {public: std::string doPay(const PayParam& payParam) { // 先验 preCheck(payParam); std::unique_ptr<PayStrategy> payStrategy; if (payParam.getPayChannelEnum() == PayChannelEnum::ALI) { payStrategy = std::make_unique<AliPayStrategyImpl>(); } else if (payParam.getPayChannelEnum() == PayChannelEnum::WECHAT) { payStrategy = std::make_unique<WechatPayStrategyImpl>(); } std::string result = payStrategy ? payStrategy->doPay(payParam) : ""; // 结果处理 result = resultHandle(result); return result; }
private: void preCheck(const PayParam& payParam) {
}
std::string resultHandle(const std::string& result) { // do something return "result"; }};这种如果需要扩展的话,就是再加一个实现类,然后if else这里构造实现类的地方再增加一个分支,其实也没有完全解决掉对修改关闭的问题。当然如果将实例交给统一的容器管理的话又会方便很多。
3.第二种(推荐)
这种就比较有趣了,是使用枚举类来进行多策略的实现。下面我们就一起来看看吧。
// C++ 的枚举无法像 Java 那样让每个枚举常量各自实现方法,// 惯用做法是枚举配合一个分派函数来模拟enum class PayChannelEnum { ALI, WECHAT};
std::string doPay(PayChannelEnum channel, const PayParam& payParam) { switch (channel) { case PayChannelEnum::ALI: return "alipay"; case PayChannelEnum::WECHAT: return "wechatpay"; default: return ""; }}这种方式,代码看着很简洁,逻辑也很清晰,使用也很方便;但是有一个缺点,如果你需要用到外部注入的对象的话,这种方式就无法工作了。
4.第三种
接下来这种就需要依赖于一个统一的注册表了,它大致就是帮我们管理实现类的,类的构造和注入等。
// 策略接口类,用于抽象支付的行为。依然需要一个策略接口用来抽象支付的行为class PayStrategy {public: virtual ~PayStrategy() = default; virtual std::string doPay(const PayParam& payParam) = 0;};
// 具体支付宝支付的实现// 重点:注册时需要指定实例的名称class AliPayStrategyImpl : public PayStrategy {public: std::string doPay(const PayParam& payParam) override { return "alipay"; }};
// 具体微信支付的实现// 重点:注册时需要指定实例的名称class WechatPayStrategyImpl : public PayStrategy {public: std::string doPay(const PayParam& payParam) override { return "wechatpay"; }};
// 调用支付的入口class OrderPay {public: OrderPay() { // 重点:构造时把各策略实现注册进 map,key 为实例名,value 为对应的实例 payStrategyMap["ALI"] = std::make_unique<AliPayStrategyImpl>(); payStrategyMap["WECHAT"] = std::make_unique<WechatPayStrategyImpl>(); }
std::string doPay(const PayParam& payParam) { preCheck(payParam); auto it = payStrategyMap.find(payParam.getPayChannelName()); std::string result = it != payStrategyMap.end() ? it->second->doPay(payParam) : ""; return resultHandle(result); }
private: void preCheck(const PayParam& payParam) { // do something check }
std::string resultHandle(const std::string& result) { // do something return "result"; }
std::unordered_map<std::string, std::unique_ptr<PayStrategy>> payStrategyMap;};当然这里我们是没有考虑那些异常情况,以及策略未找到之类的兜底或者处理等。实际编程是需要都考虑的。
可以看到在这段实现中我是标了几个重点。
-
首先是各自的实现类需要指定注册名称(策略对应的枚举名称),这是为了在注册表中查找的时候方便我们匹配用的
-
其次是在调用的入口OrderPay的构造函数里统一完成注册,这样才能把策略实现统一管理起来,调用时才能找到对应的策略实现
-
payStrategyMap这个成员对应的是个map,key为实例名,value为对应的实例。这样在调用doPay时就可以根据传入的策略名找到实例然后执行具体方法。
缺点也很明显,需要指定实例名称,且注册时的key要与策略名保持一致。这样容易出现两个问题。
- 实例名称如果和其他实例冲突,后注册的会覆盖先注册的,那换个实例名称的话,调用方也得跟着改动,容易引起其他依赖的问题。
- 策略名和实例名称需要一一对应,如果有哪里大小写或者字符敲错了等问题,不太容易排查。
5.第四种(推荐)
这种目前是我常用的一种策略实现,不过呢,如果策略本身比较少的话,写这个轮子可能会觉得比较浪费,因为本身的架子代码就挺多的了。话不多说,show you the code。
// 很熟悉了吧?一定需要的一个策略接口class PayStrategy {public: virtual ~PayStrategy() = default; virtual std::string doPay(const PayParam& payParam) = 0;};
// 重要。实现策略的一个抽象类。用于定义所有的策略实现的基类class AbstractPayStrategy : public PayStrategy {public: // 重要。新定义一个策略实现类需要实现的策略。返回对应的枚举 virtual PayChannelEnum strategy() const = 0;};
// 基础实现,注意,此处是实现顶上的策略接口。在调用入口地方也是注入此实现。// 重要。多个实现时调用方只依赖这个统一入口,由它来分派具体策略class PayStrategyImpl : public PayStrategy {public: // 构造器注入。然后转化成map存储。 explicit PayStrategyImpl(std::vector<std::unique_ptr<AbstractPayStrategy>> payStrategyList) { for (auto& payStrategy : payStrategyList) { payStrategies[payStrategy->strategy()] = std::move(payStrategy); } }
// 实际的支付接口都是调用此方法 std::string doPay(const PayParam& payParam) override { return payStrategies[payParam.getPayChannelEnum()]->doPay(payParam); }
private: std::map<PayChannelEnum, std::unique_ptr<AbstractPayStrategy>> payStrategies;};
// 注意这里的改动,此处是继承抽象类,并且实现返回策略枚举的接口class AliPayStrategyImpl : public AbstractPayStrategy {public: std::string doPay(const PayParam& payParam) override { return "alipay"; }
PayChannelEnum strategy() const override { return PayChannelEnum::ALI; }};
class WechatPayStrategyImpl : public AbstractPayStrategy {public: std::string doPay(const PayParam& payParam) override { return "wechatpay"; }
PayChannelEnum strategy() const override { return PayChannelEnum::WECHAT; }};到此整体的架子就已经搭建好了,而具体的调用入口也是很简单的一个注入即可。
// 调用入口:构造时把所有策略实现一并"注入"进去PayStrategyImpl payStrategy({std::make_unique<AliPayStrategyImpl>(), std::make_unique<WechatPayStrategyImpl>()});这种实现代码量很多,但是基本上模糊了选择策略的那部分逻辑,而且有个很重要的优点是只需要维护一个策略枚举,能很方便的将实现与策略枚举映射起来。
其中用到的知识点有:
构造器注入。PayStrategyImpl定义了一个vector参数的构造函数,实例化该类时把所有的策略实现统一传入进去即可 统一入口。当接口有多个实现时,调用方不直接依赖某个具体实现,而是只依赖PayStrategyImpl这个统一入口,由它来分派具体策略。 类图如下:

6.最后
关于策略模式的多种实现,到这里就算结束了。也是有写几个我平常有使用的策略模式实现,当然还有一些其实可以改造一下形成自己代码风格的实现方式,比如第三种,可以再定义一个全局的Map,在实现类构造时将自身注册到Map中(可以通过构造函数自注册的方式等),然后剩下的工作就水到渠成了。
每种实现其实都是有一些优缺点的,也并不一定适用于所有人和所有场景,希望大家能够找到符合自己代码风格的实现思路,也希望大家能够通过这一节对策略模式有一个清晰一点的认识,在实际工作中能够更简洁清晰的写自己的bug咯~。