二十三种设计模式[21]

前言

       策略模式,对象行为型模式的一种。在《设计模式 - 可复用的面向对象软件》一书中将之描述为“ 定义一些列的算法,把它们一个个封装起来,并且使它们可相互替换。使得算法可以独立于使用它的客户而变化 ”。

       也就是说通过策略模式,我们能够将算法与其调用者分离成相对独立的个体,降低维护成本,使代码更加优雅。

场景

       就拿数据的搜索来说,可以简单的分为模糊搜索和精确搜索。在开发这个功能时,可能会写出如下代码。

public List<string> Search(string keyword, SeachModeEnum mode)
{
    //模拟数据
    List<string> dataList = new List<string>
    {
        "abc",
        "Abc",
        "ABc",
        "ABC"
    };

    switch (mode)
    {
        case SeachModeEnum.Exact:
            return dataList.FindAll(t => t == keyword);
        case SeachModeEnum.Fuzzy:
            return dataList.FindAll(t => t.IndexOf(keyword) > -1);
        default:
            return new List<string>();
    }
}

public enum SeachModeEnum
{
    /// <summary>
    /// 精确
    /// </summary>
    Exact,
    /// <summary>
    /// 模糊
    /// </summary>
    Fuzzy
}

       上面的代码确实能够解决我们的需求,但这只是一个简单的业务逻辑,如果换成一个复杂的业务算法呢?那么Search函数会变得非常庞大、非常复杂。并且在同一时刻switch中只有一个算法复合当前运行的需求,也就意味着在运行时刻switch中存在着许多与当前业务无关的“垃圾代码”。后续,当我们需要增加新的算法时需要对swtich语句做出修改,随着业务的日渐庞大维护的成本也随之加大。

       下面,来看看如何用策略模式实现类似的业务。

结构

Strategy_1

  • Strategy(策略接口):用来定义所有算法的公共接口,保证所有算法实现类的一致性;
  • ConcreteStrategy(具体策略):策略接口的实现类,用来实现具体的算法;
  • Context(上下文):定义用户感兴趣的功能。保持Strategy的引用,并将用户的请求转发给Strategy去处理;

       在策略模式中,Context依赖于Strategy,Context本身并不实现具体的业务而是将用户的请求转发给具体的Strategy实现。

示例

       还是以数据搜索为例,我们首先为每一种搜索方式创建一个实现IStrategy接口的类,以及一个调用各个检索方式的上下文类SearchContext。如下。

Strategy_2

public interface IStrategy
{
    SearchContext Context { set; get; }
    List<string> Search(string keyword);
}

/// <summary>
/// 精确搜索
/// </summary>
public class ExactSearchStrategy : IStrategy
{
    public SearchContext Context { set; get; } = null;

    public List<string> Search(string keyword)
    {
        return this.Context.DataList.FindAll(t => t == keyword);
    }
}

/// <summary>
/// 模糊搜索
/// </summary>
public class FuzzySearchStrategy : IStrategy
{
    public SearchContext Context { set; get; } = null;

    public List<string> Search(string keyword)
    {
        return this.Context.DataList.FindAll(t => t.IndexOf(keyword) > -1);
    }
}

public class SearchContext
{
    public SearchContext(IStrategy strategy)
    {
        this.Strategy = strategy;
        this.Strategy.Context = this;
        this.DataList = new List<string>
        {
            "abc","Abc","ABc","ABC"
        };
    }

    private IStrategy Strategy { set; get; } = null;
    /// <summary>
    /// 模拟数据
    /// </summary>
    public List<string> DataList { private set; get; } = new List<string>();

    public List<string> Search(string keyword)
    {
        return this.Strategy.Search(keyword);
    }
}

static void Main(string[] args)
{
    Console.WriteLine("精确搜索:");
    SearchContext context = new SearchContext(new ExactSearchStrategy());
    var dataList = context.Search("AB");
    dataList.ForEach(t => Console.WriteLine(t));
    Console.WriteLine("-------------------------");

    Console.WriteLine("模糊搜索:");
    context = new SearchContext(new FuzzySearchStrategy());
    dataList = context.Search("AB");
    dataList.ForEach(t => Console.WriteLine(t));
    Console.ReadKey();
}

