策略模式是oop中最著名的设计模式之一,是对方法行为的抽象,可以归类为行为设计模式,也是oop中interface经典的应用。其特点简单又实用,是我最喜欢的模式之一。策略模式定义了一个拥有共同行为的算法族,每个算法都被封装起来,可以互相替换,独立于客户端而变化。策略模式本身的实现比较简单,但是结合单例模式+简单工厂模式+注解+反射,可以构造出近乎完善的策略模式,彻底的消除if-else。

一、策略模式基础

策略模式是oop中最著名的设计模式之一,是对方法行为的抽象,可以归类为行为设计模式,也是oop中interface经典的应用。其特点简单又实用,是我最喜欢的模式之一。策略模式定义了一个拥有共同行为的算法族,每个算法都被封装起来,可以互相替换,独立于客户端而变化。

我们可以从三个方面来理解策略模式:

  1. 算法族
    使用多种不同的处理方式,做同样的事情,仅仅是具体行为有差别。这些处理方式,组合构成算法策略族,它们的共性,体现在策略接口行为上。
  2. 算法封装
    将各个算法封装到不同的类中,这样有助于客户端来选择合适的算法。
  3. 可互相替换
    客户端可以在运行时选择使用哪个算法,而且算法可以进行替换,所以客户端依赖于策略接口。

据此,可以推断出策略模式的使用场景:

  1. 针对同一问题的多种处理方式,仅仅是具体行为有差别时;
  2. 需要安全地封装多种同一类型的操作时;
  3. 同一抽象类有多个子类,而客户端需要使用 if-else 或者 switch-case 来选择具体子类时。

策略模式UML类图如下:

可以看到策略模式涉及到三个角色:

环境(Context)角色:持有一个Strategy的引用。

抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

具体策略(ConcreteStrategy)角色:包装了相关的具体算法或行为。

策略模式典型代码如下:

     /// <summary>
/// 抽象策略类(接口)
/// </summary>
public interface Strategy
{
/// <summary>
/// 抽象策略方法
/// </summary>
void StrategyFunc();
}
/// <summary>
/// 具体策略类A
/// </summary>
public class ConcreteStrategyA : Strategy
{
public void StrategyFunc()
{
//具体方法A
}
}
/// <summary>
/// 具体策略类B
/// </summary>
public class ConcreteStrategyB : Strategy
{
public void StrategyFunc()
{
//具体方法B
}
}
/// <summary>
/// 具体策略类C
/// </summary>
public class ConcreteStrategyC : Strategy
{
public void StrategyFunc()
{
//具体方法C
}
}
/// <summary>
/// 环境
/// </summary>
public class Context
{
//持有一个具体策略的对象
private Strategy _strategy;
/// <summary>
/// 构造函数,传入一个具体策略对象
/// </summary>
/// <param name="strategy"></param>
public Context(Strategy strategy)
{
this._strategy = strategy;
}
/// <summary>
/// 调用策略方法
/// </summary>
public void ContextFunc()
{
_strategy.StrategyFunc();
}
}
/// <summary>
/// 客户端调用
/// </summary>
public class Client
{
public void Main(string param)
{
Context context;
if (param == "A")
{
context = new Context(new ConcreteStrategyA());
}
else if(param == "B")
{
context = new Context(new ConcreteStrategyB());
}
else if(param == "C")
{
context = new Context(new ConcreteStrategyC());
}
else
{
throw new Exception("没有可用的策略");
}
context.ContextFunc();
}
}

二、策略模式实例-会员策略

假设某店家推出三种会员,分别为普通会员,金牌会员和钻石会员,还有就是普通顾客,针对不同的会员顾客,购物结算时有不同的打折方式。购物后,客户的历史购物金额累计,可以自动升级到相应的会员级别。

这里的购物结算时,我们就很适宜使用策略模式,因为策略模式描述的就是算法的不同。这里举例我们就采用简单的方式,四类顾客分别采用原价(非会员普通顾客),九折、八折、七折的收钱方式。

那么我们首先要有一个计算价格的策略接口:

     /// <summary>
/// Vip算法规则
/// </summary>
public interface IVipAlgorithm
{
int CalcPrice(int originalPrice);
}

