竹笋

首页 » 问答 » 问答 » 详解设计模式之策略模式
TUhjnbcbe - 2024/10/12 17:23:00
白癜风研讨会 http://www.znlvye.com/xtbb/bbzz/m/3150.html

在讲策略模式之前,我们先看一个日常生活中的小例子:

现实生活中我们到商场买东西的时候,卖场往往根据不同的客户制定不同的报价策略,比如针对新客户不打折扣,针对老客户打9折,针对VIP客户打8折...

现在我们要做一个报价管理的模块,简要点就是要针对不同的客户,提供不同的折扣报价。

如果是有你来做,你会怎么做?

我们很有可能写出下面的代码:

packagestrategy.examp02;importjava.math.BigDecimal;publicclassQuoteManager{publicBigDecimalquote(BigDecimaloriginalPrice,StringcustomType){if("新客户".equals(customType)){System.out.println("抱歉!新客户没有折扣!");returnoriginalPrice;}elseif("老客户".equals(customType)){System.out.println("恭喜你!老客户打9折!");originalPrice=originalPrice.multiply(newBigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP);returnoriginalPrice;}elseif("VIP客户".equals(customType)){System.out.println("恭喜你!VIP客户打8折!");originalPrice=originalPrice.multiply(newBigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP);returnoriginalPrice;}//其他人员都是原价returnoriginalPrice;}}

经过测试,上面的代码工作的很好,可是上面的代码是有问题的。上面存在的问题:把不同客户的报价的算法都放在了同一个方法里面,使得该方法很是庞大(现在是只是一个演示,所以看起来还不是很臃肿)。

下面看一下上面的改进,我们把不同客户的报价的算法都单独作为一个方法

packagestrategy.examp02;importjava.math.BigDecimal;publicclassQuoteManagerImprove{publicBigDecimalquote(BigDecimaloriginalPrice,StringcustomType){if("新客户".equals(customType)){returnthis.quoteNewCustomer(originalPrice);}elseif("老客户".equals(customType)){returnthis.quoteOldCustomer(originalPrice);}elseif("VIP客户".equals(customType)){returnthis.quoteVIPCustomer(originalPrice);}//其他人员都是原价returnoriginalPrice;}/***对VIP客户的报价算法*

paramoriginalPrice原价*

return折后价*/privateBigDecimalquoteVIPCustomer(BigDecimaloriginalPrice){System.out.println("恭喜!VIP客户打8折");originalPrice=originalPrice.multiply(newBigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP);returnoriginalPrice;}/***对老客户的报价算法*

paramoriginalPrice原价*

return折后价*/privateBigDecimalquoteOldCustomer(BigDecimaloriginalPrice){System.out.println("恭喜!老客户打9折");originalPrice=originalPrice.multiply(newBigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP);returnoriginalPrice;}/***对新客户的报价算法*

paramoriginalPrice原价*

return折后价*/privateBigDecimalquoteNewCustomer(BigDecimaloriginalPrice){System.out.println("抱歉!新客户没有折扣!");returnoriginalPrice;}}

上面的代码比刚开始的时候要好一点,它把每个具体的算法都单独抽出来作为一个方法,当某一个具体的算法有了变动的时候,只需要修改响应的报价算法就可以了。

但是改进后的代码还是有问题的,那有什么问题呢?

1.当我们新增一个客户类型的时候,首先要添加一个该种客户类型的报价算法方法,然后再quote方法中再加一个elseif的分支,是不是感觉很是麻烦呢?而且这也违反了设计原则之一的开闭原则(open-closed-principle).

开闭原则:

对于扩展是开放的(Openforextension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。

对于修改是关闭的(Closedformodification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。

2.我们经常会面临这样的情况,不同的时期使用不同的报价规则,比如在各个节假日举行的各种促销活动时、商场店庆时往往都有普遍的折扣,但是促销时间一旦过去,报价就要回到正常价格上来。按照上面的代码我们就得修改ifelse里面的代码很是麻烦

那有没有什么办法使得我们的报价管理即可扩展、可维护,又可以方便的响应变化呢?当然有解决方案啦,就是我们下面要讲的策略模式。

定义:

策略模式定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立。

结构:

策略接口角色IStrategy:用来约束一系列具体的策略算法,策略上下文角色ConcreteStrategy使用此策略接口来调用具体的策略所实现的算法。

  

具体策略实现角色ConcreteStrategy:具体的策略实现,即具体的算法实现。

  

策略上下文角色StrategyContext:策略上下文,负责和具体的策略实现交互,通常策略上下文对象会持有一个真正的策略实现对象,策略上下文还可以让具体的策略实现从其中获取相关数据,回调策略上下文对象的方法。

  

UML类图:

UML序列图:

策略模式代码的一般通用实现:

策略接口

packagestrategy.examp01;//策略接口publicinterfaceIStrategy{//定义的抽象算法方法来约束具体的算法实现方法publicvoidalgorithmMethod();}

具体的策略实现:

packagestrategy.examp01;//具体的策略实现publicclassConcreteStrategyimplementsIStrategy{//具体的算法实现

OverridepublicvoidalgorithmMethod(){System.out.println("thisisConcreteStrategymethod...");}}

packagestrategy.examp01;//具体的策略实现2publicclassConcreteStrategy2implementsIStrategy{//具体的算法实现

OverridepublicvoidalgorithmMethod(){System.out.println("thisisConcreteStrategy2method...");}}

策略上下文:

packagestrategy.examp01;/***策略上下文*/publicclassStrategyContext{//持有一个策略实现的引用privateIStrategystrategy;//使用构造器注入具体的策略类publicStrategyContext(IStrategystrategy){this.strategy=strategy;}publicvoidcontextMethod(){//调用策略实现的方法strategy.algorithmMethod();}}

外部客户端:

packagestrategy.examp01;//外部客户端publicclassClient{publicstaticvoidmain(String[]args){//1.创建具体测策略实现IStrategystrategy=newConcreteStrategy2();//2.在创建策略上下文的同时,将具体的策略实现对象注入到策略上下文当中StrategyContextctx=newStrategyContext(strategy);//3.调用上下文对象的方法来完成对具体策略实现的回调ctx.contextMethod();}}

针对我们一开始讲的报价管理的例子:我们可以应用策略模式对其进行改造,不同类型的客户有不同的折扣,我们可以将不同类型的客户的报价规则都封装为一个独立的算法,然后抽象出这些报价算法的公共接口

公共报价策略接口:

packagestrategy.examp02;importjava.math.BigDecimal;//报价策略接口publicinterfaceIQuoteStrategy{//获取折后价的价格BigDecimalgetPrice(BigDecimaloriginalPrice);}

新客户报价策略实现:

packagestrategy.examp02;importjava.math.BigDecimal;//新客户的报价策略实现类publicclassNewCustomerQuoteStrategyimplementsIQuoteStrategy{

OverridepublicBigDecimalgetPrice(BigDecimaloriginalPrice){System.out.println("抱歉!新客户没有折扣!");returnoriginalPrice;}}

老客户报价策略实现:

packagestrategy.examp02;importjava.math.BigDecimal;//老客户的报价策略实现publicclassOldCustomerQuoteStrategyimplementsIQuoteStrategy{

OverridepublicBigDecimalgetPrice(BigDecimaloriginalPrice){System.out.println("恭喜!老客户享有9折优惠!");originalPrice=originalPrice.multiply(newBigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP);returnoriginalPrice;}}

VIP客户报价策略实现:

packagestrategy.examp02;importjava.math.BigDecimal;//VIP客户的报价策略实现publicclassVIPCustomerQuoteStrategyimplementsIQuoteStrategy{

OverridepublicBigDecimalgetPrice(BigDecimaloriginalPrice){System.out.println("恭喜!VIP客户享有8折优惠!");originalPrice=originalPrice.multiply(newBigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP);returnoriginalPrice;}}

报价上下文:

packagestrategy.examp02;importjava.math.BigDecimal;//报价上下文角色publicclassQuoteContext{//持有一个具体的报价策略privateIQuoteStrategyquoteStrategy;//注入报价策略publicQuoteContext(IQuoteStrategyquoteStrategy){this.quoteStrategy=quoteStrategy;}//回调具体报价策略的方法publicBigDecimalgetPrice(BigDecimaloriginalPrice){returnquoteStrategy.getPrice(originalPrice);}}

外部客户端:

packagestrategy.examp02;importjava.math.BigDecimal;//外部客户端publicclassClient{publicstaticvoidmain(String[]args){//1.创建老客户的报价策略IQuoteStrategyoldQuoteStrategy=newOldCustomerQuoteStrategy();//2.创建报价上下文对象,并设置具体的报价策略QuoteContextquoteContext=newQuoteContext(oldQuoteStrategy);//3.调用报价上下文的方法BigDecimalprice=quoteContext.getPrice(newBigDecimal());System.out.println("折扣价为:"+price);}}

控制台输出:

恭喜!老客户享有9折优惠!折扣价为:90.00

这个时候,商场营销部新推出了一个客户类型--MVP用户(MostValuablePerson),可以享受折扣7折优惠,那该怎么办呢?

这个很容易,只要新增一个报价策略的实现,然后外部客户端调用的时候,创建这个新增的报价策略实现,并设置到策略上下文就可以了,对原来已经实现的代码没有任何的改动。

MVP用户的报价策略实现:

packagestrategy.examp02;importjava.math.BigDecimal;//MVP客户的报价策略实现publicclassMVPCustomerQuoteStrategyimplementsIQuoteStrategy{

OverridepublicBigDecimalgetPrice(BigDecimaloriginalPrice){System.out.println("哇偶!MVP客户享受7折优惠!!!");originalPrice=originalPrice.multiply(newBigDecimal(0.7)).setScale(2,BigDecimal.ROUND_HALF_UP);returnoriginalPrice;}}

