设计模式之关于策略模式一二
一、定义
策略模式是将不同的算法进行封装,彼此独立可替换,通过策略上下文让客户端使用的设计模式。
二、使用场景
在大部分电商促销活动时,平台会针对不同的客户指定不同的价格营销策略。比如普通会员9折,黄金会员8折,铂金会员7折…
现在我们如何设计这种价格策略呢?
各种if-else 几千行代码在同一个类中;
public BigDecimal excute(String customerType,BigDecimal price){
if (customerType.equals("普通会员")){
//xxxxx
// 执行普通会员折扣
return price;
}else if (customerType.equals("黄金会员")){
//xxxxx
// 执行黄金会员折扣
return price;
}else if (customerType.equals("铂金会员")){
//xxxxx
// 执行铂金会员折扣
return price;
}
return null;
}
策略模式的实现:
1定义策略接口如下:
/**
* 价格营销策略接口
*/
public interface PricePolicy {
//执行响应价格
BigDecimal priceExcute(BigDecimal originalPrice);
}
2实现不同的营销策略:
普通会员
public class NormalCustomer implements PricePolicy {
@Override
public BigDecimal priceExcute( BigDecimal originalPrice) {
originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP);
System.out.println("普通会员价格:"+ originalPrice );
return originalPrice;
}
}
黄金会员
public class VipCustomer implements PricePolicy {
@Override
public BigDecimal priceExcute( BigDecimal originalPrice) {
originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP);
System.out.println("黄金会员价格:"+ originalPrice);
return originalPrice;
}
}
铂金会员
public class VVipCustomer implements PricePolicy {
@Override
public BigDecimal priceExcute( BigDecimal originalPrice) {
originalPrice = originalPrice.multiply(new BigDecimal(0.7)).setScale(2,BigDecimal.ROUND_HALF_UP);
System.out.println("铂金会员价格:"+ originalPrice);
return originalPrice;
}
}
3.策略上下文
//价格策略上下文
public class PriceContext {
private PricePolicy pricePolicy;
public PriceContext(PricePolicy policy){
this.pricePolicy = policy;
}
//回调具体的价格策略
public BigDecimal getPrice(BigDecimal price){
return pricePolicy.priceExcute(price);
}
}
4客户端测试
//铂金客户
VVipCustomer vv = new VVipCustomer();
PriceContext priceContext = new PriceContext(vv);
priceContext.getPrice(new BigDecimal(1000.0));
//黄金客户
VipCustomer v = new VipCustomer();
PriceContext pc = new PriceContext(v);
pc.getPrice(new BigDecimal(1000.0));
//普通客户
NormalCustomer nc = new NormalCustomer();
PriceContext context = new PriceContext(nc);
context.getPrice(new BigDecimal(1000.0));
}
当需要新增会员等级类型时,直接实现策略接口:
public class SVipCustomer implements PricePolicy {
@Override
public BigDecimal priceExcute(BigDecimal originalPrice) {
originalPrice = originalPrice.multiply(new BigDecimal(0.6)).setScale(2,BigDecimal.ROUND_HALF_UP);
System.out.println("钻石会员价格:"+originalPrice);
return originalPrice;
}
}
举个历史上有名的故事:
三国刘备取西川时,谋士庞统给的上、中、下三个建议:
上策:挑选精兵,昼夜兼行直接偷袭成都,可以一举而定,此为上计计也。
中策:杨怀、高沛是蜀中名将,手下有精锐部队,而且据守关头,我们可以装作要回荆州,引他们轻骑来见,可就此将其擒杀,而后进兵成都,此为中计。
下策:退还白帝,连引荆州,慢慢进图益州,此为下计。
这三个计策都是取西川的计策,也就是攻取西川这个问题的具体的策略算法,刘备可以采用上策,可以采用中策,当然也可以采用下策,由此可见策略模式的各种具体的策略算法都是平等的,可以相互替换。
那谁来选择具体采用哪种计策(算法)?
在这个故事中当然是刘备选择了,也就是外部的客户端选择使用某个具体的算法,然后把该算法(计策)设置到上下文当中;
还有一种情况就是客户端不选择具体的算法,把这个事交给上下文,这相当于刘备说我不管有哪些攻取西川的计策,我只要结果(成功的拿下西川),具体怎么攻占(有哪些计策,怎么选择)由参谋部来决定(上下文)。
策略和上下文的关系:
在策略模式中,一般情况下都是上下文持有策略的引用,以进行对具体策略的调用。但具体的策略对象也可以从上下文中获取所需数据,可以将上下文当做参数传入到具体策略中,具体策略通过回调上下文中的方法来获取其所需要的数据。
下面演示这种情况:
在跨国公司中,一般会在各个国家和地区设立分支机构,聘用当地人为员工,这样就有一个需求:每个月发工资时,中国国籍员工要发人民币,美国国籍员工要发美元,英国国籍员工要发英镑。
支付策略接口:
/**
* 支付策略接口
*/
public interface PayStrategy {
/**
* //这个支付方法中用支付上下文作为参数,以便在具体策略执行时获取上下文中所需数据
* @param contex
*/
public void pay(PayContext contex);
}
人民币支付:
//人民币支付策略
public class RMBPay implements PayStrategy {
@Override
public void pay(PayContext contex) {
System.out.println("现在给:"+ contex.getUsername()+"人民币支付:"+contex.getMoney()+"元!");
}
}
美元支付:
public class DollarPay implements PayStrategy {
@Override
public void pay(PayContext contex) {
System.out.println("现在给:"+ contex.getUsername()+"美元支付:"+contex.getMoney()+"美元!");
}
}
策略上下文:(包含公共数据)
/**
* 策略上下文
* 包含公共数据
*/
public class PayContext {
private String username;
private Double money;
//支付策略
private PayStrategy payStrategy;
public PayContext(String name,Double money,PayStrategy strategy){
this.username = name;
this.money = money;
this.payStrategy = strategy;
}
//调用具体的支付策略来支付
public void pay(){
payStrategy.pay(this);
}
public String getUsername() {
return username;
}
public Double getMoney() {
return money;
}
}
但是现在我们需要新增一个银行账户的支付策略,该怎么办呢?
显然我们应该新增一个支付找银行账户的策略实现,由于需要从上下文中获取数据,为了不修改上下文,,我们可以通过集成已有的上下文来拓展一个新的带有银行账户的上下文,然后客户端使用新的策略和带有银行账户的上下文,这样之前已有的实现完全不用改动,遵守了开闭原则。
//带银行账户的上下文
public class PayContextWithAccount extends PayContext {
//银行账户
private String account;
public PayContextWithAccount(String name, Double money, String account,PayStrategy strategy) {
super(name, money, strategy);
this.account = account;
}
public String getAccount(){
return account;
}
}
//添加银行支付
public class AccountPay implements PayStrategy {
@Override
public void pay(PayContext contex) {
PayContextWithAccount cont = (PayContextWithAccount)contex;
System.out.println("现在给《"+cont.getUsername()+"的银行账户:"+cont.getAccount()+" ,支付转账:"+cont.getMoney()+"元!");
}
}
除了上面的方法,还有其他方法吗?
上面的实现方式是策略实现所需的数据都从上下文中获取,因此拓展了上下文;现在我们可以不扩展上下文,,直接从策略实现内部来获取数据。
从策略实现内部获取数据
public class AccountPay implements PayStrategy{
private String account;
public AccountPay(String account){
this.account = account;
}
@Override
public void pay(PayContext contex) {
System.out.println("现在给《"+contex.getUsername()+"的银行账户:"+getAccount()+" ,支付转账:"+contex.getMoney()+"元!");
}
public String getAccount() {
return account;
}
}
比较两者之间的不同:
1拓展上下文的实现:
优点:具体的策略实现风格统一,策略所需的数据都是从上下文获取,在上下文添加数据,可视为公共数据,其他策略也可使用。
缺点:很明显很多数据只是特定策略实现需要,有浪费资源,另外,每次添加算法都扩展上下文,导致上下文层级复杂。
2在具体策略中添加所需数据的实现:
优点:实现简单
缺点:各个策略实现风格不统一,一部分数据来自上下文,一部分数据来自自身;外部在使用这个策略的时候和其他的不同,难以用统一的方式切换策略实现。
源码参考:
策略模式在JDK中应用:
在多线程编程中,我们经常使用线程池来管理线程,以减少线程频繁创建和销毁所带来的资源浪费,在创建线程池的时候,经常用一个工厂类来创建线程池Executors,实际上Excutors内部使用的事类ThreadPoolExcutor,它有一个最终的构造函数:
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
其中的handle:当线程池中没有多余线程来执行任务,并且保存任务的队列也满了,此时对仍在提交给线程池的任务的处理策略。
RejectExecutionHandle 是一个策略接口,用在上述情况的处理。
该策略类有四个实现类:
AbortPolicy : 该策略是直接将提交的任务抛弃掉,并抛出RejectedExcutionException异常。
DiscardPolicy:该策略也是将任务抛弃掉,不过并不抛出异常。
DiscardOldesPolicy:该策略是当执行器未关闭时,从任务队列取出第一个任务并抛弃,进而有空间存储刚提交的任务。
CallRunsPolicy:该策略并没有抛弃任何异常,由于线程池中已经没有对于线程来分配该任务,该策略是在当前线程(调用者线程)中直接执行该任务。
策略模式的优点:
1、通过抽象、封装定义一系列算法,使得这些算法可以相互替换,所以为这些算法定义了公共的接口,以约束这些算法功能的实现。如果这些算法具有公共的功能,可以将接口变为抽象类,将公共的功能放到抽象父类里面。
2、这一系列算法是可以相互替换的、平等的,写在一起就是if-else组织结构,也可以避免多重条件语句。
3、拓展性更好:在策略模式中拓展策略实现非常容易,只要新增一个策略实现。
缺点:
1、客户端必须了解所有的策略,清除他们的不同;
如果是客户端来决定使用何种算法,那客户端必须知道所有策略,清除各个策略的功能和不同,这样才能做出正确的选择,但这也暴露了策略的具体实现。
2、增加了对象的数量,由于每种策略单独封装一个策略类,
3、只适合扁平的算法结构:一个策略接口下的多个策略实现是兄弟关系,并且运行时只能有一个算法被使用。