然后下面是不同会员顾客的收钱算法的具体实现:

     public enum Vip
{
普通会员 = ,
黄金会员 = ,
钻石会员 =
}
     /// <summary>
/// 非会员顾客
/// </summary>
public class VipNone : IVipAlgorithm
{
public int CalcPrice(int originalPrice)
{
return originalPrice;
}
}
     /// <summary>
/// 普通会员
/// </summary>
public class VipOrdinary : IVipAlgorithm
{
public int CalcPrice(int orginalPrice)
{
var currentPrice = (int)(orginalPrice * 0.9 + 0.5);
return currentPrice;
}
}
     /// <summary>
/// 金牌会员
/// </summary>
public class VipGold : IVipAlgorithm
{
public int CalcPrice(int orginalPrice)
{
var currentPrice = (int)(orginalPrice * 0.8 + 0.5);
return currentPrice;
}
}
     /// <summary>
/// 钻石会员
/// </summary>
public class VipDiamond: IVipAlgorithm
{
public int CalcPrice(int orginalPrice)
{
var currentPrice = (int)(orginalPrice * 0.7 + 0.5);
return currentPrice;
}
}

接下来看顾客类,顾客类中自动累计历史购物金额,判断会员级别,进行打折结算。

 public class Customer
{
public int _totalAmount = ;
public Vip? _vip = null;
public IVipAlgorithm _vipAlgorithm;
public void Buy(decimal originPriceM)
{
if (_totalAmount >= * )
{
_vip = Vip.钻石会员;
_vipAlgorithm = new VipDiamond();
}
else if (_totalAmount >= * )
{
_vip = Vip.黄金会员;
_vipAlgorithm = new VipGold();
}
else if (_totalAmount >= * )
{
_vip = Vip.普通会员;
_vipAlgorithm = new VipOrdinary();
}
else
{
_vip = null;
_vipAlgorithm = new VipNone();
}
var originPrice = (int)originPriceM * ;
var finalPrice = _vipAlgorithm.CalcPrice(originPrice);
//打印
Console.WriteLine($"您在本店历史消费总额:{_totalAmount * 0.01}元");
var vipMsg = _vip.HasValue ? $"您是本店会员:{_vip.Value.ToString()}" : "您未升级为本店会员";
Console.WriteLine(vipMsg);
Console.WriteLine($"本次购买商品原价{originPrice * 0.01}元,需支付{finalPrice * 0.01}元");
_totalAmount += originPrice;
Console.WriteLine();
}
}

最后是客户端调用,系统会帮我们自动调整结算打折策略。

     class Program
{
static void Main(string[] args)
{
Customer cust = new Customer();
cust.Buy();
cust.Buy();
cust.Buy();
cust.Buy();
cust.Buy();
cust.Buy();
cust.Buy();
Console.ReadLine();
}
}

输出结果:

可以看到随着此顾客购物金额累加,会员身份自动升级,购物结算折扣也越来越多。这样设计的好处显而易见,客户端不再依赖于具体的收费策略,依赖于抽象永远是正确的。

在上面的基础上,我们可以使用简单工厂来解耦顾客类的结算策略依赖。

     public class VipAlgorithmFactory
{
private VipAlgorithmFactory() { }
//根据客户的总金额产生相应的策略
public static IVipAlgorithm GetVipAlgorithm(Customer cust, out Vip? vipLevel)
{
if (cust._totalAmount >= * )
{
vipLevel = Vip.钻石会员;
return new VipDiamond();
}
else if (cust._totalAmount >= * )
{
vipLevel = Vip.黄金会员;
return new VipGold();
}
else if (cust._totalAmount >= * )
{
vipLevel = Vip.普通会员;
return new VipOrdinary();
}
else
{
vipLevel = null;
return new VipNone();
}
}
}

