本帖介绍 Strategy Pattern (策略模式)。本文的撰写以容易入门为主,读者若欲更深入了解,可参阅帖子最后附的「相关文章及书籍」。
--------------------------------------------------------
本帖的示例下载点:
http://files.cnblogs.com/WizardWu/090622.zip
(执行本示例,需要 Visual Studio 或 IIS,不需要数据库)
--------------------------------------------------------
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
![ContractedBlock.gif](/Images/OutliningIndicators/ContractedBlock.gif)
![ExpandedBlockStart.gif](/Images/OutliningIndicators/ExpandedBlockStart.gif)
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() + "<br>");
context = new Context(new ConcreteStrategyB());
Response.Write(context.ContextInterface() + "<br>");
context = new Context(new ConcreteStrategyC());
Response.Write(context.ContextInterface() + "<br>");
}
}
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」功能,所自动产生的图
![ContractedBlock.gif](/Images/OutliningIndicators/ContractedBlock.gif)
![ExpandedBlockStart.gif](/Images/OutliningIndicators/ExpandedBlockStart.gif)
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 里,导至事后的维护和扩展不易。
![ContractedBlock.gif](/Images/OutliningIndicators/ContractedBlock.gif)
![ExpandedBlockStart.gif](/Images/OutliningIndicators/ExpandedBlockStart.gif)
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。
--------------------------------------------------------
相关文章:
[1] Strategy 模式,作者: caterpillar (繁体中文)
http://caterpillar.onlyfun.net/Gossip/DesignPattern/StrategyPattern.htm
http://www.javaworld.com.tw/jute/post/view?bid=44&id=28251&sty=1&tpg=1&age=-1
[2] .Net中的设计模式——Strategy模式
http://www.cnblogs.com/wayfarer/archive/2006/09/12/502517.html
[3] 鸭子-策略模式(Strategy)
http://www.cnblogs.com/justinw/archive/2007/02/06/641414.html
[4] Bridge Strategy 和 State 的区别
http://www.cnblogs.com/idior/articles/97283.html
[5] 我的实用设计模式之关于Policy-based design
http://www.cnblogs.com/procoder/archive/2009/03/24/1420362.html
[6] Design Pattern - 真 OO无双
http://www.cnblogs.com/oomusou/category/82066.html
[7] 史帝芬心得筆記 -- Factory Method Pattern (繁体中文)
http://my.so-net.net.tw/idealist/Patterns/index.html
[8] .Net Go2 OO (繁体中文)
http://netgo2oo.blog.ithome.com.tw/post/224/1022
----------------------------
相关书籍:
[9] C# 3.0 Design Patterns, Judith Bishop, O'Reilly
http://oreilly.com/catalog/9780596527730/
http://www.oreilly.com.cn/book.php?bn=978-7-111-25080-7
[10] 大话设计模式 (繁体中文翻译本), 程杰 着, 清华大学出版社
http://www.cnblogs.com/cj723/archive/2009/03/12/1409359.html
[11] Head First 设计模式, Freeman, O'Reilly
http://oreilly.com.cn/book.php?bn=978-7-5083-5393-7
[12] 设计模式 - 基于 C# 的工程化实现及扩展, 王翔 着, 电子工业出版社
http://www.china-pub.com/129880
[13] Design Patterns: Elements of Reusable Object-Oriented Software
http://www.amazon.com/exec/obidos/tg/detail/-/0201633612/ref=ase_portlandpatternrA/002-6454224-1040813?v=glance&s=books
[14] Design Patterns 于 Java 语言上的实习应用 (繁体中文翻译本), 结城浩 着, 博硕文化出版社
--------------------------------------------------------