C++策略模式及其实现(附带实例)
策略模式用于在运行时实现动态地选择算法。策略模式将算法的实现从主体逻辑中分离出来,使算法可以独立于客户端代码进行变化和扩展。策略模式是对if-else语句和switch语句的一种更加优雅的实现方式。
策略模式的主要作用是将特定的算法封装在可互换的策略类中,并将选择合适的策略委托给客户端来决定。这样,同一种算法可以在不改变客户端代码的情况下进行切换和替换。策略模式符合开闭原则,使系统更加灵活和可扩展。
通过策略模式可以将不同的算法封装在不同的策略类中,从而使算法可以独立地变化。当需要使用不同的策略时,只需创建一个新的具体策略类,而无须修改客户端代码或环境类。
策略模式的优点包括算法的独立性和可复用性,策略可以在不同的环境中复用,可以在运行时动态切换策略。对开闭原则的支持,可以通过增加新的策略类来扩展系统功能。
然而,策略模式容易增加类的数量和复杂度,可能会导致代码的维护性变差。此外,对于只有一个或少数几个策略的简单情况,使用策略模式可能会显得过于复杂。

图 1 传统策略模式 UML 简图

图 2 C++11 模板策略模式UML简图
keyType 是用来检索方法的索引,由于使用了模板参数,所以可以适应各种不同的检索方式,但由于内部使用 unordered_map,对于不能进行 hash 的数据类型则不能使用,读者可以考虑使用其他类型容器或者使用自定义的容器来存储。
函数对象也使用了模板参数 Ret 约定返回值类型,使用 Args 约定参数类型。采用这种设计可以满足绝大多数应用场景。在开发业务代码时仅需针对模板进行全特化处理就可以满足具体使用的要求了。
实现的主要原理就是利用 std::unordered_map<> 模板类存储索引和存储函数对象,此后调用时根据索引进行调用,从而实现在满足条件时调用对应的方式执行相关操作,代码如下:
对外暴露迭代器。方法是使用 unordered_map 来存储,方便快速检索方法。unordered_map 使用哈希表来存储数据检索的时间复杂度,时间复杂度一般为 O(1);如果不考虑这段时间复杂度的要求,则可以考虑使用 map 容器来存储,代码如下:
这里需要特别注意,如果不适用模板函数的方式而是使用 Args&&... 的方式就是右值引用,代码的通用性就会大打折扣,代码如下:
策略模式的主要作用是将特定的算法封装在可互换的策略类中,并将选择合适的策略委托给客户端来决定。这样,同一种算法可以在不改变客户端代码的情况下进行切换和替换。策略模式符合开闭原则,使系统更加灵活和可扩展。
通过策略模式可以将不同的算法封装在不同的策略类中,从而使算法可以独立地变化。当需要使用不同的策略时,只需创建一个新的具体策略类,而无须修改客户端代码或环境类。
策略模式的优点包括算法的独立性和可复用性,策略可以在不同的环境中复用,可以在运行时动态切换策略。对开闭原则的支持,可以通过增加新的策略类来扩展系统功能。
然而,策略模式容易增加类的数量和复杂度,可能会导致代码的维护性变差。此外,对于只有一个或少数几个策略的简单情况,使用策略模式可能会显得过于复杂。
传统策略模式
传统策略模式的主要组成部分有环境类(Context)、抽象策略类(Strategy)和具体策略类(Concrete Strategy),如下图所示。
图 1 传统策略模式 UML 简图
- 环境类包含一个策略对象,并提供方法来执行相应的算法。环境类将请求委托给策略对象,并不关心具体的算法;
- 抽象策略类定义了策略对象的接口,在其中声明了一个或多个算法的抽象方法。所有具体策略类都必须实现这些抽象方法;
- 具体策略类实现了抽象策略类定义的算法。每个具体策略类对应一个具体的算法实现。
C++11元编程下的结构设计
本节的策略模式主要包括方法索引、方法和策略管理调度。方法索引,对应于模板参数 keyType;方法,方法内容使用函数对象来完成,函数对象使用 unordered_map 记录,如下图所示。
图 2 C++11 模板策略模式UML简图
keyType 是用来检索方法的索引,由于使用了模板参数,所以可以适应各种不同的检索方式,但由于内部使用 unordered_map,对于不能进行 hash 的数据类型则不能使用,读者可以考虑使用其他类型容器或者使用自定义的容器来存储。
函数对象也使用了模板参数 Ret 约定返回值类型,使用 Args 约定参数类型。采用这种设计可以满足绝大多数应用场景。在开发业务代码时仅需针对模板进行全特化处理就可以满足具体使用的要求了。
C++策略模式实现和解析
代码主要分成两部分实现,首先定义一个模板框架。模板参数 keyType 约定的条件参数也就是检索类型,funcType 用来定义方法类型。第二部分的模板针对方法进行精细化处理,约定了方法的返回值类型和参数表类型,并针对第 1 个模板进行特化,明确方法类型。实现的主要原理就是利用 std::unordered_map<> 模板类存储索引和存储函数对象,此后调用时根据索引进行调用,从而实现在满足条件时调用对应的方式执行相关操作,代码如下:
//designM/strategy.hpp template<typename keyType,typename funcType >struct strategy{}; template< typename keyType,typename Ret,typename...Args > class strategy< keyType,std::function< Ret(Args...) >>{ public: //对方法类型进行别名处理,方便后续模块使用,也可以方便外部使用 //方法类型定义自己的函数对象 using callee_type = std::function< Ret(Args...) >;
对外暴露迭代器。方法是使用 unordered_map 来存储,方便快速检索方法。unordered_map 使用哈希表来存储数据检索的时间复杂度,时间复杂度一般为 O(1);如果不考虑这段时间复杂度的要求,则可以考虑使用 map 容器来存储,代码如下:
using iterator = typename std::unordered_map<keyType, callee_type>::iterator; protected: // 实例化的方法表 std::unordered_map<keyType, callee_type> m_strates__; public: strategy() = default; virtual ~strategy() = default; // add 方法用来动态地添加方法,并将方法和检索索引绑定 bool add(const keyType& key, callee_type callFn) { // 方法和索引具有唯一对应的关系,在添加方法前首先检查是否已存在 auto it = m_strates__.find(key); if (it == m_strates__.end()) { // unordered_map::insert 返回 std::pair<iterator,bool> auto rst = m_strates__.insert(std::make_pair(key, callFn)); return rst.second; } return false; } // erase 方法用来删除方法 bool erase(const keyType& key) { auto it = m_strates__.find(key); if (it != m_strates__.end()) { m_strates__.erase(it); return true; } return false; } void clear() { m_strates__.clear(); } size_t count() const { return m_strates__.size(); }函数 call_each() 用来针对指定范围的或者全部的方法使用同样的参数遍历调用。这里仍然使用模板函数的方式实现是为了保证使用万能引用的方式来传递参数。
这里需要特别注意,如果不适用模板函数的方式而是使用 Args&&... 的方式就是右值引用,代码的通用性就会大打折扣,代码如下:
// 1) 对指定迭代器区间内的所有函数对象进行调用 template<typename... Params> void call_each(iterator from, iterator to, Params&&... args) { for (auto it = from; it != to; ++it) { it->second(std::forward<Params>(args)...); } } // 2) 对整张策略表中的所有函数对象进行调用 template<typename... Params> void call_each(Params&&... args) { for (auto it = m_strates__.begin(); it != m_strates__.end(); ++it) { it->second(std::forward<Params>(args)...); } } // 3) 根据 key 调用对应函数对象并返回结果 template<typename Ret, typename... Params> Ret call(const keyType& key, Params&&... args) { // 检索方法,并获取对应的迭代器 auto it = m_strates__.find(key); if (it != m_strates__.end()) { // 如果方法存在,则执行并返回结果 return it->second(std::forward<Params>(args)...); } // 方法不存在时的默认返回值 return Ret{}; }在本节中使用了 std::unordered_map 模板类来存储函数对象,这个结构的查询效率很高,但是容易出现哈希冲突的情况。在效率要求不高的情况下,读者可以将这个容器修改为 std::map,从而利用红黑树存储数据。
C++策略模式应用示例
在下面的示例中使用枚举类型 emKey 作为检索的索引,分别使用 fun1() 函数、类成员函数 myClassA::fun1() 和匿名函数 3 种方式来演示如何使用 strategy 模板类,代码如下:// strategy.cpp #include <iostream> #include <functional> #include "designM/strategy.hpp" using namespace wheels; using namespace dm; // 定义策略索引枚举 enum class emKey { EM_A, EM_B, EM_C }; // 普通函数 void fun1(int data) { std::cout << "fun1 data = " << data << std::endl; } // 类成员函数示例 struct myclassA { void fun1(int data) { std::cout << "myclassA::fun1 data = " << data << std::endl; } }; int main() { // 实例化策略模式对象 using strategy_t = strategy<emKey, std::function<void(int)>>; strategy_t strgy; myclassA class_a; // 添加策略分支:普通函数、成员函数、lambda strgy.add(emKey::EM_A, fun1); strgy.add(emKey::EM_B, std::bind(&myclassA::fun1, &class_a, std::placeholders::_1)); strgy.add(emKey::EM_C, [](int data) { std::cout << "lambda fun data = " << data << std::endl; }); // 根据 key 选择分支执行 strgy.call<void>(emKey::EM_A, 1); strgy.call<void>(emKey::EM_B, 10); strgy.call<void>(emKey::EM_C, 99); return 0; }上述示例的执行结果如下:
fun1 data =1
myClassA::fun1 data =10
lambda fun data =99