策略设计模式
现实生活中我们到商场买东西的时候,卖场往往根据不同的客户制定不同的报价策略,比如针对新客户不打折扣,针对老客户打9折,针对VIP客户打8折...
现在我们要做一个报价管理的模块,简要点就是要针对不同的客户,提供不同的折扣报价。
如果是有你来做,你会怎么做?
我们很有可能写出下面的代码:
public class QuoteManager { public BigDecimal quote(BigDecimal originalPrice,String customType){ if ("新客户".equals(customType)) { System.out.println("抱歉!新客户没有折扣!"); return originalPrice; }else if ("老客户".equals(customType)) { System.out.println("恭喜你!老客户打9折!"); originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; }else if("VIP客户".equals(customType)){ System.out.println("恭喜你!VIP客户打8折!"); originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; } //其他人员都是原价 return originalPrice; } }
经过测试,上面的代码工作的很好,可是上面的代码是有问题的。上面存在的问题:
下面看一下上面的改进,我们把不同客户的报价的算法都单独作为一个方法
public class QuoteManagerImprove { public BigDecimal quote(BigDecimal originalPrice, String customType){ if ("新客户".equals(customType)) { return this.quoteNewCustomer(originalPrice); }else if ("老客户".equals(customType)) { return this.quoteOldCustomer(originalPrice); }else if("VIP客户".equals(customType)){ return this.quoteVIPCustomer(originalPrice); } //其他人员都是原价 return originalPrice; } /** * 对VIP客户的报价算法 * @param originalPrice 原价 * @return 折后价 */ private BigDecimal quoteVIPCustomer(BigDecimal originalPrice) { System.out.println("恭喜!VIP客户打8折"); originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; } /** * 对老客户的报价算法 * @param originalPrice 原价 * @return 折后价 */ private BigDecimal quoteOldCustomer(BigDecimal originalPrice) { System.out.println("恭喜!老客户打9折"); originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; } /** * 对新客户的报价算法 * @param originalPrice 原价 * @return 折后价 */ private BigDecimal quoteNewCustomer(BigDecimal originalPrice) { System.out.println("抱歉!新客户没有折扣!"); return originalPrice; } }
上面的代码比刚开始的时候要好一点,它把每个具体的算法都单独抽出来作为一个方法,当某一个具体的算法有了变动的时候,只需要修改响应的报价算法就可以了。
但是改进后的代码还是有问题的,那有什么问题呢?
开闭原则:
对于扩展是开放的(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。
对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。
2.我们经常会面临这样的情况,不同的时期使用不同的报价规则,比如在各个节假日举行的各种促销活动时、商场店庆时往往都有普遍的折扣,但是促销时间一旦过去,报价就要回到正常价格上来。按照上面的代码我们就得修改if else里面的代码很是麻烦
那有没有什么办法使得我们的报价管理即可扩展、可维护,又可以方便的响应变化呢?当然有解决方案啦,就是我们下面要讲的策略模式。
定义:
策略模式定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立。
结构:
-
策略接口角色IStrategy:用来约束一系列具体的策略算法,策略上下文角色ConcreteStrategy使用此策略接口来调用具体的策略所实现的算法。
-
具体策略实现角色ConcreteStrategy:具体的策略实现,即具体的算法实现。
-
策略上下文角色StrategyContext:策略上下文,负责和具体的策略实现交互,通常策略上下文对象会持有一个真正的策略实现对象,策略上下文还可以让具体的策略实现从其中获取相关数据,回调策略上下文对象的方法。
策略模式代码的一般通用实现:
策略接口
public interface IStrategy { //定义的抽象算法方法 来约束具体的算法实现方法 public void algorithmMethod(); }
public class ConcreteStrategy implements IStrategy { //具体的算法实现 @Override public void algorithmMethod() { System.out.println("this is ConcreteStrategy method..."); } }
// 具体的策略实现2 public class ConcreteStrategy2 implements IStrategy { //具体的算法实现 @Override public void algorithmMethod() { System.out.println("this is ConcreteStrategy2 method..."); } }
public class StrategyContext { //持有一个策略实现的引用 private IStrategy strategy; //使用构造器注入具体的策略类 public StrategyContext(IStrategy strategy) { this.strategy = strategy; } public void contextMethod(){ //调用策略实现的方法 strategy.algorithmMethod(); } }
public class Client { public static void main(String[] args) { //1.创建具体测策略实现 IStrategy strategy = new ConcreteStrategy2(); //2.在创建策略上下文的同时,将具体的策略实现对象注入到策略上下文当中 StrategyContext ctx = new StrategyContext(strategy); //3.调用上下文对象的方法来完成对具体策略实现的回调 ctx.contextMethod(); } }
公共报价策略接口:
public interface IQuoteStrategy { //获取折后价的价格 BigDecimal getPrice(BigDecimal originalPrice); }
public class NewCustomerQuoteStrategy implements IQuoteStrategy { @Override public BigDecimal getPrice(BigDecimal originalPrice) { System.out.println("抱歉!新客户没有折扣!"); return originalPrice; } }
public class OldCustomerQuoteStrategy implements IQuoteStrategy { @Override public BigDecimal getPrice(BigDecimal originalPrice) { System.out.println("恭喜!老客户享有9折优惠!"); originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; } }
public class VIPCustomerQuoteStrategy implements IQuoteStrategy { @Override public BigDecimal getPrice(BigDecimal originalPrice) { System.out.println("恭喜!VIP客户享有8折优惠!"); originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; } }
public class QuoteContext { //持有一个具体的报价策略 private IQuoteStrategy quoteStrategy; //注入报价策略 public QuoteContext(IQuoteStrategy quoteStrategy){ this.quoteStrategy = quoteStrategy; } //回调具体报价策略的方法 public BigDecimal getPrice(BigDecimal originalPrice){ return quoteStrategy.getPrice(originalPrice); } }
public class Client { public static void main(String[] args) { //1.创建老客户的报价策略 IQuoteStrategy oldQuoteStrategy = new OldCustomerQuoteStrategy(); //2.创建报价上下文对象,并设置具体的报价策略 QuoteContext quoteContext = new QuoteContext(oldQuoteStrategy); //3.调用报价上下文的方法 BigDecimal price = quoteContext.getPrice(new BigDecimal(100)); System.out.println("折扣价为:" +price); } }
深入理解策略模式:
-
-
策略模式的着重点:不是如何来实现算法,而是如何组织和调用这些算法,从而让我们的程序结构更加的灵活、可扩展。
我们前面的第一个报价管理的示例,发现每个策略算法实现对应的都是在QuoteManager 中quote方法中的if else语句里面,我们知道if else if语句里面的代码在执行的可能性方面可以说是平等的,你要么执行if,要么执行else,要么执行else if。
策略模式就是把各个平等的具体实现进行抽象、封装成为独立的算法类,然后通过上下文和具体的算法类来进行交互。各个策略算法都是平等的,地位是一样的,正是由于各个算法的平等性,所以它们才是可以相互替换的。虽然我们可以动态的切换各个策略,但是同一时刻只能使用一个策略。
在这个点上,我们举个历史上有名的故事作为示例:
三国刘备取西川时,谋士庞统给的上、中、下三个计策:
-
上策:挑选精兵,昼夜兼行直接偷袭成都,可以一举而定,此为上计计也。
-
中策:杨怀、高沛是蜀中名将,手下有精锐部队,而且据守关头,我们可以装作要回荆州,引他们轻骑来见,可就此将其擒杀,而后进兵成都,此为中计。
-
下策:退还白帝,连引荆州,慢慢进图益州,此为下计。
这三个计策都是取西川的计策,也就是攻取西川这个问题的具体的策略算法,刘备可以采用上策,可以采用中策,当然也可以采用下策,由此可见策略模式的各种具体的策略算法都是平等的,可以相互替换。
那谁来选择具体采用哪种计策(算法)?
在这个故事中当然是刘备选择了,也就是外部的客户端选择使用某个具体的算法,然后把该算法(计策)设置到上下文当中;
还有一种情况就是客户端不选择具体的算法,把这个事交给上下文,这相当于刘备说我不管有哪些攻取西川的计策,我只要结果(成功的拿下西川),具体怎么攻占(有哪些计策,怎么选择)由参谋部来决定(上下文)。
下面我们演示下这种情景:
//攻取西川的策略 public interface IOccupationStrategyWestOfSiChuan { public void occupationWestOfSiChuan(String msg); }
//攻取西川的上上计策 public class UpperStrategy implements IOccupationStrategyWestOfSiChuan { @Override public void occupationWestOfSiChuan(String msg) { if (msg == null || msg.length() < 5) { //故意设置障碍,导致上上计策失败 System.out.println("由于计划泄露,上上计策失败!"); int i = 100/0; } System.out.println("挑选精兵,昼夜兼行直接偷袭成都,可以一举而定,此为上计计也!"); } }
//攻取西川的中计策 public class MiddleStrategy implements IOccupationStrategyWestOfSiChuan { @Override public void occupationWestOfSiChuan(String msg) { System.out.println("杨怀、高沛是蜀中名将,手下有精锐部队,而且据守关头,我们可以装作要回荆州,引他们轻骑来见,可就此将其擒杀,而后进兵成都,此为中计。"); } }
//攻取西川的下计策 public class LowerStrategy implements IOccupationStrategyWestOfSiChuan { @Override public void occupationWestOfSiChuan(String msg) { System.out.println("退还白帝,连引荆州,慢慢进图益州,此为下计。"); } }
//攻取西川参谋部,就是上下文啦,由上下文来选择具体的策略 public class OccupationContext { public void occupationWestOfSichuan(String msg){ //先用上上计策 IOccupationStrategyWestOfSiChuan strategy = new UpperStrategy(); try { strategy.occupationWestOfSiChuan(msg); } catch (Exception e) { //上上计策有问题行不通之后,用中计策 strategy = new MiddleStrategy(); strategy.occupationWestOfSiChuan(msg); } } }
//此时外部客户端相当于刘备了,不管具体采用什么计策,只要结果(成功的攻下西川) public class Client { public static void main(String[] args) { OccupationContext context = new OccupationContext(); //这个给手下的人激励不够啊 context.occupationWestOfSichuan("拿下西川"); System.out.println("========================="); //这个人人有赏,让士兵有动力啊 context.occupationWestOfSichuan("拿下西川之后,人人有赏!"); } }
我们上面的策略接口采用的是接口的形式来定义的,其实这个策略接口,是广义上的接口,不是语言层面的interface,也可以是一个抽象类,如果多个算法具有公有的数据,则可以将策略接口设计为一个抽象类,把公共的东西放到抽象类里面去。
策略模式在JDK中的应用:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
-
-
maximumPoolSize:线程池中的最多能够创建的线程数量。
-
keepAliveTime:当线程池中的线程数量大于corePoolSize时,多余的线程等待新任务的最长时间。
-
unit:keepAliveTime的时间单位。
-
workQueue:在线程池中的线程还没有还得及执行任务之前,保存任务的队列(当线程池中的线程都有任务在执行的时候,仍然有任务不断的提交过来,这些任务保存在workQueue队列中)。
-
threadFactory:创建线程池中线程的工厂。
-
handler:当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。
RejectedExecutionHandler 是一个策略接口,用在当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。
public interface RejectedExecutionHandler { /** *当ThreadPoolExecutor的execut方法调用时,并且ThreadPoolExecutor不能接受一个任务Task时,该方法就有可能被调用。 * 不能接受一个任务Task的原因:有可能是没有多余的线程来处理,有可能是workqueue队列中没有多余的位置来存放该任务,该方法有可能抛出一个未受检的异常RejectedExecutionException * @param r the runnable task requested to be executed * @param executor the executor attempting to execute this task * @throws RejectedExecutionException if there is no remedy */ void rejectedExecution(Runnable r, ThreadPoolExecutor executor); }
该策略接口有四个实现类:
AbortPolicy:该策略是直接将提交的任务抛弃掉,并抛出RejectedExecutionException异常。
/** * A handler for rejected tasks that throws a * <tt>RejectedExecutionException</tt>. */ public static class AbortPolicy implements RejectedExecutionHandler { /** * Creates an <tt>AbortPolicy</tt>. */ public AbortPolicy() { } /** * Always throws RejectedExecutionException. * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task * @throws RejectedExecutionException always. */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException(); } }
/** * A handler for rejected tasks that silently discards the * rejected task. */ public static class DiscardPolicy implements RejectedExecutionHandler { /** * Creates a <tt>DiscardPolicy</tt>. */ public DiscardPolicy() { } /** * Does nothing, which has the effect of discarding task r. * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } }
/** * A handler for rejected tasks that discards the oldest unhandled * request and then retries <tt>execute</tt>, unless the executor * is shut down, in which case the task is discarded. */ public static class DiscardOldestPolicy implements RejectedExecutionHandler { /** * Creates a <tt>DiscardOldestPolicy</tt> for the given executor. */ public DiscardOldestPolicy() { } /** * Obtains and ignores the next task that the executor * would otherwise execute, if one is immediately available, * and then retries execution of task r, unless the executor * is shut down, in which case task r is instead discarded. * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } } }
/** * A handler for rejected tasks that runs the rejected task * directly in the calling thread of the {@code execute} method, * unless the executor has been shut down, in which case the task * is discarded. */ public static class CallerRunsPolicy implements RejectedExecutionHandler { /** * Creates a {@code CallerRunsPolicy}. */ public CallerRunsPolicy() { } /** * Executes task r in the caller's thread, unless the executor * has been shut down, in which case the task is discarded. * * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } } }
策略模式的优点:
-
策略模式的功能就是通过抽象、封装来定义一系列的算法,使得这些算法可以相互替换,所以为这些算法定义一个公共的接口,以约束这些算法的功能实现。如果这些算法具有公共的功能,可以将接口变为抽象类,将公共功能放到抽象父类里面。
-
策略模式的一系列算法是可以相互替换的、是平等的,写在一起就是if-else组织结构,如果算法实现里又有条件语句,就构成了多重条件语句,可以用策略模式,避免这样的多重条件语句。
-
扩展性更好:在策略模式中扩展策略实现非常的容易,只要新增一个策略实现类,然后在使用策略实现的地方,使用这个新的策略实现就好了。
策略模式的缺点:
1.客户端必须了解所有的策略,清楚它们的不同:
如果由客户端来决定使用何种算法,那客户端必须知道所有的策略,清楚各个策略的功能和不同,这样才能做出正确的选择,但是这暴露了策略的具体实现。
2.增加了对象的数量:
由于策略模式将每个具体的算法都单独封装为一个策略类,如果可选的策略有很多的话,那对象的数量也会很多。
3.只适合偏平的算法结构:
由于策略模式的各个策略实现是平等的关系(可相互替换),实际上就构成了一个扁平的算法结构。即一个策略接口下面有多个平等的策略实现(多个策略实现是兄弟关系),并且运行时只能有一个算法被使用。这就限制了算法的使用层级,且不能被嵌套。
策略模式的本质:
分离算法,选择实现。
如果你仔细思考策略模式的结构和功能的话,就会发现:如果没有上下文,策略模式就回到了最基本的接口和实现了,只要是面向接口编程,就能够享受到面向接口编程带来的好处,通过一个统一的策略接口来封装和分离各个具体的策略实现,无需关系具体的策略实现。
貌似没有上下文什么事,但是如果没有上下文的话,客户端就必须直接和具体的策略实现进行交互了,尤其是需要提供一些公共功能或者是存储一些状态的时候,会大大增加客户端使用的难度;引入上下文之后,这部分工作可以由上下文来完成,客户端只需要和上下文进行交互就可以了。这样可以让策略模式更具有整体性,客户端也更加的简单。
策略模式体现了开闭原则:策略模式把一系列的可变算法进行封装,从而定义了良好的程序结构,在出现新的算法的时候,可以很容易的将新的算法实现加入到已有的系统中,而已有的实现不需要修改。
策略模式体现了里氏替换原则:策略模式是一个扁平的结构,各个策略实现都是兄弟关系,实现了同一个接口或者继承了同一个抽象类。这样只要使用策略的客户端保持面向抽象编程,就可以动态的切换不同的策略实现以进行替换。
public class NewCustomerQuoteStrategy implements IQuoteStrategy { @Override public BigDecimal getPrice(BigDecimal originalPrice) { System.out.println("抱歉!新客户没有折扣!"); return originalPrice; }}