策略模式是一种行为设计模式 它能让你定义一系列算法 并将每种算法分别放入独立的类中 以使算法的对象能够相互替换。这么说还有点抽象,这里就举个例子。假设我们需要输出一个字符串列表,比如以如下列表方式输出:

  • 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>来实现。