这样就将制定策略的功能从客户类分离出来,我们的客户类可以变成这样。

     public class Customer
{
public int _totalAmount = ;
public Vip? _vip = null;
public IVipAlgorithm _vipAlgorithm;
public void Buy(decimal originPriceM)
{
//变化点,我们将策略的制定转移给了策略工厂,将这部分责任分离出去
_vipAlgorithm = VipAlgorithmFactory.GetVipAlgorithm(this,out this._vip);
var originPrice = (int)originPriceM * ;
var finalPrice = _vipAlgorithm.CalcPrice(originPrice);
//
Console.WriteLine($"您在本店历史消费总额:{_totalAmount * 0.01}元");
var vipMsg = _vip.HasValue ? $"您是本店会员:{_vip.Value.ToString()}" : "您未升级为本店会员";
Console.WriteLine(vipMsg);
Console.WriteLine($"本次购买商品原价{originPrice * 0.01}元,需支付{finalPrice * 0.01}元");
_totalAmount += originPrice;
Console.WriteLine();
}
}

三、消除If-else

虽然结合简单工厂模式,我们的策略模式灵活了一些,但不免发现在工厂中仍然存在if-else判断,如果当我们增加一个会员级别,又得增加一个if-else语句,这是简单工厂的缺点,对修改开放。

那有什么方法,可以较好的解决这个问题呢?那就是使用注解,我们给策略类加上Vip级别注解,以根据Vip级别确定哪个策略生效,从而解决结算折扣策略选择的问题。另外在较复杂的系统中,我们还可以考虑使用IOC容器和依赖注入的方式来解决。这里只演示注解方式。

首先添加一个注解类,然后再为我们的每个策略类增加注解,如为钻石会员策略类增加注解[Vip(Vip.钻石会员)]。

     public class VipAttribute : Attribute
{
public Vip Vip { get; set; }
public VipAttribute(Vip vip)
{
Vip = vip;
}
}

然后增加一个配置查询类,使用单例模式,避免每次访问都去实例化配置。

     public class VipConfig
{
public readonly Dictionary<Vip, int> VipCondition;
public readonly Dictionary<Vip, IVipAlgorithm> VipAlgorithm;
private static VipConfig _vipConfigInstance;
private VipConfig()
{
//这里将配置硬编码到字典中,实际可以从配置文件中读取
VipCondition = new Dictionary<Vip, int> { { Vip.普通会员, * }, { Vip.黄金会员, * }, { Vip.钻石会员, * } };
VipAlgorithm = Assembly.GetExecutingAssembly().GetExportedTypes()
.Select(t => new
{
Type = t,
Vip = t.GetCustomAttribute<VipAttribute>()?.Vip
})
.Where(x => x.Vip != null)
.ToDictionary(x => x.Vip.Value, x => (IVipAlgorithm)Activator.CreateInstance(x.Type));
}
public static VipConfig Instance
{
get
{
if (_vipConfigInstance == null)
{
_vipConfigInstance = new VipConfig();
}
return _vipConfigInstance;
}
}
}

策略工厂仍然帮我们自动找到适应的策略,现在将我们的工厂类调整如下:

     public class VipAlgorithmFactory
{
public static IVipAlgorithm GetVipAlgorithm(Customer cust, out Vip? vipLevel)
{
var custVip = VipConfig.Instance.VipCondition.Where(x => x.Value <= cust._totalAmount)
.OrderByDescending(x => x.Value)
.ToList();
IVipAlgorithm vipAlgorithm = null;
if (custVip.Count == )
{
vipLevel = null;
vipAlgorithm = new VipNone();
}
else
{
vipLevel = custVip.First().Key;
vipAlgorithm = VipConfig.Instance.VipAlgorithm[vipLevel.Value];
}
return vipAlgorithm;
}
}

最终的顾客类相应调整如下:

     public class Customer
{
public int _totalAmount = ;
public Vip? _vip = null;
public IVipAlgorithm _vipAlgorithm;
public void Buy(decimal originPriceM)
{
_vipAlgorithm = VipAlgorithmFactory.GetVipAlgorithm(this, out this._vip);
var originPrice = (int)originPriceM * ;
var finalPrice = _vipAlgorithm.CalcPrice(originPrice);
//
Console.WriteLine($"您在本店历史消费总额:{_totalAmount * 0.01}元");
var vipMsg = _vip.HasValue ? $"您是本店会员:{_vip.Value.ToString()}" : "您未升级为本店会员";
Console.WriteLine(vipMsg);
Console.WriteLine($"本次购买商品原价{originPrice * 0.01}元,需支付{finalPrice * 0.01}元");
_totalAmount += originPrice;
Console.WriteLine();
}
}