外部客户端:

packagestrategy.examp02;importjava.math.BigDecimal;//外部客户端publicclassClient{publicstaticvoidmain(String[]args){//创建MVP客户的报价策略IQuoteStrategymvpQuoteStrategy=newMVPCustomerQuoteStrategy();//创建报价上下文对象,并设置具体的报价策略QuoteContextquoteContext=newQuoteContext(mvpQuoteStrategy);//调用报价上下文的方法BigDecimalprice=quoteContext.getPrice(newBigDecimal());System.out.println("折扣价为:"+price);}}

控制台输出:

哇偶!MVP客户享受7折优惠!!!折扣价为:70.00

深入理解策略模式:

策略模式的作用:就是把具体的算法实现从业务逻辑中剥离出来,成为一系列独立算法类,使得它们可以相互替换。

  

策略模式的着重点:不是如何来实现算法,而是如何组织和调用这些算法,从而让我们的程序结构更加的灵活、可扩展。

  

我们前面的第一个报价管理的示例,发现每个策略算法实现对应的都是在QuoteManager中quote方法中的ifelse语句里面,我们知道ifelseif语句里面的代码在执行的可能性方面可以说是平等的,你要么执行if,要么执行else,要么执行elseif。

策略模式就是把各个平等的具体实现进行抽象、封装成为独立的算法类,然后通过上下文和具体的算法类来进行交互。各个策略算法都是平等的,地位是一样的,正是由于各个算法的平等性,所以它们才是可以相互替换的。虽然我们可以动态的切换各个策略,但是同一时刻只能使用一个策略。

