策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。这么说还有点抽象,这里就举个例子。假设我们需要输出一个字符串列表,比如以如下列表方式输出:
- just
- like
- this
随着需求的变更,可能需要输出不同的格式,比如增加一些特殊符号,比如如果要输出列表,则需要在用"<ul>"或者"<li>"来对字符串进行包装,再比如在HTML或者JSON格式中,需要输出一些起始标签或者结束标签。
所以我们可以抽象出一种输出列表格式的策略:
- 渲染开始标签或者元素
- 渲染列表中的每一个对象
- 渲染结束标签或者元素
不同的策略,可能有不同的格式,但是流程是通用的。
根据策略能否在运行时动态替换,策略模式有两种形式,分别是动态策略和静态策略模式。
动态策略模式
我们的目标是以两种格式输出列表:Markdown和Html,所以首先定义一个输出格式的枚举:
public enum OutputFormat
{
Markdown,
Html
}
然后定义一个接口类型,用来表示策略的抽象:
public interface IListStrategy
{
void Start(StringBuilder sb);
void AddListItem(StringBuilder sb, string item);
void End(StringBuilder sb);
}
接口里定义了三个方法,分别为处理开始和结束标签的方法,以及处理集合中,每个元素的方法,输出的内容写入到StringBuilder参数中。
然后定义了输出处理类。
public class TextProcessor
{
private IListStrategy listStrategy;
private StringBuilder sb = new StringBuilder();
public void AppendList(IEnumerable<string> items)
{
listStrategy.Start(sb);
foreach (string item in items)
{
listStrategy.AddListItem(sb, item);
}
listStrategy.End(sb);
}
public override string ToString()
{
return sb.ToString();
}
}
可以看到,在TextProcessor中,将IListStrategy作为了局部变量,我们可以使用构造函数或者通过公共方法来注入真实的实现了IListStrategy的策略。在AppendList方法中,调用了IListStrategy的三个方法来格式化输出字符串列表。
我们这里不用构造函数依赖注入的方式,而是通过添加额外的方法,来主动设置实现了IListStrategy方法,为后续的动态修改策略提供便利。
public void SetOutputFormat(OutputFormat format)
{
switch (format)
{
case OutputFormat.Html:
listStrategy = new HtmlListStrategy();
break;
case OutputFormat.Markdown:
listStrategy = new MarkdownListStrategy();
break;
}
}
接下来,我们实现IListStrategy接口来实现具体的格式化策略,首先是Html的列表格式化输出策略:
public class HtmlListStrategy : IListStrategy
{
public void AddListItem(StringBuilder sb, string item)
{
sb.AppendLine($" <li>{item} </li>");
}
public void Start(StringBuilder sb) => sb.AppendLine("<ul>");
public void End(StringBuilder sb) => sb.AppendLine("</ul>");
}
起始标签为"<ul></ul>",元素标签为"<li></li>",接下来实现Markdown的列表格式化策略:
public class MarkdownListStrategy : IListStrategy
{
public void Start(StringBuilder sb) { }
public void AddListItem(StringBuilder sb, string item)
{
sb.AppendLine($" * {item}");
}
public void End(StringBuilder sb) { }
}
Markdown里列表没有起始标签,只需要在元素前面添加“*”即可,可以看到这里Start和End方法什么都没做,这里其实可以将IListStrategy改为抽象类,将Start和End设为抽象方法,这个就是模板方法模式了。
现在,用法如下:
TextProcessor tp = new TextProcessor();
tp.SetOutputFormat(OutputFormat.Markdown);
tp.AppendList(new List<string>() { "how", "are", "you", "?" });
Console.WriteLine(tp);
输出为:
* how
* are
* you
* ?
还可以动态替换策略。我们在TextProcess中添加Clear方法,方法里简单的调用StringBuilder的Clear方法:
public void Clear()
{
sb.Clear();
}
然后动态的替换策略:
tp.Clear();
tp.SetOutputFormat(OutputFormat.Html);
tp.AppendList(new List<string>() { "how", "are", "you", "?" });
Console.WriteLine(tp);
输出结果如下:
<ul>
<li>how </li>
<li>are </li>
<li>you </li>
<li>? </li>
</ul>
静态策略模式
因为有泛型,所以我们可以将策略作为泛型类型来定义。需要修改的地方很简单,只需要修改TextProcess类,添加泛型类型。
public class TextProcessor<LS> where LS : IListStrategy, new()
{
private IListStrategy listStrategy = new LS();
private StringBuilder sb = new StringBuilder();
public void AppendList(IEnumerable<string> items)
{
listStrategy.Start(sb);
foreach (string item in items)
{
listStrategy.AddListItem(sb, item);
}
listStrategy.End(sb);
}
public override string ToString()
{
return sb.ToString();
}
}
泛型类型LS限定了必须实现IListStrategy,并且必须具有无参构造函数。其他的跟之前的TextProcessor一致,用法如下:
var tp1 = new TextProcessor<MarkdownListStrategy>();
tp1.AppendList(new List<string>() { "how", "are", "you", "?" });
Console.WriteLine(tp1);
var tp2 = new TextProcessor<HtmlListStrategy>();
tp2.AppendList(new List<string>() { "how", "are", "you", "?" });
Console.WriteLine(tp2);
输出结果跟上述一致。需要注意的是泛型实现,必须定义两个TextProcessor类,而不是像前面的方法那样,可以动态修改策略实现,所以这也是静态策略模式的一个缺点。
总结
策略模式能够允许我们定义算法的框架,然后使用组合的方式提供算法策略的具体实现,这种方式的具体实现有两种方式:
- 动态策略模式只是保存了一个队当前策略的一个引用,如果需要修改策略,只需要使用新的策略替换旧的引用即可。
- 静态策略模式需要我们在编译时选定具体的策略类型,后续不能修改。
是选择动态策略模式,还是静态策略模式?其实都行,比如,加入我们有一个UI界面,能够让用户选择列表的文本输出格式,既可以使用单个TextProcessor对象,在中途更换输出策略,也能够使用多个泛型类型比如TextProcessor<HtmlListStrategy>和TextProcessor<MarkdownListStrategy>来实现。