最终,通过单例、工厂、注解、反射共同配合,配置读取单例化,实现了更加完善的策略模式,消除了if-else。

四、总结

一,策略模式在.NET中的应用

在.NET Framework中也不乏策略模式的应用例子。例如,在.NET中,为集合类型ArrayList和List<T>提供的排序功能,其中实现就利用了策略模式,定义了IComparer接口来对比较算法进行封装,实现IComparer接口的类可以是顺序,或逆序地比较两个对象的大小,还可以使用快速排序,希尔排序,归并排序类等。具体.NET中的实现可以使用反编译工具查看List<T>.Sort(IComparer<T>)的实现。其中List<T>就是承担着环境角色,而IComparer<T>接口承担着抽象策略角色,具体的策略角色就是实现了IComparer<T>接口的类,List<T>类本身存在实现了该接口的类,我们可以自定义继承于该接口的具体策略类。

二,策略模式的优缺点

优点

  • 易于扩展,增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合开放封闭原则
  • 避免使用多重条件选择语句,充分体现面向对象设计思想
  • 策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换
  • 每个策略类使用一个策略类,符合单一职责原则
  • 客户端与策略算法解耦,两者都依赖于抽象策略接口,符合依赖反转原则
  • 客户端不需要知道都有哪些策略类,符合最小知识原则

缺点

  • 策略模式,当策略算法太多时,会造成很多的策略类
  • 客户端不知道有哪些策略类,不能决定使用哪个策略类,这点可以通过本文中的方式解决,也可以考虑使用IOC容器和依赖注入的方式来解决

三,结论

策略模式、状态模式、责任链模式都是在解决if-else的问题,但目的不太一样。策略模式主要是对方法的封装,把一系列方法封装到一系列的策略类中,客户端可以根据不同情况选择使用适宜的策略类,不同的策略类可以自由切换。

策略模式除了在OOP中实践,也可以在FP中应用。OOP让我们以抽象设计的角度看系统,而FP让我们以简化设计的角度看系统,我们建议以OOP做第一阶段的重构,再辅以FP做第二阶段的重构,可以解决OOP容易过度设计的问题。在这里即可以进一步解决策略类过多的问题,比如策略接口可以退化为一个委托,或如Func<T1,T2>,C#向FP迈进,都是起源于委托的。然后策略类通常只有一个方法,所以真的有必要建立这么多类吗?完全可以退化为一个静态类加多个静态策略方法的形式,还有工厂类则从返回策略类实例变化为返回策略方法,这就是辅以FP思想重构C#策略模式的思路。

C# 1.0主要是OOP,C# 2.0主要是泛型,C# 3.0之后主要是FP部分,如Lambda、Linq,C# 也是在3.0开始与Java分道扬镳,朝向OOP+FP双hybrid语言目标推进,尤其到C# 7.0非常明显,如Tuple、Descontruction、Pattern Matching、Local Function...都是FP语言基本的机制。

完整的示例代码已放置于 https://github.com/gitsongs/StrategyPattern

--End

