学习设计模式《十六》——策略模式
一、基础概念
策略模式的本质是【分离算法,选择实现】。
策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使他们可以相互替换。本模式使得算法可独立于使用它的客户而变化。
策略模式的上下文(context)和策略(Strategy)关系:通常上下文使用具体的策略实现对象。反过来,策略实现对象也可以从上下文获取所需的数据。因此可以将上下文当做参数传递给策略实现对象,这种情况下上下文和策略实现对象是紧密耦合的(这种情况下,上下文封装着具体策略对象进行算法运算所需要的数据,具体策略对象通过回调上下文方法来获取这些数据)。某些情况下,策略实现对象还可以回调上下文的方法来实现一定的功能,这种使用场景,上下文变相充当了多个策略算法实现的公共接口。在上下文定义的方法可以当做是所有或者是部分策略算法使用的公共功能。
请注意:由于所有的策略实现对象都实现同一个策略接口,传入同一个上下文,可能会造成传入的上下文数据的浪费,因为有的算法会使用这些数据,而有的算法不会使用,但是上下文和策略对象之间交互的开销是存在的。
序号 | 认识策略模式 | 说明 |
1 | 策略模式的功能 | 把具体的算法实现从具体的业务处理中独立出来,把它们实现成为单独的算法类,从而形成一系列的算法,并且让这些算法可以相互替换。 【策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性】 |
2 | 策略模式和if-else语句 | 每个策略算法具体实现的功能,就是原来在if-else结构中的具体实现(其实多个if-elseif语句表达的就是一个平等的功能结构,你要么执行if,要么执行else或者elseif,从运行地位上来说它们都是平等的)。 策略模式就是把各个平等的具体实现封装到单独的策略实现类,然后通过上下文与具体的策略类进行交互。因此多个if-else语句就可以考虑使用策略模式。 |
3 | 算法的平等性 | 策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正式因为这个平等性,才能实现算法之间的相互替换。 所有的策略算法在实现杀昂也是相互独立的,相互之间没有依赖。【即:策略算法是相同行为的不同实现】 |
4 | 谁来选择具体的策略算法 | 在策略模式中可以在两个地方进行具体策略的选择: 《1》在客户端,当使用上下文的时候,由客户端来选择具体的策略算法;然后把这个策略算法设置给上下文。 《2》客户端不管,由上下文来选择具体的策略算法。 |
5 | 策略(strategy)的实现方式 | 一般情况下策略都是使用接口来定义的; 但是如果多个算法具有公共功能的话,可以把策略实现为抽象类,然后把多个算法的功能功能实现到策略中。 |
6 | 运行时策略的唯一性 | 运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个。 |
7 | 增加新的策略 | 新建一个类策略算法类继承策略接口或模板来实现新的要求,然后在客户端使用时指定新的策略算法类就可以了。 |
序号 | 策略模式的优点 | 策略模式的缺点 |
1 | 定义一系列的算法 定义一系列的算法,实现让这些算法可以相互替换【所以会为这一系列算法定一个公共的接口,以约束一系列算法要实现的功能。如果这一系列的算法具有公共功能,可以把策略接口实现为抽象类,把这些公共功能实现到父类中】 | 客户必须了解每种策略的不同 如:让客户端来选择具体使用哪一种策略,这就需要客户了解所有的策略,还要了解各种策略的功能和不同,这样才能做出正确的选择,而且这也暴露了策略的具体实现。 |
2 | 避免多重条件语句 策略模式的一系列策略算法是平等的,可以互换,写在一起就是通过【if-else】结构或【Switch】来组织,如果此时具体的算法实现中又有条件语句,就构成了多重条件语句,使用策略模式可以避免这样的多种条件语句。 | 增加了对象数目 由于策略模式把每个具体的策略实现都单独封装成为了类,如果备选的策略很多,那么对象的数目就会很可观。 |
3 | 更好的扩展性 在策略模式中扩展新的策略非常容易,只要增加新的策略实现类,然后在使用策略的地方使用这个新的策略实现就可以了。 | 只适合扁平的算法结构 策略模式的一系列算法地位是平等的,是可以相互替换的;事实上构成了一个扁平的算法结构,也就是在一个策略接口下,有很多平等的策略算法,就相当于兄弟算法,而且运行时刻只有一个算法被使用,这就限制了算法使用的层级,使用的时候不能嵌套使用。对于需要嵌套使用多个算法的情况(如:折上折、折后返券等业务的实现),需要组合或是嵌套使用多个算法的情况,可以考虑使用装饰模式,或是变形的职责链,或是AOP等方式来实现。 |
何时选用策略模式?
1、出现有许多相关的类,仅仅是行为有差别的情况下,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法的动态切换。
2、出现同一个算法,有很多不同实现的情况下,可以使用策略模式把这些“不同的实现”实现成为一个算法的类层次。
3、需要封装算法中,有与算法相关数据的情况下,可以使用策略模式来避免暴露这些跟算法相关的数据结构。
4、出现抽象一个定义了很多行为的类,并且是通过多个if-else语句来选择这些行为的情况下,可以使用策略模式来代替这些条件语句。
二、策略模式示例
业务需求:销售部门的人需要向客户报价,这是一个非常重大和复杂的问题,对不同的客户要报不同的价格,如:
1、对普通客户或者是新客户报的是全价;
2、对老客户报的价格,根据客户年限,给于一定的折扣;
3、对大客户报的价格,根据大客户的累计消费金额,给于一定的折扣;
4、还要考虑客户购买的数量和金额(如:虽然是新用户,但是一次购买的数量非常大,或者总金额非常高,也会有一定的折扣);
5、还有报价人员的职务高低,也决定了他是否有权限对价格进行一定的浮动折扣。
6、甚至在不同的阶段,对客户的报价也不同。一般情况是刚开始比较高,越接近成交阶段,报价越趋于合理。总之,向客户报价是非常复杂的。
我们这里为了演示的简洁性,现在实现一个简化的报价管理功能:
1、对普通客户或新客户报全价;
2、对老客户报的价格,统一折扣5%;
3、对大客户报的价格,统一折扣10%;
2.1、不使用模式的示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern
{/// <summary>/// 价格管理,主要完成计算向客户所报价格功能/// </summary>internal class PriceOne{//报价,对不同类型,计算不同的价格public double Quote(double goodsPrice,string customerType){if ("普通客户".Equals(customerType)){Console.WriteLine("对于新客户或普通客户,没有折扣");return goodsPrice;}else if ("老客户".Equals(customerType)){Console.WriteLine("对于老客户,统一折扣5%");return goodsPrice*(1-0.05);}else if ("大客户".Equals(customerType)){Console.WriteLine("对于大客户,统一折扣10%");return goodsPrice * (1-0.1);}//其余人都是报原价return goodsPrice;}}//Class_end
}
这个写法是实现了我们的现有的需求,但是仔细想一想,这样实现还是存在一些问题:
《1》价格类包含了所有计算报价的算法,使得价格类,尤其是报价这个方法比较庞杂,难以维护。
《2》在不同的时候,需要使用不同的计算方式(如,在公司周年庆的时候,所有客户额外增加3%的折扣,在换季促销的时候,普通客户是额外增加折扣2%,老客户是额外增加3%,大客户是额外增加折扣5%,这意味着计算报价的方式会被经常修改,或者被切换);通常是切换,因为过了促销时间,又回到正常的价格体系了;而目前现有的价格类中计算报价的方法,是固定的各种计算方式,这使得切换调用不同的计算方式很麻烦,每次都需要修改if-else中的调用代码。
针对《1》中价格类的报价方法庞杂问题,还可以将每种类型的报价单独设置为对应的计算方法如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern
{//价格管理,主要完成计算向客户所报价格的功能internal class PriceTwo{//报价,对不同类型,计算不同的价格public double Quote(double goodsPrice,string customerType){if ("普通客户".Equals(customerType)){return CaculatePriceForNormal(goodsPrice);}else if ("老客户".Equals(customerType)){return CaculatePriceForOld(goodsPrice);}else if ("大客户".Equals(customerType)){return CaculatePriceForLarge(goodsPrice);}//其余人都是报原价return goodsPrice;}//为新客户或普通顾客计算应报的价格private double CaculatePriceForNormal(double goodsPrice){Console.WriteLine("对于新客户或普通客户,没有折扣");return goodsPrice;}//为老客户计算应报的价格private double CaculatePriceForOld(double goodsPrice){Console.WriteLine("对于老客户,统一折扣5%");return goodsPrice * (1 - 0.05);}//为大客户计算应报的价格private double CaculatePriceForLarge(double goodsPrice){Console.WriteLine("对于大客户,统一折扣10%");return goodsPrice * (1 - 0.1);}}//Class_end
}
这样修改后,虽然是每个独立的报价内容在各自的方法中了;但是问题还存在(假如有100个或者更多这样的计算方式,会使得这个价格类非常庞大,难以维护。而且维护和扩展都需要去修改已有的代码,这样很不好,风险较大,且违反了开闭原则) 。
那如何实现,才能够让价格类中的计算报价算法,能够很容易的可维护、可扩展且能够动态的切换变化呢?——《策略模式》就是解决这类问题的
2.2、策略模式的示例1——典型
问题分析:上面的两种实现方式,根据原因在于算法和使用算法的客户是耦合的,甚至是密不可分的。
针对业务需求我们分析后,抽象一下是: 各种计算报价的计算方式就好比是具体的算法,而使用这些计算方式来计算报价的程序,就相当于使用算法的客户。
现在按照策略模式的方式来解决这个问题:应该先把所有的计算方式独立出来,每个计算方式做成一个单独的算法类,从而形成一系列的算法,并且为这一系列的算法定义一个公共的接口,这些算法实现是同一接口的不同实现,地位是平等的,可以相互替换。这样一来,要扩展新的算法只用增加一个新的算法类,要维护某个算法,也只是修改某个具体的算法实现即可,不会对其他代码造成影响;这样就解决了可维护、可扩展的问题。【为了实现算法能独立于使用他的客户,策略模式引入了一个上下文对象,这个对象负责持有算法,设置到上下文对象中,让上下文对策持有客户选择的算法,当客户通知上下文对象执行功能时,上下文对象则调用具体的算法。这样一来,具体的算法和直接使用算法的客户是分离的】。
2.2.1、定义策略接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemo
{/// <summary>/// 策略,定义计算报价算法的接口/// </summary>internal interface IStrategy{/// <summary>/// 计算应报的价格/// </summary>/// <param name="goodsPrice">商品的价格(原价)</param>/// <returns></returns>double CaculatePrice(double goodsPrice);}//Interface_end
}
2.2.2、实现不同的算法
《1》新客户或普通用户计算应报价格
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemo
{/// <summary>/// 具体的算法实现,为新客户或普通用户计算应报价格/// </summary>internal class NormalCustomerStrategy : IStrategy{public double CaculatePrice(double goodsPrice){Console.WriteLine("\n对于新客户或普通用户,没有折扣");return goodsPrice;}}//Class_end
}
《2》为老客户计算应报价格
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemo
{/// <summary>/// 具体的算法实现,为老客户计算应报价格/// </summary>internal class OldCustomerStrategy : IStrategy{public double CaculatePrice(double goodsPrice){Console.WriteLine("\n对于老客户,统一折扣5%");return goodsPrice * (1 - 0.05);}}//Class_end
}
《3》为大客户计算应报的价格
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemo
{/// <summary>/// 具体的算法实现,为大客户计算应报的价格/// </summary>internal class LargeCustomerStrategy : IStrategy{public double CaculatePrice(double goodsPrice){Console.WriteLine("\n对于大客户,统一折扣10%");return goodsPrice * (1 - 0.1);}}//Class_end
}
2.2.3、上下文的实现(客户使用的价格类)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemo
{/// <summary>/// 价格管理,主要完成计算向客户报价格功能/// </summary>internal class Price{//持有一个具体的策略对象private IStrategy strategy = null;/// <summary>/// 构造方法/// </summary>/// <param name="strategy">具体的策略对象</param>public Price(IStrategy strategy){this.strategy = strategy;}/// <summary>/// 计算给客户的报价/// </summary>/// <param name="goodsPrice">商品价格(原价)</param>/// <returns></returns>public double Quote(double goodsPrice){return this.strategy.CaculatePrice(goodsPrice);}}//Class_end
}
2.2.4、客户端测试
namespace StrategyPattern
{internal class Program{static void Main(string[] args){PriceStrategyDemoTest();Console.ReadLine();}/// <summary>/// 价格策略示例测试/// </summary>private static void PriceStrategyDemoTest(){Console.WriteLine("------价格策略示例测试------");//商品价格原价double goodsPrice = 1000;Console.WriteLine($"商品的原价是【{goodsPrice}】\n");1-选择并创建需要使用的策略对象【普通用户或新客户策略】//StrategyDemo.IStrategy strategy = new StrategyDemo.NormalCustomerStrategy();2-创建上下文//StrategyDemo.Price price = new StrategyDemo.Price(strategy);3-计算报价//double caculatePrice = price.Quote(goodsPrice);//Console.WriteLine($"报价是【{caculatePrice}】");老客户策略//strategy = new StrategyDemo.OldCustomerStrategy();//price = new StrategyDemo.Price(strategy);//caculatePrice = price.Quote(goodsPrice);//Console.WriteLine($"报价是【{caculatePrice}】");大客户策略//strategy = new StrategyDemo.LargeCustomerStrategy();//price = new StrategyDemo.Price(strategy);//caculatePrice = price.Quote(goodsPrice);//Console.WriteLine($"报价是【{caculatePrice}】");//普通用户报价CaculatePrice(goodsPrice,new StrategyDemo.NormalCustomerStrategy());//老客户报价CaculatePrice(goodsPrice,new StrategyDemo.OldCustomerStrategy());//大客户策略CaculatePrice(goodsPrice,new StrategyDemo.LargeCustomerStrategy());}private static void CaculatePrice(double goodsPrice,StrategyDemo.IStrategy strategy){//1-获取到具体的策略if (strategy == null) return;//2-创建上下文StrategyDemo.Price price = new StrategyDemo.Price(strategy);//3-计算报价double caculatePrice = price.Quote(goodsPrice);Console.WriteLine($"报价是【{caculatePrice}】");}}//Class_end
}
2.2.5、运行结果
2.2.6、增加新的策略
比如我们现在需要新增一个战略合作客户给出计算后的报价;战略客户的原价的8折;
《1》新创建一个类继承策略接口实现战略客户的折扣算法
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemo
{/// <summary>/// 新增的具体算法,为战略合作客户计算应报的价格/// </summary>internal class CooperateCustomerStragegy : IStrategy{public double CaculatePrice(double goodsPrice){Console.WriteLine("\n对于战略客户,统一八折");return goodsPrice * 0.8;}}//Class_end
}
《2》客户端测试
namespace StrategyPattern
{internal class Program{static void Main(string[] args){PriceStrategyDemoTest();Console.ReadLine();}/// <summary>/// 价格策略示例测试/// </summary>private static void PriceStrategyDemoTest(){Console.WriteLine("------价格策略示例测试------");//商品价格原价double goodsPrice = 1000;Console.WriteLine($"商品的原价是【{goodsPrice}】\n");//普通用户报价CaculatePrice(goodsPrice,new StrategyDemo.NormalCustomerStrategy());//老客户报价CaculatePrice(goodsPrice,new StrategyDemo.OldCustomerStrategy());//大客户策略CaculatePrice(goodsPrice,new StrategyDemo.LargeCustomerStrategy());//新增的战略合作客户策略CaculatePrice(goodsPrice, new StrategyDemo.CooperateCustomerStragegy());}private static void CaculatePrice(double goodsPrice,StrategyDemo.IStrategy strategy){//1-获取到具体的策略if (strategy == null) return;//2-创建上下文StrategyDemo.Price price = new StrategyDemo.Price(strategy);//3-计算报价double caculatePrice = price.Quote(goodsPrice);Console.WriteLine($"报价是【{caculatePrice}】");}}//Class_end
}
《3》运行结果
2.3、策略模式的示例2——将上下文作为参数传递给策略对象
业务需求:工资支付方式问题;很多企业的工资支付方式很灵活,可支付方式较多(如:人民币现金支付、美元现金支付、银行转账到工资账户、银行转账到工资卡;一些创业型的企业为了留住骨干员工,还有可能工资转股权等方式等等)。而随着公司的发展,会不断有新的工资支付方式出现,这就要求能够方便扩展。且工作支付方式不固定,是由公司和员工协商确定的(即:可能不同的员工采用不同的支付方式,甚至同一个员工,不同时间采用的支付方式也会不同,这就要求能够很方便的切换具体支付方式)。
需求分析:1、不同的策略算法需要的数据不一样(现金支付就不用银行卡号、银行转账就需要银行卡号)这就导致在设计策略接口中的方法时,不好确定参数的个数;而且,就算现在把所有的参数都列上了,今后扩展呢?难道再来修改接口吗?如果这样做,那无异于一场灾难;加入一个新策略,就需要修改接口,然后修改所有已有的实现,不疯掉才怪。那么到底如何实现,在今后的扩展时才最方便呢?
我们可以将上下文当做参数传递给策略对象,这样一来,如果要扩展新的策略实现,只需要扩展上下文就可以了,已有的实现不需要做任何修改。
2.3.1、定义工资支付的策略接口
也就是定义一个支付工资的方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoTwo
{/// <summary>/// 支付工资的策略接口,公司有多重支付工资的算法(如:现金、银行卡、现金加股票、现金加期权、美元支付等)/// </summary>internal interface IPayStrategy{/// <summary>/// 公司给某人真正支付工资策略/// </summary>/// <param name="payContext">支付工资的上下文(包含所需的数据)</param>void Pay(PayContext payContext);}//Interface_end
}
2.3.2、实现多种具体的支付策略
《1》人民币现金支付策略
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoTwo
{/// <summary>/// 人民币现金支付/// </summary>internal class RMBCashPay : IPayStrategy{public void Pay(PayContext payContext){Console.WriteLine($"\n现在给【{payContext.Name}】使用人民币现金支付【{payContext.Money}】元");}}//Class_end
}
《2》美元现金支付策略
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoTwo
{/// <summary>/// 美金现金支付/// </summary>internal class DollarCashPay : IPayStrategy{public void Pay(PayContext payContext){Console.WriteLine($"\n现在给【{payContext.Name}】使用美元现金支付【{payContext.Money}】美金");}}//Class_end
}
2.3.3、支付上下文的实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoTwo
{/// <summary>/// 支付工资的上下文(每个人支付工资的方式不同、工资不同)/// </summary>internal class PayContext{//应被支付工资的人员(我们这里为了示意就用名字了)private string name = string.Empty;//应被支付的工资金额private double money = 0.0;//支付工资的策略private IPayStrategy payStrategy=null;public string Name { get => name; }public double Money { get => money; }public IPayStrategy PayStrategy { get => payStrategy; }/// <summary>/// 构造函数/// </summary>/// <param name="name">应被支付工资的人员</param>/// <param name="money">应被支付的工资金额</param>/// <param name="payStrategy">支付工资的策略</param>public PayContext(string name,double money,IPayStrategy payStrategy){this.name = name;this.money = money;this.payStrategy = payStrategy;}/// <summary>/// 立即支付工资/// </summary>public void PayNow(){this.payStrategy.Pay(this);}}//Class_end
}
2.3.4、客户端测试
namespace StrategyPattern
{internal class Program{static void Main(string[] args){SlarayPayStrategyTest();Console.ReadLine();}/// <summary>/// 工资支付策略测试/// </summary>private static void SlarayPayStrategyTest(){Console.WriteLine("------工资支付策略测试------");//1-创建相应的支付策略StrategyDemoTwo.IPayStrategy payStrategy_RMB = new StrategyDemoTwo.RMBCashPay();StrategyDemoTwo.IPayStrategy payStrategy_Dollar=new StrategyDemoTwo.DollarCashPay();//2-准备小李的支付工资上下文StrategyDemoTwo.PayContext payContext1 = new StrategyDemoTwo.PayContext("张三",8000,payStrategy_RMB);//3-向小李支付工资payContext1.PayNow();//切换一个人支付工资StrategyDemoTwo.PayContext payContext2 = new StrategyDemoTwo.PayContext("Jack",6000,payStrategy_Dollar);payContext2.PayNow();}}//Class_end
}
2.3.5、运行结果
2.3.6、增加新的支付方式——新增上下文继承原有上下文内容进行拓展
现在需要增加一种支付方式,要求工资能支付到银行卡。
《1》新增一个上下文的拓展类并继承原有的支付上下文
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoTwo
{/// <summary>/// 新增的上下文内容/// </summary>internal class PayContextExtand : PayContext{//银行卡号private string account = string.Empty;public string Account { get => account; }/// <summary>/// 构造函数/// </summary>/// <param name="name">应被支付工资的人员</param>/// <param name="money">应被支付的工资金额</param>/// <param name="account">应被支付的银行卡号</param>/// <param name="payStrategy">支付工资的策略</param>public PayContextExtand(string name, double money,string account ,IPayStrategy payStrategy) : base(name, money, payStrategy){this.account = account;}}//Class_end
}
《2》新建一个银行卡支付策略
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoTwo
{/// <summary>/// 银行卡支付/// </summary>internal class CardPay : IPayStrategy{public void Pay(PayContext payContext){//这个新算法自己知道使用扩展,因此这里需要强制转换PayContextExtand payContextExtand = (PayContextExtand)payContext;Console.WriteLine($"\n现在给【{payContextExtand.Name}】的【{payContextExtand.Account}】账号支付【{payContextExtand.Money}】元人民币");//后续就是真正的连接银行接口进行对应的转账操作(这里仅作示意)}}//Class_end
}
《3》客户端测试
原有的代码不用改动,直接添加新的测试就可以了:
namespace StrategyPattern
{internal class Program{static void Main(string[] args){SlarayPayStrategyTest();Console.ReadLine();}/// <summary>/// 工资支付策略测试/// </summary>private static void SlarayPayStrategyTest(){Console.WriteLine("------工资支付策略测试------");//1-创建相应的支付策略StrategyDemoTwo.IPayStrategy payStrategy_RMB = new StrategyDemoTwo.RMBCashPay();StrategyDemoTwo.IPayStrategy payStrategy_Dollar=new StrategyDemoTwo.DollarCashPay();//2-准备小李的支付工资上下文StrategyDemoTwo.PayContext payContext1 = new StrategyDemoTwo.PayContext("张三",8000,payStrategy_RMB);//3-向小李支付工资payContext1.PayNow();//切换一个人支付工资StrategyDemoTwo.PayContext payContext2 = new StrategyDemoTwo.PayContext("Jack",6000,payStrategy_Dollar);payContext2.PayNow();//测试新添加的银行卡支付1StrategyDemoTwo.IPayStrategy payStrategy_Card = new StrategyDemoTwo.CardPay();StrategyDemoTwo.PayContext payContext3 = new StrategyDemoTwo.PayContextExtand("李四", 12000, "010998877656", payStrategy_Card);payContext3.PayNow();}}//Class_end
}
《4》运行结果
2.3.7、增加新的支付方式——通过策略的构造方法传入所需数据实现
《1》新增支付到银行卡策略
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoTwo
{/// <summary>/// 支付到银行卡/// </summary>internal class CardPay2:IPayStrategy{//银行卡账号信息private string account=string.Empty;public string Account { get => account; }/// <summary>/// 构造方法/// </summary>/// <param name="account">应被支付的银行卡号</param>public CardPay2(string account){this.account = account;}public void Pay(PayContext payContext){Console.WriteLine($"\n现在给【{payContext.Name}】的【{this.Account}】账号支付【{payContext.Money}】元人民币");//后续就是真正的连接银行接口进行对应的转账操作(这里仅作示意)}}//Class_end
}
《2》客户端测试
namespace StrategyPattern
{internal class Program{static void Main(string[] args){SlarayPayStrategyTest();Console.ReadLine();}/// <summary>/// 工资支付策略测试/// </summary>private static void SlarayPayStrategyTest(){Console.WriteLine("------工资支付策略测试------");//1-创建相应的支付策略StrategyDemoTwo.IPayStrategy payStrategy_RMB = new StrategyDemoTwo.RMBCashPay();StrategyDemoTwo.IPayStrategy payStrategy_Dollar=new StrategyDemoTwo.DollarCashPay();//2-准备小李的支付工资上下文StrategyDemoTwo.PayContext payContext1 = new StrategyDemoTwo.PayContext("张三",8000,payStrategy_RMB);//3-向小李支付工资payContext1.PayNow();//切换一个人支付工资StrategyDemoTwo.PayContext payContext2 = new StrategyDemoTwo.PayContext("Jack",6000,payStrategy_Dollar);payContext2.PayNow();//测试新添加的银行卡支付1StrategyDemoTwo.IPayStrategy payStrategy_Card = new StrategyDemoTwo.CardPay();StrategyDemoTwo.PayContext payContext3 = new StrategyDemoTwo.PayContextExtand("李四", 12000, "010998877656", payStrategy_Card);payContext3.PayNow();//测试新添加的银行卡支付2StrategyDemoTwo.IPayStrategy payStrategy_Card2 = new StrategyDemoTwo.CardPay2("010998877656");StrategyDemoTwo.PayContext payContext4 = new StrategyDemoTwo.PayContext("王五", 9500, payStrategy_Card2);payContext4.PayNow();}}//Class_end
}
《3》运行结果
2.3.8、 以上两种《增加新的支付方式》该如何选择?
《1》新增上下文继承原有上下文内容进行拓展方式:这样实现可以保持所有策略的风格统一,策略需要的数据都统一从上下文来获取,这样在使用方法上也统一;另外,在上下文中添加新的数据,别的相应算法也可以用得上,可以视为公共的数据。缺点也很明显,如果这些数据只有一个特定的算法来使用,那么这些数据有些浪费;另外每次添加新的算法都需要去扩展上下文,容易形成复杂的上下文对象层次,也未见得有必要。
《2》通过策略的构造方法传入所需数据实现方式:这样的实现,比较好想,实现起来也简单。但是缺点很明显,与其他策略实现的风格不一致,其他策略都是从上下文中来获取数据,而这个策略的实现一部分数据来自上下文,一部分数据来自自己,有些不统一;另外,这样一来,外部使用这些策略算法的时候也不一样了,难以一个统一的方式来动态切换策略算法。
两种实现方式各有优劣,至于如何选择,那就需要再具体使用的情况需要进行具体分析了。
2.4、策略模式的示例3——容错恢复机制
容错恢复机制是应用程序开发中常见的功能,简单点说就是,程序运行的时候,正常情况下应该按照某种方式来做,如果按照某种方式来做放生错误的话,系统不会崩溃,也不会就此不能继续向下运行了,而是有容忍出错的能力,不但可以容忍程序运行出现错误,还可以提供出现错误后的备用方案【即恢复机制】来代替正常执行的功能,使得程序继续向下运行。
容错恢复机制的示例:如在一个系统中,所有对系统的操作都要有日志记录,而且这个日志还需要有管理界面,这种情况下通常会把日志记录在数据库中,方便后续的管理,但是记录日志到数据库的时候,可能会发生错误(如:暂时连接不上数据库,就先记录在文件里面,然后在合适的时候把文件中的记录在转录到数据库中)。
对于这样的需求,就可以采用策略模式,把日志记录到数据库和文件当做两种记录日志的策略,然后在运行期间根据需要进行动态的切换。
2.4.1、定义日志策略接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoThree
{/// <summary>/// 日志策略接口/// </summary>internal interface IlogStrategy{/// <summary>/// 记录日志/// </summary>/// <param name="message">需记录的日志消息</param>void Log(string message);}//Interface_end
}
2.4.2、实现日志记录到数据库和文件中的策略
《1》实现默认的日志记录到数据库中策略
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoThree
{/// <summary>/// 把日志记录到数据库/// </summary>internal class DbLog : IlogStrategy{public void Log(string message){//人为模拟错误情况(如:数据库连接断开等错误情况)if (message != null && message.Length>=6){throw new Exception("数据库异常");}Console.WriteLine($"现在将【{message}】记录到数据库中");}}//Class_end
}
《2》实现日志记录到文件中的策略
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoThree
{/// <summary>/// 把日志记录到文件中/// </summary>internal class FileLog : IlogStrategy{public void Log(string message){Console.WriteLine($"现在把【{message}】记录到文件中");}}//Class_end
}
2.4.3、定义使用这些策略的上下文
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoThree
{/// <summary>/// 日志记录的上下文/// </summary>internal class LogContext{/// <summary>/// 记录日志的方法,提供给客户端使用/// </summary>/// <param name="message">需记录的消息</param>public void Log(string message){//在上下文这里,自行实现对具体策略的选择(这里优先选用数据库策略)IlogStrategy strategy = new DbLog();try{strategy.Log(message);}catch (Exception ex){//出错了,就记录到日志文件中Console.WriteLine($"记录日志【{message}】内容到数据库出错了,报错信息是【{ex.Message}】\n现在将日志记录到本地文件中");strategy = new FileLog();strategy.Log(message);//throw;}}}//Class_end
}
2.4.4、客户端测试
namespace StrategyPattern
{internal class Program{static void Main(string[] args){LogRecordStrategyTest();Console.ReadLine();}/// <summary>/// 日志记录策略测试/// </summary>private static void LogRecordStrategyTest(){Console.WriteLine($"------日志记录策略测试------");StrategyDemoThree.LogContext logContext= new StrategyDemoThree.LogContext();logContext.Log("记录日志");logContext.Log("再次记录日志");}}//Class_end
}
2.4.5、运行结果
2.5、策略模式的示例4——策略模式结合模板方法模式
在实际应用策略模式的过程中,会经常出现一种情况(发现一系列算法的实现上存在公共功能,甚至这一些列算法的实现步骤都是一样的,只是在某些局部步骤上有所不同,这时候,就需要对策略模式进行些许变化使用了)。
序号 | 一系列算法的实现上存在公共功能情况,策略模式的三种实现方式 |
1 | 在上下文当中实现公共功能,让所有具体的策略算法回调这些方法 |
2 | 将策略的接口改为抽象类,然后在其中实现具体算法的公共功能 |
3 | 为所有的策略算法定义一个抽象的父类,让这个父类去实现接口策略,然后在这个父类中去实现公共功能。 (更进一步,如果这个时候发现“一系列算法的实现步骤都是一样的,只是在某些局部步骤有所不同”的情况,那就可以在这个抽象类里面定义算法实现的骨架,然后让具体的策略算法去实现变化的部分。这样一个结构自然就变成了策略模式结合模板方法模式了,那个抽象类就成了模板方法模式的模板类) |
业务需求:我们现在需要对上面已经实现的日志记录功能的所有日志消息前面都添加上日志时间【即:1、为日志消息添加日志时间;2、记录具体的日志】 。
2.5.1、定义日志策略接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoFour
{/// <summary>/// 日志记录策略接口/// </summary>internal interface ILogStrategy{/// <summary>/// 记录日志/// </summary>/// <param name="message">需记录的消息</param>void Log(string message);}//Interface_end
}
2.5.2、定义一个实现策略接口的抽象类
这个实现策略接口的抽象类中定义记录日志的算法骨架,相当于模板方法模式的模板。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoFour
{/// <summary>/// 实现日志策略的抽象模板(实现为消息添加时间)/// </summary>internal abstract class LogStrategyTemplate : ILogStrategy{public void Log(string message){//1-为消息添加记录的时间message = AddTimeOfMessage(message);//2-真正执行记录操作LogRecord(message);}//给消息前添加时间private string AddTimeOfMessage(string message){string curTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");message = $"{curTime} {message}";return message ;}/// <summary>/// 真正执行日志记录,让子类去实现/// </summary>/// <param name="message">需记录的消息</param>protected abstract void LogRecord(string message);}//Class_end
}
2.5.3、实现具体的日志记录策略
此时这里的具体数据库日志记录和文件日志记录就不再继承日志接口,而是直接继承抽象类(即:日志策略模板)并实现模板的方法。
《1》实现具体的数据库日志记录
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoFour
{/// <summary>/// 日志记录到数据库中/// </summary>internal class DbLog : LogStrategyTemplate{protected override void LogRecord(string message){//人为模拟错误情况(如:数据库连接断开等错误情况)if (message != null && message.Length >= 6){throw new Exception("数据库异常");}Console.WriteLine($"现在将【{message}】记录到数据库中\n");}}//Class_end
}
《2》实现具体的文件日志记录
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoFour
{internal class FileLog : LogStrategyTemplate{protected override void LogRecord(string message){Console.WriteLine($"现在把【{message}】记录到文件中\n");}}//Class_end
}
2.5.4、算法实现的改变不影响使用这些策略的上下文
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace StrategyPattern.StrategyDemoFour
{/// <summary>/// 日志记录的上下文/// </summary>internal class LogContext{/// <summary>/// 记录日志的方法,提供给客户端使用/// </summary>/// <param name="message">需记录的消息</param>public void Log(string message){//在上下文这里,自行实现对具体策略的选择(这里优先选用数据库策略)ILogStrategy strategy = new DbLog();try{strategy.Log(message);}catch (Exception ex){//出错了,就记录到日志文件中Console.WriteLine($"记录日志【{message}】内容到数据库出错了,报错信息是【{ex.Message}】\n现在将日志记录到本地文件中");strategy = new FileLog();strategy.Log(message);//throw;}}}//Class_end
}
2.5.5、客户端测试与原来一样
namespace StrategyPattern
{internal class Program{static void Main(string[] args){LogRecordStrategyTest2();Console.ReadLine();}/// <summary>/// 日志记录策略测试2/// </summary>private static void LogRecordStrategyTest2(){Console.WriteLine($"------日志记录策略测试------");StrategyDemoFour.LogContext logContext = new StrategyDemoFour.LogContext();logContext.Log("记录日志");logContext.Log("再次记录日志");}}//Class_end
}
2.5.6、运行结果
三、项目源码工程
kafeiweimei/Learning_DesignPattern: 这是一个关于C#语言编写的基础设计模式项目工程,方便学习理解常见的26种设计模式https://github.com/kafeiweimei/Learning_DesignPattern