在这个点上,我们举个历史上有名的故事作为示例:

三国刘备取西川时,谋士庞统给的上、中、下三个计策:

上策:挑选精兵,昼夜兼行直接偷袭成都,可以一举而定,此为上计计也。

  

中策:杨怀、高沛是蜀中名将,手下有精锐部队,而且据守关头,我们可以装作要回荆州,引他们轻骑来见,可就此将其擒杀,而后进兵成都,此为中计。

  

下策:退还白帝,连引荆州,慢慢进图益州,此为下计。

  

这三个计策都是取西川的计策,也就是攻取西川这个问题的具体的策略算法,刘备可以采用上策,可以采用中策,当然也可以采用下策,由此可见策略模式的各种具体的策略算法都是平等的,可以相互替换。

那谁来选择具体采用哪种计策(算法)?

在这个故事中当然是刘备选择了,也就是外部的客户端选择使用某个具体的算法,然后把该算法(计策)设置到上下文当中;

还有一种情况就是客户端不选择具体的算法,把这个事交给上下文,这相当于刘备说我不管有哪些攻取西川的计策,我只要结果(成功的拿下西川),具体怎么攻占(有哪些计策,怎么选择)由参谋部来决定(上下文)。

下面我们演示下这种情景:

//攻取西川的策略publicinterfaceIOccupationStrategyWestOfSiChuan{publicvoidoccupationWestOfSiChuan(Stringmsg);}

