1. 什么是策略模式? 策略模式是一种行为设计模式,它定义了一系列算法,将每一个算法封装起来,并使它们可以相互替换 。此模式让算法的变化独立于使用算法的客户。
简单来说,策略模式的核心思想是:
分离 : 将算法的 定义(是什么) 与算法的 使用(何时用) 分离开来。
封装 : 将不同的算法(即“策略”)封装在各自独立的类中。
委托 : “上下文”(Context)对象不亲自执行算法,而是将任务委托给它所持有的策略对象。
GoF(《设计模式》)中的经典定义是:“Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.”
2. 为什么需要策略模式?(问题场景) 假设我们正在编写一个数据处理程序,其中一个功能是对一组数据进行排序。一开始,我们可能只实现了一种排序算法,比如冒泡排序。
一个反例:使用 if-else 或 switch
随着需求增加,我们需要支持更多的排序算法,如快速排序、归并排序等。一种常见的“坏”设计是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <iostream> #include <vector> #include <string> #include <algorithm> enum class SortType { BubbleSort, QuickSort, MergeSort }; class Sorter {public : void sort (std::vector<int >& data, SortType type) { if (type == SortType::BubbleSort) { std::cout << "Sorting using Bubble Sort" << std::endl; } else if (type == SortType::QuickSort) { std::cout << "Sorting using Quick Sort" << std::endl; } else if (type == SortType::MergeSort) { std::cout << "Sorting using Merge Sort" << std::endl; } else { std::cout << "Unknown sort type" << std::endl; } } };
这种设计的弊端非常明显:
违反开放/封闭原则 (Open/Closed Principle) :每当需要添加一种新的排序算法时,我们都必须修改 Sorter 类的 sort 方法,增加一个新的 else if 分支。这使得类变得不稳定且难以维护。
职责不单一 : Sorter 类承担了太多的责任。它不仅要负责“进行排序”这个行为,还要知道所有 具体的排序算法是如何实现的。
代码臃肿 : 随着算法增多,sort 方法会变得越来越长,难以阅读和理解。
复用性差 : 如果其他地方也需要用到某个排序算法,你可能需要复制代码,或者让那个地方也依赖这个巨大的 Sorter 类。
策略模式正是为了解决这类问题而生的。
3. 策略模式的结构 策略模式主要由以下三个角色组成:
Strategy (策略接口/抽象基类)
它定义了一个所有支持的算法的公共接口。
在 C++ 中,通常是一个包含纯虚函数的抽象基类。
Context 使用这个接口来调用由 ConcreteStrategy 定义的算法。
ConcreteStrategy (具体策略类)
它实现了 Strategy 接口,封装了具体的算法或行为。
每个具体策略类都代表一种算法。
Context (上下文)
它包含一个指向 Strategy 对象的引用(通常是成员变量)。
它不直接实现算法,而是将请求委托给其持有的 Strategy 对象。
它通常提供一个方法来让客户端可以设置或更换具体的策略。
UML 类图 下面是策略模式的经典 UML 类图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 +----------------+ <> +------------------+ | Context |---------------+ IStrategy | +----------------+ +------------------+ | - strategy | | + execute() = 0 | +----------------+ +------------------+ | + setStrategy()| ^ | + doSomething()| | +----------------+ | | +---------------------------------+---------------------------------+ | | | +--------------------+ +--------------------+ +--------------------+ | ConcreteStrategyA | | ConcreteStrategyB | | ConcreteStrategyC | +--------------------+ +--------------------+ +--------------------+ | + execute() | | + execute() | | + execute() | +--------------------+ +--------------------+ +--------------------+
4. C++ 代码实现(经典方式:基于继承和多态) 我们用前面提到的排序器例子来重构代码,使其符合策略模式。
场景设定:一个排序器,可以动态切换排序算法。 第一步:定义策略接口 (Strategy) 我们创建一个抽象基类 SortStrategy,它定义了所有排序算法都必须实现的 sort 方法。
1 2 3 4 5 6 7 8 9 10 11 #pragma once #include <vector> class SortStrategy {public : virtual void sort (std::vector<int >& vec) const = 0 ; virtual ~SortStrategy () = default ; };
注意 : 基类中的析构函数必须是虚函数 (virtual),否则通过基类指针删除派生类对象时,会导致派生类的析构函数无法被调用,造成资源泄漏。
第二步:实现具体策略 (Concrete Strategies) 现在我们为每种排序算法创建一个具体的策略类,它们都继承自 SortStrategy。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include "SortStrategy.h" #include <iostream> #include <algorithm> class BubbleSortStrategy : public SortStrategy {public : void sort (std::vector<int >& vec) const override { std::cout << "Sorting using Bubble Sort..." << std::endl; int n = vec.size (); for (int i = 0 ; i < n - 1 ; ++i) { for (int j = 0 ; j < n - i - 1 ; ++j) { if (vec[j] > vec[j + 1 ]) { std::swap (vec[j], vec[j + 1 ]); } } } } }; class QuickSortStrategy : public SortStrategy {public : void sort (std::vector<int >& vec) const override { std::cout << "Sorting using Quick Sort (std::sort)..." << std::endl; std::sort (vec.begin (), vec.end ()); } }; class MergeSortStrategy : public SortStrategy {public : void sort (std::vector<int >& vec) const override { std::cout << "Sorting using Merge Sort..." << std::endl; std::stable_sort (vec.begin (), vec.end ()); } };
第三步:创建上下文 (Context) Context 类 (Sorter) 持有一个 SortStrategy 的指针,并将排序任务委托给它。我们使用 std::unique_ptr 来管理策略对象的生命周期,这是一种更安全的现代 C++ 做法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include "SortStrategy.h" #include <vector> #include <memory> #include <utility> class Sorter {private : std::unique_ptr<SortStrategy> strategy_; public : Sorter (std::unique_ptr<SortStrategy> strategy) : strategy_ (std::move (strategy)) {} void setStrategy (std::unique_ptr<SortStrategy> strategy) { strategy_ = std::move (strategy); } void performSort (std::vector<int >& vec) { if (strategy_) { strategy_->sort (vec); } else { std::cout << "No sort strategy set!" << std::endl; } } };
第四步:客户端代码调用 客户端代码负责创建具体的策略对象和上下文对象,并将它们组合起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> #include <vector> #include <memory> #include "ConcreteStrategies.h" #include "Sorter.h" void printVector (const std::vector<int >& vec) { for (int val : vec) { std::cout << val << " " ; } std::cout << std::endl; } int main () { std::vector<int > data = {5 , 2 , 8 , 1 , 9 , 4 }; auto sorter = std::make_unique <Sorter>(std::make_unique <BubbleSortStrategy>()); std::cout << "Initial data: " ; printVector (data); std::cout << "\n--- Using Bubble Sort ---" << std::endl; sorter->performSort (data); std::cout << "Sorted data: " ; printVector (data); std::cout << "\n-----------------------------\n" << std::endl; data = {5 , 2 , 8 , 1 , 9 , 4 }; std::cout << "--- Changing to Quick Sort ---" << std::endl; sorter->setStrategy (std::make_unique <QuickSortStrategy>()); sorter->performSort (data); std::cout << "Sorted data: " ; printVector (data); return 0 ; }
现在,如果我们要添加一个新的排序算法(比如“堆排序”),我们只需要:
创建一个新的 HeapSortStrategy 类,继承自 SortStrategy。
在客户端代码中实例化并使用它。
Sorter 类完全不需要任何修改,完美符合开放/封闭原则。
5. 策略模式的优缺点分析 优点
开闭原则 :策略模式很好地支持了开闭原则,可以轻松地增加新的策略而无需修改上下文代码。
避免多重条件语句 :它将 if-else 或 switch 结构替换为对象的多态性,使代码更清晰、更易于维护。
策略的复用 :可以将策略类定义为公共组件,在不同的上下文中复用。
关注点分离 :上下文只关心“何时”执行,而具体策略关心“如何”执行,职责清晰。
运行时切换 :可以在程序运行时动态地改变对象的行为(算法)。
缺点
类数量增多 :每个策略都是一个独立的类,如果策略很多,会导致系统中类的数量增加。
客户端必须了解所有策略 :客户端需要知道有哪些具体的策略,并自行决定使用哪一个。在某些情况下,这可能会将策略的实现细节暴露给客户端。
增加了对象间的通信 :上下文和策略之间存在一次间接的调用,对于非常简单的算法,可能会显得有些小题大做。
6. C++ 中的其他实现方式 经典的基于继承和多态的方式非常通用,但在现代 C++ 中,我们有更灵活、更轻量级的选择。
方式一:使用 std::function 和 Lambda 表达式 我们可以用 std::function 替代策略基类指针。std::function 是一个通用的、可调用对象的包装器,它可以存储、复制和调用任何可调用目标——包括普通函数、Lambda 表达式、函数指针和函数对象。
修改后的 Sorter (Context):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <functional> #include <vector> #include <utility> class Sorter {private : std::function<void (std::vector<int >&)> sort_func_; public : Sorter (std::function<void (std::vector<int >&)> func) : sort_func_ (std::move (func)) {} void setStrategy (std::function<void (std::vector<int >&)> func) { sort_func_ = std::move (func); } void performSort (std::vector<int >& vec) { if (sort_func_) { sort_func_ (vec); } } };
客户端调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> #include <vector> #include <algorithm> void bubbleSortFunc (std::vector<int >& vec) { std::cout << "Sorting using bubble sort function..." << std::endl; } int main () { std::vector<int > data = {5 , 2 , 8 , 1 , 9 , 4 }; Sorter sorter ([](std::vector<int >& vec) { std::cout << "Sorting using a lambda (Quick Sort)..." << std::endl; std::sort(vec.begin(), vec.end()); }) ; sorter.performSort (data); data = {5 , 2 , 8 , 1 , 9 , 4 }; sorter.setStrategy (bubbleSortFunc); sorter.performSort (data); return 0 ; }
这种方式优点 是非常轻量 ,不需要定义一堆策略类,尤其适合那些逻辑简单、只用一次的策略。
方式二:使用模板元编程(静态策略模式) 如果策略在编译时就已经确定,并且不需要在运行时动态切换,那么可以使用模板实现静态策略模式 。这种方式没有运行时多态的开销(虚函数调用),性能更好。
修改后的 Sorter (Context):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <vector> #include <iostream> template <typename SortStrategy>class Sorter {private : SortStrategy strategy_; public : void performSort (std::vector<int >& vec) { strategy_.sort (vec); } }; struct BubbleSortStrategy { void sort (std::vector<int >& vec) const { std::cout << "Static sorting using Bubble Sort..." << std::endl; } }; struct QuickSortStrategy { void sort (std::vector<int >& vec) const { std::cout << "Static sorting using Quick Sort..." << std::endl; } };
客户端调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main () { std::vector<int > data1 = {5 , 2 , 8 , 1 , 9 , 4 }; std::vector<int > data2 = {5 , 2 , 8 , 1 , 9 , 4 }; Sorter<BubbleSortStrategy> bubbleSorter; bubbleSorter.performSort (data1); Sorter<QuickSortStrategy> quickSorter; quickSorter.performSort (data2); return 0 ; }
这种方式的优点 是性能高 (没有虚函数调用开销),类型安全 在编译期检查。缺点 是失去了运行时动态切换策略的灵活性 。
7. 总结与适用场景 何时应该使用策略模式?
当你有多个相关的类,它们只有行为上的差异时 。策略模式可以让你用一个统一的接口来处理这些不同的行为。
当你需要一个算法的多个变体时 。例如,不同的压缩算法、不同的验证规则、不同的文件保存格式。
当你希望避免使用大量的条件语句时 。将这些分支逻辑移到各自的策略类中,可以使主逻辑更清晰。
当你希望算法的实现细节与客户端代码分离时 。客户端只需知道使用哪个策略,而不需要关心策略内部是如何工作的。
当你希望在运行时能够动态地为一个对象选择或切换算法时 。(这是动态策略模式的核心优势)
策略模式是面向对象设计中一个非常强大且常用的工具,它体现了“组合优于继承”和“面向接口编程”等核心设计原则。掌握它对于编写可扩展、可维护的软件至关重要。