image

       示例中,分别定义了实现IStrategy接口的ExactSearchStrategy和FuzzySearchStrategy策略类来实现模糊搜索和精确搜索业务。同时在这两个策略类中保持了一个SearchContext的引用以便在执行搜索时获取策略类感兴趣的信息(比如示例中的DataList)。当然也可以将这些信息通过参数传递给策略类(比如示例中的keyword)。但并不是所有的策略类都对这些信息感兴趣。保持SearchContext的引用能够使Context避免创建一些策略不感兴趣的数据,但同时也会造成Context与Strategy的循环引用,使其更紧密的耦合。用户在使用SearchContext时将其感兴趣的搜索策略注入到上下文中,再由SearchContext将用户的请求转发给注入的搜索策略类即可实现不同的搜索方式。

       在此基础上,我们在增加精确且不区分大小写和模糊且不区分大小写两种搜索方式。如下。

Strategy_3

/// <summary>
/// 精确且不区分大小写
/// </summary>
public class ExactAndCaseInsensitiveSearchStrategy : IStrategy
{
    public SearchContext Context { set; get; } = null;

    public List<string> Search(string keyword)
    {
        return this.Context.DataList.FindAll(t => t.ToLower() == keyword.ToLower());
    }
}

/// <summary>
/// 模糊且不区分大小写
/// </summary>
public class FuzzyAndCaseInsensitiveSearchStrategy : IStrategy
{
    public SearchContext Context { set; get; } = null;

    public List<string> Search(string keyword)
    {
        return this.Context.DataList.FindAll(t => t.ToLower().IndexOf(keyword.ToLower()) > -1);
    }
}

static void Main(string[] args)
{
    Console.WriteLine("精确搜索:");
    //SearchContext context = new SearchContext(new ExactSearchStrategy());
    SearchContext context = new SearchContext(new ExactAndCaseInsensitiveSearchStrategy());
    var dataList = context.Search("AB");
    dataList.ForEach(t => Console.WriteLine(t));
    Console.WriteLine("-------------------------");

    Console.WriteLine("模糊搜索:");
    //context = new SearchContext(new FuzzySearchStrategy());
    context = new SearchContext(new FuzzyAndCaseInsensitiveSearchStrategy());
    dataList = context.Search("AB");
    dataList.ForEach(t => Console.WriteLine(t));
    Console.ReadKey();
}

image

       如示例中所示,我们在使用策略模式的过程中可以很轻易的增加或修改使用的算法。只需要增加相应的策略类并修改Context中注入的策略即可增加和修改算法。符合开闭原则。

补充

  • 策略模式 + 单例/享元模式

       在策略模式中,当我们只通过参数传递策略感兴趣的数据时这个策略类只存在内部行为而不存在外部状态。所以可以将策略类作为享元共享或者将其设计为一个单例。

       策略模式与状态模式的实现很相似。并且都是能够改变类的行为以及减少判断语句的使用。但策略模式的核心在于算法的封装,使用户可以很容易且低成本的变更算法,并且一般情况是这个过程是静态的。而状态模式的核心在于动态的变更对象的行为,在程序的运行过程中由于一些条件的触发,导致上下文(Conatext)中的状态(State)对象发生变更从而改变了上下文的行为。

总结

       策略模式可以帮助我们减少条件语句if-else和switch-case的使用。将每种算法单独封装起来即降低了用户对算法的使用、维护以及切换的成本,也易于代码的理解和扩展,复合开闭原则。但策略模式的使用也使得类的数量增多,并且在某些情况下用户需要知道这些策略到底有何不同,此时就不得不向客户暴露策略类具体的实现。


       以上,就是我对策略模式的理解,希望对你有所帮助。

       示例源码:https://gitee.com/wxingChen/DesignPatternsPractice

       系列汇总:https://www.cnblogs.com/wxingchen/p/10031592.html

       本文著作权归本人所有,如需转载请标明本文链接(https://www.cnblogs.com/wxingchen/p/10264077.html)

原文地址:https://www.cnblogs.com/wxingchen/p/10264077.html