//攻取西川的上上计策publicclassUpperStrategyimplementsIOccupationStrategyWestOfSiChuan{

OverridepublicvoidoccupationWestOfSiChuan(Stringmsg){if(msg==null

msg.length()5){//故意设置障碍,导致上上计策失败System.out.println("由于计划泄露,上上计策失败!");inti=/0;}System.out.println("挑选精兵,昼夜兼行直接偷袭成都,可以一举而定,此为上计计也!");}}

//攻取西川的中计策publicclassMiddleStrategyimplementsIOccupationStrategyWestOfSiChuan{

OverridepublicvoidoccupationWestOfSiChuan(Stringmsg){System.out.println("杨怀、高沛是蜀中名将,手下有精锐部队,而且据守关头,我们可以装作要回荆州,引他们轻骑来见,可就此将其擒杀,而后进兵成都,此为中计。");}}

//攻取西川的下计策publicclassLowerStrategyimplementsIOccupationStrategyWestOfSiChuan{

OverridepublicvoidoccupationWestOfSiChuan(Stringmsg){System.out.println("退还白帝,连引荆州,慢慢进图益州,此为下计。");}}

//攻取西川参谋部,就是上下文啦,由上下文来选择具体的策略publicclassOccupationContext{publicvoidoccupationWestOfSichuan(Stringmsg){//先用上上计策IOccupationStrategyWestOfSiChuanstrategy=newUpperStrategy();try{strategy.occupationWestOfSiChuan(msg);}catch(Exceptione){//上上计策有问题行不通之后,用中计策strategy=newMiddleStrategy();strategy.occupationWestOfSiChuan(msg);}}}

//此时外部客户端相当于刘备了,不管具体采用什么计策,只要结果(成功的攻下西川)publicclassClient{publicstaticvoidmain(String[]args){OccupationContextcontext=newOccupationContext();//这个给手下的人激励不够啊context.occupationWestOfSichuan("拿下西川");System.out.println("=========================");//这个人人有赏,让士兵有动力啊context.occupationWestOfSichuan("拿下西川之后,人人有赏!");}}

控制台输出:

由于计划泄露,上上计策失败!杨怀、高沛是蜀中名将,手下有精锐部队,而且据守关头,我们可以装作要回荆州,引他们轻骑来见,可就此将其擒杀,而后进兵成都,此为中计。=========================挑选精兵,昼夜兼行直接偷袭成都,可以一举而定,此为上计计也!

我们上面的策略接口采用的是接口的形式来定义的,其实这个策略接口,是广义上的接口,不是语言层面的interface,也可以是一个抽象类,如果多个算法具有公有的数据,则可以将策略接口设计为一个抽象类,把公共的东西放到抽象类里面去。

策略和上下文的关系:

在策略模式中,一般情况下都是上下文持有策略的引用,以进行对具体策略的调用。但具体的策略对象也可以从上下文中获取所需数据,可以将上下文当做参数传入到具体策略中,具体策略通过回调上下文中的方法来获取其所需要的数据。

下面我们演示这种情况:

