策略模式(Strategy)
定義
策略是一種行為設計模式,它能讓你定義一系列算法,並將每種算法分別放入獨 立的類中,以使算法的對象能夠相互替換。
前言
1. 問題
你打算為遊客們創建一款導遊程序。該程序的核心功能是提供美觀的地圖,以幫助用户在任何城市中快速定位。
用户期待的程序新功能是自動路線規劃:他們希望輸入地址後就能在地圖上看到前往目的地的最快路線。
程序的首個版本只能規劃公路路線。駕車旅行的人們對此非常滿意。但很顯然,並非所有人都會在度假時開車。因此你在下次更新時添加了規劃步行路線的功能。此後,你又添加了規劃公共交通路線的功能。
而這只是個開始。不久後,你又要為騎行者規劃路線。又過了一段時間,你又要為遊覽城市中的所有景點規劃路線。
儘管從商業角度來看,這款應用非常成功,但其技術部分卻讓你非常頭疼:每次添加新的路線規劃算法後,導遊應用中主要類的體積就會增加一倍。終於在某個時候,你覺得自己沒法繼續維護這堆代碼了。
無論是修復簡單缺陷還是微調街道權重,對某個算法進行任何修改都會影響整個類,從而增加在已有正常運行代碼中引入錯誤的風險。
此外,團隊合作將變得低效。如果你在應用成功發佈後招募了團隊成員,他們會抱怨在合併衝突的工作上花費了太多時間。在實現新功能的過程中,你的團隊需要修改同一個巨大的類,這樣他們所編寫的代碼相互之間就可能會出現衝突。
2. 解決方案
策略模式建議找出負責用許多不同方式完成特定任務的類,然後將其中的算法抽取到一組被稱為策略的獨立類中。
名為上下文的原始類必須包含一個成員變量來存儲對於每種策略的引用。上下文並不執行任務,而是將工作委派給已連接的策略對象。
上下文不負責選擇符合任務需要的算法——客户端會將所需策略傳遞給上下文。實際上,上下文並不十分了解策略,它會通過同樣的通用接口與所有策略進行交互,而該接口只需暴露一個方法來觸發所選策略中封裝的算法即可。
因此,上下文可獨立於具體策略。這樣你就可在不修改上下文代碼或其他策略的情況下添加新算法或修改已有算法了。
在導遊應用中, 每個路線規劃算法都可被抽取到只有一個buildRoute方法的獨立類中。該方法接收起點和終點作為參數,並返回路線中途點的集合。
即使傳遞給每個路徑規劃類的參數一模一樣,其所創建的路線也可能完全不同。主要導遊類的主要工作是在地圖上渲染一系列中途點,不會在意如何選擇算法。該類中還有一個用於切換當前路徑規劃策略的方法,因此客户端(例如用户界面中的按鈕)可用其他策略替換當前選擇的路徑規劃行為。
真實世界類比
假如你需要前往機場。 你可以選擇乘坐公共汽車、 預約出租車或騎自行車。 這些就是你的出行策略。 你可以根據預算或時間等因素來選擇其中一種策略。
結構
- 上下文(Context)維護指向具體策略的引用,且僅通過策略接口與該對象進行交流。
- 策略(Strategy)接口是所有具體策略的通用接口,它聲明瞭一個上下文用於執行策略的方法。
- 具體策略(Concrete Strategies)實現了上下文所用算法的各種不同變體。
- 當上下文需要運行算法時,它會在其已連接的策略對象上調用執行方法。上下文不清楚其所涉及的策略類型與算法的執行方式。
- 客户端(Client)會創建一個特定策略對象並將其傳遞給上下文。上下文則會提供一個設置器以便客户端在運行時替換相關聯的策略。
適用場景
- 當你想使用對象中各種不同的算法變體,並希望能在運行時切換算法時,可使用策略模式。
策略模式讓你能夠將對象關聯至可以不同方式執行特定子任務的不同子對象,從而以間接方式在運行時更改對象行為。
- 當你有許多僅在執行某些行為時略有不同的相似類時,可使用策略模式。
策略模式讓你能將不同行為抽取到一個獨立類層次結構中,並將原始類組合成同一個,從而減少重複代碼。
- 如果算法在上下文的邏輯中不是特別重要,使用該模式能將類的業務邏輯與其算法實現細節隔離開來。
策略模式讓你能將各種算法的代碼、內部數據和依賴關係與其他代碼隔離開來。不同客户端可通過一個簡單接口執行算法,並能在運行時進行切換。
- 當類中使用了複雜條件運算符以在同一算法的不同變體中切換時,可使用該模式。
策略模式將所有繼承自同樣接口的算法抽取到獨立類中,因此不再需要條件語句。 原始對象並不實現所有算法的變體,而是將執行工作委派給其中的一個獨立算法對象。
實現方式
- 從上下文類中找出修改頻率較高的算法(也可能是用於在運行時選擇某個算法變體的複雜條件運算符)。
- 聲明該算法所有變體的通用策略接口。
- 將算法逐一抽取到各自的類中,它們都必須實現策略接口。
- 在上下文類中添加一個成員變量用於保存對於策略對象的引用。然後提供設置器以修改該成員變量。上下文僅可通過策略接口同策略對象進行交互,如有需要還可定義一個接口來讓策略訪問其數據。
- 客户端必須將上下文類與相應策略進行關聯,使上下文可以預期的方式完成其主要工作。
優點
- 你可以在運行時切換對象內的算法。
- 你可以將算法的實現和使用算法的代碼隔離開來。
- 你可以使用組合來代替繼承。
- 開閉原則。你無需對上下文進行修改就能夠引入新的策略。
缺點
- 如果你的算法極少發生改變,那麼沒有任何理由引入新的類和接口。使用該模式只會讓程序過於複雜。
- 客户端必須知曉策略間的不同——它需要選擇合適的策略。
- 許多現代編程語言支持函數類型功能,允許你在一組匿名函數中實現不同版本的算法。這樣,你使用這些函數的方式就和使用策略對象時完全相同,無需藉助額外的類和接口來保持代碼簡潔
與其他模式的關係
- 橋接、狀態和策略(在某種程度上包括適配器)模式的接口非常相似。實際上,它們都基於組合模式——即將工作委派給其他對象,不過也各自解決了不同的問題。模式並不只是以特定方式組織代碼的配方,你還可以使用它們來和其他開發者討論模式所解決的問題。
- 命令和策略看上去很像,因為兩者都能通過某些行為來參數化對象。但是,它們的意圖有非常大的不同。
- 你可以使用命令來將任何操作轉換為對象。操作的參數將成為對象的成員變量。 你可以通過轉換來延遲操作的執行、將操作放入隊列、保存歷史命令或者向遠程服務發送命令等。
- 另一方面,策略通常可用於描述完成某件事的不同方式,讓你能夠在同一個上下文類中切換算法。
- 裝飾可讓你更改對象的外表,策略則讓你能夠改變其本質。
- 模板方法基於繼承機制:它允許你通過擴展子類中的部分內容來改變部分算法。策略基於組合機制:你可以通過對相應行為提供不同的策略來改變對象的部分行為。模板方法在類層次上運作,因此它是靜態的。策略在對象層次上運作,因此允許在運行時切換行為。
- 狀態可被視為策略的擴展。兩者都基於組合機制:它們都通過將部分工作委派給“幫手”對象來改變其在不同情景下的行為。策略使得這些對象相互之間完全獨立,它們不知道其他對象的存在。 但狀態模式沒有限制具體狀態之間的依賴,且允許它們自行改變在不同情景下的狀態。
實例
Strategy.h:
#ifndef STRATEGY_H_
#define STRATEGY_H_
#include <vector>
// 抽象策略類: 排序
class Sort {
public:
virtual void sortVector(std::vector<int> &arr) = 0;
};
#endif // STRATEGY_H_
ConcreteStrategy.h:
#ifndef CONCRETE_STRATEGY_H_
#define CONCRETE_STRATEGY_H_
#include <vector>
#include <string>
#include <utility>
#include <iostream>
#include "Strategy.h"
// 打印vector內容
void printVector(const std::string prefix, const std::vector<int> &vi) {
std::cout << prefix;
for (auto i : vi) {
std::cout << " " << i;
}
std::cout << std::endl;
}
// 具體策略類: 冒泡排序
class BubbleSort : public Sort {
public:
void sortVector(std::vector<int> &vi) override {
printVector("冒泡排序前:", vi);
int len = vi.size();
// 輪次: 從1到n-1輪
for (int i = 0; i < len - 1; ++i) {
// 優化: 判斷本輪是否有交換元素, 如果沒交換則可直接退出
bool is_exchange = false;
for (int j = 0; j < len - i - 1; ++j) {
if (vi[j] > vi[j+1]) {
std::swap(vi[j], vi[j+1]);
is_exchange = true;
}
}
// 如果本輪無交換, 則可以直接退出
if (!is_exchange) {
printVector("冒泡排序後:", vi);
return;
}
}
printVector("冒泡排序後:", vi);
}
};
// 具體策略類: 選擇排序
class SelectionSort : public Sort {
public:
void sortVector(std::vector<int> &vi) override {
printVector("選擇排序前:", vi);
// 需要進行 n-1 輪
for (int i = 0; i < vi.size() - 1; ++i) {
// 找到此輪的最小值下標
int min_index = i;
for (int j = i + 1; j < vi.size(); ++j) {
if (vi[j] < vi[min_index]) {
min_index = j;
}
}
std::swap(vi[i], vi[min_index]);
}
printVector("選擇排序後:", vi);
}
};
// 具體策略類: 插入排序
class InsertionSort : public Sort {
public:
void sortVector(std::vector<int> &vi) override {
printVector("插入排序前:", vi);
// 第一輪不需要操作, 第二輪比較一次, 第n輪比較 n-1 次
for (int i = 1; i < vi.size(); ++i) {
// 存儲待插入的值和下標
int insert_value = vi[i];
int j = i - 1;
while (j >= 0 && vi[j] > insert_value) {
vi[j + 1] = vi[j]; // 如果左側的已排序元素比目標值大, 那麼右移
j--;
}
// 注意這裏insert_index 需要+1
vi[j + 1] = insert_value;
}
printVector("插入排序後:", vi);
}
};
#endif // CONCRETE_STRATEGY_H_
Context.h:
#ifndef CONTEXT_H_
#define CONTEXT_H_
#include <vector>
#include "Strategy.h"
class ArrayHandler {
public:
void sortVector(std::vector<int> &arr) {
return sort_->sortVector(arr);
}
void setSortStrategy(Sort* sort) {
sort_ = sort;
}
private:
Sort *sort_;
};
#endif // CONTEXT_H_
main.cpp:
#include <vector>
#include <algorithm>
#include <random>
#include <iostream>
#include "ConcreteStrategy.h"
#include "Context.h"
std::vector<int> test_array = {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};
int main() {
ArrayHandler array_handler;
{
// 冒泡排序
BubbleSort* bubble_sort = new BubbleSort();
auto rng = std::default_random_engine {};
std::shuffle(std::begin(test_array), std::end(test_array), rng);
array_handler.setSortStrategy(bubble_sort);
array_handler.sortVector(test_array);
delete bubble_sort;
}
{
// 選擇排序
SelectionSort* select_sort = new SelectionSort();
auto rng = std::default_random_engine {};
std::shuffle(std::begin(test_array), std::end(test_array), rng);
array_handler.setSortStrategy(select_sort);
array_handler.sortVector(test_array);
delete select_sort;
}
{
// 插入排序
InsertionSort* insert_sort = new InsertionSort();
auto rng = std::default_random_engine {};
std::shuffle(std::begin(test_array), std::end(test_array), rng);
array_handler.setSortStrategy(insert_sort);
array_handler.sortVector(test_array);
delete insert_sort;
}
return 0;
}
編譯運行:
$g++ -g main.cpp -o strategy -std=c++11
$./strategy
冒泡排序前: 19 21 5 6 12 4 13 20 1 22 11 14 18 2 24 23 9 10 16 3 7 17 15 25 8
冒泡排序後: 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
選擇排序前: 19 21 5 6 12 4 13 20 1 22 11 14 18 2 24 23 9 10 16 3 7 17 15 25 8
選擇排序後: 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
插入排序前: 19 21 5 6 12 4 13 20 1 22 11 14 18 2 24 23 9 10 16 3 7 17 15 25 8
插入排序後: 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