Strategy Pattern (策略模式)
所謂 Strategy Pattern 的精神,就是將策略 (算法) 封裝為一個對象,易于相互替換,如同 USB 設備一樣可即插即用;而不是將策略、具體的算法和行為,硬編碼在某個類或客戶程序中,導至事后的修改和擴展不易。
若有多種「策略」,就將這些個策略,和這些策略的算法、行為,封裝在各個類中,并讓這些類,去繼承某個公用的抽象類或接口。接著在客戶程序中,就可動態引用,且易于更換這些不同的「策略」,不會因為日后添加、修改了某一個「策略」,就得重新修改、編譯多處的源代碼。此即為一種「封裝變化點」的做法,將常會變化的部分進行抽象、定義為接口,亦即實現「面向接口編程」的概念。且客戶程序 (調用者) 只須知道接口的外部定義即可,具體的實現則無須理會。
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
- Design Patterns: Elements of Reusable Object-Oriented Software
Strategy Pattern 適用的情景:
- 應用中的許多類,在解決某些問題時很相似,但實現的行為有所差異。比如:不同功能的程序,都可能要用到「排序」算法。
- 根據運行環境的不同,需要采用不同的算法。比如:在手機、PC 計算機上,因硬件等級不同,必須采用不同的排序算法。
- 針對給定的目的,存在多種不同的算法,且我們可用代碼實現算法選擇的標準。
- 需要封裝復雜的數據結構。比如:特殊的加密算法,客戶程序僅需要知道調用的方式即可。
- 同上,算法中的羅輯和使用的數據,應該與客戶程序隔離時。

圖 1 這張為很多書籍和文檔都曾出現過的 Strategy 經典 Class Diagram

01_Shell.aspx.cs
using System;
using com.cnblogs.WizardWu.sample01;
//客戶程序
public partial class _01_Shell : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//執行對象
Context context;
context = new Context(new ConcreteStrategyA());
Response.Write(context.ContextInterface() + "
");
context = new Context(new ConcreteStrategyB());
Response.Write(context.ContextInterface() + "
");
context = new Context(new ConcreteStrategyC());
Response.Write(context.ContextInterface() + "
");
}
}
namespace com.cnblogs.WizardWu.sample01
{
//抽象算法類 (亦可用接口)。定義了所有策略的公共接口
abstract class Strategy
{
//算法需要完成的功能
public abstract string AlgorithmInterface();
}
//具體算法類A
class ConcreteStrategyA : Strategy
{
//算法A實現方法
public override string AlgorithmInterface()
{
return "算法A實現";
}
}
//具體算法類B
class ConcreteStrategyB : Strategy
{
//算法B實現方法
public override string AlgorithmInterface()
{
return "算法B實現";
}
}
//具體算法類C
class ConcreteStrategyC : Strategy
{
//算法C實現方法
public override string AlgorithmInterface()
{
return "算法C實現";
}
}
//執行對象。需要采用可替換策略執行的對象
class Context
{
Strategy strategy;
public Context(Strategy strategy) //構造函數
{
this.strategy = strategy;
}
//執行對象依賴于策略對象的操作方法
public string ContextInterface()
{
return strategy.AlgorithmInterface();
}
}
} // end of namespace
/*
結行結果:
算法A實現
算法B實現
算法C實現
*/
上方的「Shell (殼)」示例中,最下方的 Context 類,為一種維護上下文信息的類,讓 Strategy 類 (或 IStrategy 接口) 及其子類對象的算法,能運行在這個上下文里。
下方的圖 2 及其代碼,為此 Shell 示例和 Strategy Pattern 的一個具體實現示例。我們知道,Linux 和 Windows 操作系統,在文本文件的「換行符」是不同的,前者為「\n」,后者為「\r\n」。若我們要設計一個文本編輯工具,或簡易的編程工具,必須要能隨時轉換這兩種不同操作系統的換行符 (假設 .NET 已可執行于 Linux 上)。此時我們即不該在客戶程序 (如:ASP.NET 頁面的 Code-Behind) 中用硬編碼 switch...case 的 hard coding 寫法,而應如下方示例,以 Strategy Pattern 實現此一功能,并將這些算法 (策略) 各自封裝在各個子類中 (如 ASP.NET 項目的 App_Code 文件夾中的類,或其他類庫項目中的類),使他們易于組合、更換,便于日后的維護和修改。

圖 2 示例 02_Strategy.aspx.cs 的 Class Diagram。此為 Sybase PowerDesigner 的「Reverse Engineer」功能,所自動產生的圖

02_Strategy.aspx.cs
using System;
using com.cnblogs.WizardWu.sample02;
//客戶程序
public partial class _02_Strategy : System.Web.UI.Page
{
String strLinuxText = "操作系統 \n 紅帽 Linux 創建的 \n 文本文件";
String strWindowsText = "操作系統 \r\n 微軟 Windows 創建的 \r\n 文本文件";
protected void Page_Load(object sender, EventArgs e)
{
}
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
switch(DropDownList1.SelectedValue)
{
case "Linux":
Label1.Text = ContextCharChange.contextInterface(new LinuxStrategy(strWindowsText));
//Label1.Text = strWindowsText.Replace("\r\n", "\n"); //未用任何 Pattern 的寫法
break;
case "Windows":
Label1.Text = ContextCharChange.contextInterface(new WindowsStrategy(strLinuxText));
//Label1.Text = strLinuxText.Replace("\n", "\r\n"); //未用任何 Pattern 的寫法
break;
default:
Label1.Text = String.Empty;
break;
}
}
}
namespace com.cnblogs.WizardWu.sample02
{
//抽象算法類 (亦可用接口)。定義了所有策略的公共接口
public abstract class TextStrategy
{
protected String text;
public TextStrategy(String text) //構造函數
{
this.text = text;
}
//算法需要完成的功能
public abstract String replaceChar();
}
//具體算法類A
public class LinuxStrategy : TextStrategy
{
public LinuxStrategy(String text) //構造函數
: base(text)
{
}
//算法A實現方法
public override String replaceChar()
{
text = text.Replace("\r\n", "\n");
return text;
}
}
//具體算法類B
public class WindowsStrategy : TextStrategy
{
public WindowsStrategy(String text) //構造函數
: base(text)
{
}
//算法B實現方法
public override String replaceChar()
{
text = text.Replace("\n", "\r\n");
return text;
}
}
//執行對象。需要采用可替換策略執行的對象
public class ContextCharChange
{
//執行對象依賴于策略對象的操作方法
public static String contextInterface(TextStrategy strategy)
{
return strategy.replaceChar();
}
}
} // end of namespace