在跨国公司中,一般都会在各个国家和地区设置分支机构,聘用当地人为员工,这样就有这样一个需要:每月发工资的时候,中国国籍的员工要发人民币,美国国籍的员工要发美元,英国国籍的要发英镑。

//支付策略接口publicinterfacePayStrategy{//在支付策略接口的支付方法中含有支付上下文作为参数,以便在具体的支付策略中回调上下文中的方法获取数据publicvoidpay(PayContextctx);}

//人民币支付策略publicclassRMBPayimplementsPayStrategy{

Overridepublicvoidpay(PayContextctx){System.out.println("现在给:"+ctx.getUsername()+"人民币支付"+ctx.getMoney()+"元!");}}

//美金支付策略publicclassDollarPayimplementsPayStrategy{

Overridepublicvoidpay(PayContextctx){System.out.println("现在给:"+ctx.getUsername()+"美金支付"+ctx.getMoney()+"dollar!");}}

//支付上下文,含有多个算法的公有数据publicclassPayContext{//员工姓名privateStringusername;//员工的工资privatedoublemoney;//支付策略privatePayStrategypayStrategy;publicvoidpay(){//调用具体的支付策略来进行支付payStrategy.pay(this);}publicPayContext(Stringusername,doublemoney,PayStrategypayStrategy){this.username=username;this.money=money;this.payStrategy=payStrategy;}publicStringgetUsername(){returnusername;}publicdoublegetMoney(){returnmoney;}}

//外部客户端publicclassClient{publicstaticvoidmain(String[]args){//创建具体的支付策略PayStrategyrmbStrategy=newRMBPay();PayStrategydollarStrategy=newDollarPay();//准备小王的支付上下文PayContextctx=newPayContext("小王",,rmbStrategy);//向小王支付工资ctx.pay();//准备Jack的支付上下文ctx=newPayContext("jack",00,dollarStrategy);//向Jack支付工资ctx.pay();}}

控制台输出:

现在给:小王人民币支付.0元!现在给:jack美金支付00.0dollar!

那现在我们要新增一个银行账户的支付策略,该怎么办呢?

显然我们应该新增一个支付找银行账户的策略实现,由于需要从上下文中获取数据,为了不修改已有的上下文,我们可以通过继承已有的上下文来扩展一个新的带有银行账户的上下文,然后再客户端中使用新的策略实现和带有银行账户的上下文,这样之前已有的实现完全不需要改动,遵守了开闭原则。

//银行账户支付publicclassAccountPayimplementsPayStrategy{

Overridepublicvoidpay(PayContextctx){PayContextWithAccountctxAccount=(PayContextWithAccount)ctx;System.out.println("现在给:"+ctxAccount.getUsername()+"的账户:"+ctxAccount.getAccount()+"支付工资:"+ctxAccount.getMoney()+"元!");}}

//带银行账户的支付上下文publicclassPayContextWithAccountextendsPayContext{//银行账户privateStringaccount;publicPayContextWithAccount(Stringusername,doublemoney,PayStrategypayStrategy,Stringaccount){super(username,money,payStrategy);this.account=account;}publicStringgetAccount(){returnaccount;}}

//外部客户端publicclassClient{publicstaticvoidmain(String[]args){//创建具体的支付策略PayStrategyrmbStrategy=newRMBPay();PayStrategydollarStrategy=newDollarPay();//准备小王的支付上下文PayContextctx=newPayContext("小王",,rmbStrategy);//向小王支付工资ctx.pay();//准备Jack的支付上下文ctx=newPayContext("jack",00,dollarStrategy);//向Jack支付工资ctx.pay();//创建支付到银行账户的支付策略PayStrategyaccountStrategy=newAccountPay();//准备带有银行账户的上下文ctx=newPayContextWithAccount("小张",,accountStrategy,"");//向小张的账户支付ctx.pay();}}

控制台输出:

现在给:小王人民币支付.0元!现在给:jack美金支付00.0dollar!现在给:小张的账户:支付工资:.0元!

除了上面的方法,还有其他的实现方式吗?