设计完美的策略模式,消除If-else的更多相关文章

  1. ASP.net之策略模式

    设计思路: 用ASP.net设计,调用策略模式.在第一个数和第二个数的文本框中输入数值,单击录题按钮,数值保存在n1,n2文档中,把要做的题都保存完后,单击开始按钮,开始做题,做完单击判断按钮,进行判 ...

  2. js-设计模式学习笔记-策略模式

    策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换(相互替换:表现为它们具有相同的目标和意图). 策略模式的目的是讲算法的使用与算法的实现分离开来. 一个基于策略模式的程 ...

  3. 策略模式与SPI机制,到底有什么不同?

    这里说的策略模式是一种设计模式,经常用于有多种分支情况的程序设计中.例如我们去掉水果皮,一般来说对于不同的水果,会有不同的拨皮方式.此时用程序语言来表示是这样的: if(type == apple){ ...

  4. 设计模式之策略模式(iOS开发,代码用Objective-C展示)

    在实际开发过程中,app需求都是由产品那边给出,往往是他给出第一版功能,我们写好代码后,会相应的给出第二版.第三版功能,而这些功能是在实际使用中,根据用户需求而不断增加的.如果在编码之初,我们并未认识 ...

  5. Java的设计模式(5)-- 策略模式

    定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换,本模式使得算法可以独立于使用它的客户而变化.策略模式包括以下三种角色 策略(Strategy):策略是一个接口,该接口定义若干个算法标识, ...

  6. php模式设计之 策略模式

    策略模式: 策略模式设计帮助构建的对象不必自身包含逻辑,而是能够根据需要利用其他对象中的算法. 使用场景: 例如有一个CD类,我们类存储了CD的信息. 原先的时候,我们在CD类中直接调用getCD方法 ...

  7. [一]Head First设计模式之【策略模式】(鸭子设计的优化历程)

    public abstract class Duck { FlyBehavior flyBehavior; QuackBehavior quackBehavior; public Duck() { } ...

  8. php 23种设计模型 - 策略模式

    介绍 意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换,用户还不需要知道其具体的实现 主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护. 何时使 ...

  9. python设计模式之策略模式

    每次看到项目中存在大量的if else代码时,都会心生一丝不安全感. 特别是产品给的需求需要添加或者更改一种if条件时,生怕会因为自己的疏忽而使代码天崩地裂,哈哈,本文的目的就是来解决这种不安全感的, ...

随机推荐

  1. IScroll5不能滑到最底端的解决办法

    IScroll总体上用起来比较简单,但是如果用不好的可能会产生底部一点滚动不上去的问题. 环境:weui+iscroll5 整体布局及id如下 searchbarwrapper   divscroll ...

  2. shell 生成目录的树状视图、生成文件及子目录的汇总信息

    -exec 创建子shell $ find . -exec sh -c 'echo -n {} | tr -d "[:alnum:]_.\-" | \ tr "/&quo ...

  3. Springboot 项目启动后执行某些自定义代码

    Springboot 项目启动后执行某些自定义代码 Springboot给我们提供了两种"开机启动"某些方法的方式:ApplicationRunner和CommandLineRun ...

  4. vmware虚拟机磁盘挂载

    执行mount命令时找不到介质或者mount:no medium found的解决办法 使用vmware时,在虚拟机设置里,设置CD/DVD为系统镜像,挂载时,有时会有找不到介质或者no medium ...

  5. LOJ #6485 LJJ 学二项式定理

    QwQ LOJ #6485 题意 求题面中那个算式 题解 墙上暴利 设$ f(x)=(sx+1)^n$ 假设求出了生成函数$ f$的各项系数显然可以算出答案 因为模$ 4$的缘故只要对于每个余数算出次 ...

  6. JavaScript—对象创建方式

    JavaScript 也是面向对象的语言(oop) 之前学JavaScript 没有学对象.现在做下笔记 创建对象的方式: 1.  对象字面量 const hero = { name: '吕布', w ...

  7. 2018-2019-2 20165234 《网络对抗技术》 Exp5 MSF基础应用

    实验五 MSF基础应用 实验内容 本实践目标是掌握metasploit的基本应用方式,重点常用的三种攻击方式的思路.具体需要完成: 1.一个主动攻击实践,ms08_067(成功) 2. 一个针对浏览器 ...

  8. python算法&二分查找法

    import random def random_list(n): result = [] ids = list(range(1001,1001+n)) a1 = ["赵",&qu ...

  9. rabbitmq - (消息队列) 的基本原理介绍

    介绍 MQ全称为Message Queue, 是一种分布式应用程序的的通信方法,它是消费-生产者模型的一个典型的代表,producer往消息队列中不断写入消息,而另一端consumer则可以读取或者订 ...

  10. python元组类型的变量以及字符串类型的变量作为参数进行传值

    今天做selenium元素对象剥离时(我把元素对象都放到了元组类型的变量中,格式:user = (“id”,“X-Auto-2”)),遇到一个元组变量,以及str字符串变量一起作为参数传值的问题,发现 ...