圖 3 示例 02_Strategy.aspx.cs 的執行結果
若未用任何 Pattern 的客戶程序,可能就如下方的硬編碼,將「換行符」和算法,直接寫死在 ASP.NET 的 Code-Behind 里,導至事后的維護和擴展不易。

hard coding
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
switch(DropDownList1.SelectedValue)
{
case "Linux":
Label1.Text = strWindowsText.Replace("\r\n", "\n");
break;
case "Windows":
Label1.Text = strLinuxText.Replace("\n", "\r\n");
break;
default:
Label1.Text = String.Empty;
break;
}
}
此外,若用 Simple Factory Pattern (簡單工廠模式) 雖然也能解決上述硬編碼的問題,但就如我們前一篇帖子「C# Design Patterns (1) - Factory Method」曾經提過的缺點,日后若要添加或修改功能時,仍要修改、重新編譯 server-side 的「工廠類」。所以在此種情況下,用 Strategy 會是比 Simple Factory 更好的選擇。
--------------------------------------------------------
Strategy Pattern 的優點:
- 簡化了單元測試,因為每個算法都有自己的類,可以通過自己的接口單獨做測試。
- 避免程序中使用多重條件轉移語句,使系統更靈活,并易于擴展。
- 高內聚、低偶合。
Strategy Pattern 的缺點:
- 因為每個具體策略都會產生一個新類,所以會增加需要維護的類的數量。
- 選擇所用具體實現的職責由客戶程序承擔,并轉給 Context 對象,并沒有解除客戶端需要選擇判斷的壓力。
若要減輕客戶端壓力,或程序有特殊考量,還可把 Strategy 與 Simple Factory 兩種 Pattern 結合,即可將選擇具體算法的職責改由 Context 來承擔,亦即將具體的算法,和客戶程序做出隔離。有關這方面的概念和示例,可參考伍迷的「大話設計模式」一書 [10]。
--------------------------------------------------------
此外,從行為上來看,State Pattern 和 Strategy Pattern 有點類似,但前者可看作后者的動態版本。
- State:看當前是什么狀態,就采取什么動作。
- Strategy:看需求及情景為何,采用適當的策略。
State 中,當對象內部的狀態改變時,它可切換到一組不同的操作,從而改變對象的行為,例如 GoF 示例中的 TCP 連接;而 Strategy 是直接采用適當的策略 (算法),如本帖示例中,不同的操作系統,實現換行的具體算法類 LinuxStrategy 與 WindowsStrategy。