zoukankan      html  css  js  c++  java
  • 设计完美的策略模式,消除If-else

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

    一、策略模式基础

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

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

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

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

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

    策略模式UML类图如下:

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

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

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

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

    策略模式典型代码如下:

     1     /// <summary>
     2     /// 抽象策略类(接口)
     3     /// </summary>
     4     public interface Strategy
     5     {
     6         /// <summary>
     7         /// 抽象策略方法
     8         /// </summary>
     9         void StrategyFunc();
    10     }
    11     /// <summary>
    12     /// 具体策略类A
    13     /// </summary>
    14     public class ConcreteStrategyA : Strategy
    15     {
    16         public void StrategyFunc()
    17         {
    18             //具体方法A
    19         }
    20     }
    21     /// <summary>
    22     /// 具体策略类B
    23     /// </summary>
    24     public class ConcreteStrategyB : Strategy
    25     {
    26         public void StrategyFunc()
    27         {
    28             //具体方法B
    29         }
    30     }
    31     /// <summary>
    32     /// 具体策略类C
    33     /// </summary>
    34     public class ConcreteStrategyC : Strategy
    35     {
    36         public void StrategyFunc()
    37         {
    38             //具体方法C
    39         }
    40     }
    41     /// <summary>
    42     /// 环境
    43     /// </summary>
    44     public class Context
    45     {
    46         //持有一个具体策略的对象
    47         private Strategy _strategy;
    48         /// <summary>
    49         /// 构造函数,传入一个具体策略对象
    50         /// </summary>
    51         /// <param name="strategy"></param>
    52         public Context(Strategy strategy)
    53         {
    54             this._strategy = strategy;
    55         }
    56         /// <summary>
    57         /// 调用策略方法
    58         /// </summary>
    59         public void ContextFunc()
    60         {
    61             _strategy.StrategyFunc();
    62         }
    63     }
    64     /// <summary>
    65     /// 客户端调用
    66     /// </summary>
    67     public class Client
    68     {
    69         public void Main(string param)
    70         {
    71             Context context;
    72             if (param == "A")
    73             {
    74                 context = new Context(new ConcreteStrategyA());
    75             }
    76             else if(param == "B")
    77             {
    78                 context = new Context(new ConcreteStrategyB());
    79             }
    80             else if(param == "C")
    81             {
    82                 context = new Context(new ConcreteStrategyC());
    83             }
    84             else
    85             {
    86                 throw new Exception("没有可用的策略");
    87             }
    88             context.ContextFunc();
    89         }
    90     }
    View Code

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

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

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

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

    1     /// <summary>
    2     /// Vip算法规则
    3     /// </summary>
    4     public interface IVipAlgorithm
    5     {
    6         int CalcPrice(int originalPrice);
    7     }

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

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

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

     1 public class Customer
     2     {
     3         public int _totalAmount = 0;
     4         public Vip? _vip = null;
     5         public IVipAlgorithm _vipAlgorithm;
     6         public void Buy(decimal originPriceM)
     7         {
     8             if (_totalAmount >= 5000 * 100)
     9             {
    10                 _vip = Vip.钻石会员;
    11                 _vipAlgorithm = new VipDiamond();
    12             }
    13             else if (_totalAmount >= 3000 * 100)
    14             {
    15                 _vip = Vip.黄金会员;
    16                 _vipAlgorithm = new VipGold();
    17             }
    18             else if (_totalAmount >= 1000 * 100)
    19             {
    20                 _vip = Vip.普通会员;
    21                 _vipAlgorithm = new VipOrdinary();
    22             }
    23             else
    24             {
    25                 _vip = null;
    26                 _vipAlgorithm = new VipNone();
    27             }
    28             var originPrice = (int)originPriceM * 100;
    29             var finalPrice = _vipAlgorithm.CalcPrice(originPrice);
    30             //打印
    31             Console.WriteLine($"您在本店历史消费总额:{_totalAmount * 0.01}元");
    32             var vipMsg = _vip.HasValue ? $"您是本店会员:{_vip.Value.ToString()}" : "您未升级为本店会员";
    33             Console.WriteLine(vipMsg);
    34             Console.WriteLine($"本次购买商品原价{originPrice * 0.01}元,需支付{finalPrice * 0.01}元");
    35             _totalAmount += originPrice;
    36             Console.WriteLine();
    37         }
    38     }

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

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             Customer cust = new Customer();
     6             cust.Buy(1000);
     7             cust.Buy(1000);
     8             cust.Buy(1000);
     9             cust.Buy(1000);
    10             cust.Buy(1000);
    11             cust.Buy(1000);
    12             cust.Buy(1000);
    13             Console.ReadLine();
    14         }
    15     }

     输出结果:

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

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

     1     public class VipAlgorithmFactory
     2     {
     3         private VipAlgorithmFactory() { }
     4         //根据客户的总金额产生相应的策略
     5         public static IVipAlgorithm GetVipAlgorithm(Customer cust, out Vip? vipLevel)
     6         {
     7             if (cust._totalAmount >= 5000 * 100)
     8             {
     9                 vipLevel = Vip.钻石会员;
    10                 return new VipDiamond();
    11             }
    12             else if (cust._totalAmount >= 3000 * 100)
    13             {
    14                 vipLevel = Vip.黄金会员;
    15                 return new VipGold();
    16             }
    17             else if (cust._totalAmount >= 1000 * 100)
    18             {
    19                 vipLevel = Vip.普通会员;
    20                 return new VipOrdinary();
    21             }
    22             else
    23             {
    24                 vipLevel = null;
    25                 return new VipNone();
    26             }
    27         }
    28     }

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

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

     三、消除If-else

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

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

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

    1     public class VipAttribute : Attribute
    2     {
    3         public Vip Vip { get; set; }
    4         public VipAttribute(Vip vip)
    5         {
    6             Vip = vip;
    7         }
    8     }

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

     1     public class VipConfig
     2     {
     3         public readonly Dictionary<Vip, int> VipCondition;
     4         public readonly Dictionary<Vip, IVipAlgorithm> VipAlgorithm;
     5         private static VipConfig _vipConfigInstance;
     6         private VipConfig()
     7         {
     8             //这里将配置硬编码到字典中,实际可以从配置文件中读取
     9             VipCondition = new Dictionary<Vip, int> { { Vip.普通会员, 1000 * 100 }, { Vip.黄金会员, 3000 * 100 }, { Vip.钻石会员, 5000 * 100 } };
    10             VipAlgorithm = Assembly.GetExecutingAssembly().GetExportedTypes()
    11                         .Select(t => new
    12                         {
    13                             Type = t,
    14                             Vip = t.GetCustomAttribute<VipAttribute>()?.Vip
    15                         })
    16                         .Where(x => x.Vip != null)
    17                         .ToDictionary(x => x.Vip.Value, x => (IVipAlgorithm)Activator.CreateInstance(x.Type));
    18         }
    19         public static VipConfig Instance
    20         {
    21             get
    22             {
    23                 if (_vipConfigInstance == null)
    24                 {
    25                     _vipConfigInstance = new VipConfig();
    26                 }
    27                 return _vipConfigInstance;
    28             }
    29         }
    30     }

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

     1     public class VipAlgorithmFactory
     2     {
     3         public static IVipAlgorithm GetVipAlgorithm(Customer cust, out Vip? vipLevel)
     4         {
     5             var custVip = VipConfig.Instance.VipCondition.Where(x => x.Value <= cust._totalAmount)
     6                 .OrderByDescending(x => x.Value)
     7                 .ToList();
     8             IVipAlgorithm vipAlgorithm = null;
     9             if (custVip.Count == 0)
    10             {
    11                 vipLevel = null;
    12                 vipAlgorithm = new VipNone();
    13             }
    14             else
    15             {
    16                 vipLevel = custVip.First().Key;
    17                 vipAlgorithm = VipConfig.Instance.VipAlgorithm[vipLevel.Value];
    18             }
    19             return vipAlgorithm;
    20         }
    21     }

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

     1     public class Customer
     2     {
     3         public int _totalAmount = 0;
     4         public Vip? _vip = null;
     5         public IVipAlgorithm _vipAlgorithm;
     6         public void Buy(decimal originPriceM)
     7         {
     8             _vipAlgorithm = VipAlgorithmFactory.GetVipAlgorithm(this, out this._vip);
     9             var originPrice = (int)originPriceM * 100;
    10             var finalPrice = _vipAlgorithm.CalcPrice(originPrice);
    11             //
    12             Console.WriteLine($"您在本店历史消费总额:{_totalAmount * 0.01}元");
    13             var vipMsg = _vip.HasValue ? $"您是本店会员:{_vip.Value.ToString()}" : "您未升级为本店会员";
    14             Console.WriteLine(vipMsg);
    15             Console.WriteLine($"本次购买商品原价{originPrice * 0.01}元,需支付{finalPrice * 0.01}元");
    16             _totalAmount += originPrice;
    17             Console.WriteLine();
    18         }
    19     }

    最终,通过单例、工厂、注解、反射共同配合,配置读取单例化,实现了更加完善的策略模式,消除了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

  • 相关阅读:
    遇到屏蔽selenium的站点如何突破
    subprocess.Popen stdout重定向内容实时获取
    thinkphp Composer安装指南
    职场片
    php。。。
    多线程相关
    狂刷1000题~~2
    狂刷1000题~~1
    关于eclipse中看不到源码的问题
    一篇看懂++i i++
  • 原文地址:https://www.cnblogs.com/jiujiduilie/p/9191629.html
Copyright © 2011-2022 走看看