当然有了,上面的实现方式是策略实现所需要的数据都是从上下文中获取,因此扩展了上下文;现在我们可以不扩展上下文,直接从策略实现内部来获取数据,看下面的实现:

//支付到银行账户的策略publicclassAccountPay2implementsPayStrategy{//银行账户privateStringaccount;publicAccountPay2(Stringaccount){this.account=account;}

Overridepublicvoidpay(PayContextctx){System.out.println("现在给:"+ctx.getUsername()+"的账户:"+getAccount()+"支付工资:"+ctx.getMoney()+"元!");}publicStringgetAccount(){returnaccount;}publicvoidsetAccount(Stringaccount){this.account=account;}}

//外部客户端publicclassClient{publicstaticvoidmain(String[]args){//创建具体的支付策略PayStrategyrmbStrategy=newRMBPay();PayStrategydollarStrategy=newDollarPay();//准备小王的支付上下文PayContextctx=newPayContext("小王",,rmbStrategy);//向小王支付工资ctx.pay();//准备Jack的支付上下文ctx=newPayContext("jack",00,dollarStrategy);//向Jack支付工资ctx.pay();//创建支付到银行账户的支付策略PayStrategyaccountStrategy=newAccountPay2("");//准备上下文ctx=newPayContext("小张",,accountStrategy);//向小张的账户支付ctx.pay();}}

控制台输出:

现在给:小王人民币支付.0元!现在给:jack美金支付00.0dollar!现在给:小张的账户:支付工资:.0元!

那我们来比较一下上面两种实现方式:

扩展上下文的实现:

优点:具体的策略实现风格很是统一,策略实现所需要的数据都是从上下文中获取的,在上下文中添加的数据,可以视为公共的数据,其他的策略实现也可以使用。

  

缺点:很明显如果某些数据只是特定的策略实现需要,大部分的策略实现不需要,那这些数据有“浪费”之嫌,另外如果每次添加算法数据都扩展上下文,很容易导致上下文的层级很是复杂。

  

在具体的策略实现上添加所需要的数据的实现:

优点:容易想到,实现简单

  

缺点:与其他的策略实现风格不一致,其他的策略实现所需数据都是来自上下文,而这个策略实现一部分数据来自于自身,一部分数据来自于上下文;外部在使用这个策略实现的时候也和其他的策略实现不一致了,难以以一个统一的方式动态的切换策略实现。

  

策略模式在JDK中的应用:

在多线程编程中,我们经常使用线程池来管理线程,以减缓线程频繁的创建和销毁带来的资源的浪费,在创建线程池的时候,经常使用一个工厂类来创建线程池Executors,实际上Executors的内部使用的是类ThreadPoolExecutor.它有一个最终的构造函数如下:

publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueRunnableworkQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler){if(corePoolSize0

maximumPoolSize=0

maximumPoolSizecorePoolSize

keepAliveTime0)thrownewIllegalArgumentException();if(workQueue==null

threadFactory==null

handler==null)thrownewNullPointerException();this.corePoolSize=corePoolSize;this.maximumPoolSize=maximumPoolSize;this.workQueue=workQueue;this.keepAliveTime=unit.toNanos(keepAliveTime);this.threadFactory=threadFactory;this.handler=handler;}

corePoolSize:线程池中的核心线程数量,即使这些线程没有任务干,也不会将其销毁。

  

maximumPoolSize:线程池中的最多能够创建的线程数量。

  

keepAliveTime:当线程池中的线程数量大于corePoolSize时,多余的线程等待新任务的最长时间。

  

unit:keepAliveTime的时间单位。

  

workQueue:在线程池中的线程还没有还得及执行任务之前,保存任务的队列(当线程池中的线程都有任务在执行的时候,仍然有任务不断的提交过来,这些任务保存在workQueue队列中)。

  

threadFactory:创建线程池中线程的工厂。

  

handler:当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。

  

RejectedExecutionHandler是一个策略接口,用在当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。

线程池的具体介绍和实战,可以

1
查看完整版本: 详解设计模式之策略模式