在生活中我们在做商品促销时,会针对不同的客户有多同的折扣价格,新客户不打折,老客户打九折,vip打八折等等。这就要我们针对不同的客户有不同的定价策略。
所以我会可能会有如下的代码
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; }
}
这次我们将具体的定价策略给抽了出来,这样当某类客户的定价策略发生改变时,我们只需要找到对应的方法进行修改就可以了。
咋一看代码已经有点完美了,可以当我们增加一种客户类型的时候是不是还要在if-else 在添加一个分支?这样显然非常麻烦,而且违反设计模式的开闭原则:对模块扩展开放,对修改模块的源代码关闭。因为在实际的项目中修改源码可能带来不可预知的后果。
定义:
策略模式:定义一系列算法,在分别分别封装各个算法,而这些算法可以相互替代。
结构:
- 策略接口Strategy:用来统一管理算法策略。
- 具体算法策略ConcreteStrategy,具体的策略实现,即具体的算法实现。
- 策略上下文StrategyContext,可理解为为策略的使用者。
策略模式的通用实现。
策略具体实现A
// 具体的策略实现
public class ConcreteStrategyA implements Strategy {
//具体的算法实现
@Override
public void algorithmMethod() {
System.out.println("this is ConcreteStrategyA method...");
}
}
策略具体实现B
// 具体的策略实现
public class ConcreteStrategyB implements Strategy {
//具体的算法实现
@Override
public void algorithmMethod() {
System.out.println("this is ConcreteStrategyB method...");
}
}
策略上下文
/**
* 策略上下文
*/
public class StrategyContext {
//持有一个策略实现的引用
private Strategy strategy;
//使用构造器注入具体的策略类
public StrategyContext(Strategy strategy) {
this.strategy = strategy;
}
public void contextMethod(){
//调用策略实现的方法
strategy.algorithmMethod();
}
}
测试
//外部客户端
public class Client {
public static void main(String[] args) {
//1.创建具体测策略实现
IStrategy strategy = new ConcreteStrategyA();
//2.在创建策略上下文的同时,将具体的策略实现对象注入到策略上下文当中
StrategyContext ctx = new StrategyContext(strategy);
//3.调用上下文对象的方法来完成对具体策略实现的回调
ctx.contextMethod();
}
}
策略模式的作用:
- 把一系列组合中的算法抽离出来,封装成单个算法,在StrategyContext定义策略接口,可实现多态,灵活扩展。
- 策略模式的核心时灵活的组织调用算法,降低代码维护成本。
策略与上下文的关系
在策略模式中,一般情况下都是策略上下文持有策略的引用以便于执行具体的策略,但具体的策略也可以包含上下文对象,以便从上下文中获取数据,具体策略通过回调上下文方法获取数据。
下面我们演示这种情况:
在跨国公司中,一般都会在各个国家和地区设置分支机构,聘用当地人为员工,这样就有这样一个需要:每月发工资的时候,中国国籍的员工要发人民币,美国国籍的员工要发美元,英国国籍的要发英镑。
支付接口策略
//支付策略接口
public interface PayStrategy {
//在支付策略接口的支付方法中含有支付上下文作为参数,以便在具体的支付策略中回调上下文中的方法获取数据
public void pay(PayContext ctx);
}
美金支付策略
//美金支付策略
public class DollarPay implements PayStrategy {
@Override
public void pay(PayContext ctx) {
System.out.println("现在给:"+ctx.getUsername()+" 美金支付 "+ctx.getMoney()+"dollar !");
}
}
人民币支付策略
//人民币支付策略
public class RMBPay implements PayStrategy {
@Override
public void pay(PayContext ctx) {
System.out.println("现在给:"+ctx.getUsername()+" 人民币支付 "+ctx.getMoney()+"元!");
}
}
支付上下文
//支付上下文,含有多个算法的公有数据
public class PayContext {
//员工姓名
private String username;
//员工的工资
private double money;
//支付策略
private PayStrategy payStrategy;
public void pay(){
//调用具体的支付策略来进行支付
payStrategy.pay(this);
}
public PayContext(String username, double money, PayStrategy payStrategy) {
this.username = username;
this.money = money;
this.payStrategy = payStrategy;
}
public String getUsername() {
return username;
}
public double getMoney() {
return money;
}
}
客户端调用
//外部客户端
public class Client {
public static void main(String[] args) {
//创建具体的支付策略
PayStrategy rmbStrategy = new RMBPay();
PayStrategy dollarStrategy = new DollarPay();
//准备小王的支付上下文
PayContext ctx = new PayContext("小王",30000,rmbStrategy);
//向小王支付工资
ctx.pay();
//准备Jack的支付上下文
ctx = new PayContext("jack",10000,dollarStrategy);
//向Jack支付工资
ctx.pay();
}
}
那么现在要新增一个银行账户的支付策略应该怎么办呢?
根据开闭原则,我们不能对原有的代码进行修改,而且鉴于原有的上下文中需要有银行账户的需求,所以我们必须新增一个支付策略,并且扩展出一个带我支付账户的上下文,所以我们可以通过子类继承来实现。
银行账户支付策略
//银行账户支付
public class AccountPay implements PayStrategy {
@Override
public void pay(PayContext ctx) {
PayContextWithAccount ctxAccount = (PayContextWithAccount) ctx;
System.out.println("现在给:"+ctxAccount.getUsername()+"的账户:"+ctxAccount.getAccount()+" 支付工资:"+ctxAccount.getMoney()+" 元!");
}
}
带我银行账户的支付上下文
//带银行账户的支付上下文
public class PayContextWithAccount extends PayContext {
//银行账户
private String account;
public PayContextWithAccount(String username, double money, PayStrategy payStrategy,String account) {
super(username, money, payStrategy);
this.account = account;
}
public String getAccount() {
return account;
}
}
测试客户端
//外部客户端
public class Client {
public static void main(String[] args) {
//创建具体的支付策略
PayStrategy rmbStrategy = new RMBPay();
PayStrategy dollarStrategy = new DollarPay();
//准备小王的支付上下文
PayContext ctx = new PayContext("小王",30000,rmbStrategy);
//向小王支付工资
ctx.pay();
//准备Jack的支付上下文
ctx = new PayContext("jack",10000,dollarStrategy);
//向Jack支付工资
ctx.pay();
//创建支付到银行账户的支付策略
PayStrategy accountStrategy = new AccountPay();
//准备带有银行账户的上下文
ctx = new PayContextWithAccount("小张",40000,accountStrategy,"1234567890");
//向小张的账户支付
ctx.pay();
}
}
上面是从策略的上下文中获取数据,我们还可以在策略实现中添账户,直接获取数据。
//支付到银行账户的策略
public class AccountPay2 implements PayStrategy {
//银行账户
private String account;
public AccountPay2(String account) {
this.account = account;
}
@Override
public void pay(PayContext ctx) {
System.out.println("现在给:"+ctx.getUsername()+"的账户:"+getAccount()+" 支付工资:"+ctx.getMoney()+" 元!");
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
}
测试客户端
//外部客户端
public class Client {
public static void main(String[] args) {
//创建具体的支付策略
PayStrategy rmbStrategy = new RMBPay();
PayStrategy dollarStrategy = new DollarPay();
//准备小王的支付上下文
PayContext ctx = new PayContext("小王",30000,rmbStrategy);
//向小王支付工资
ctx.pay();
//准备Jack的支付上下文
ctx = new PayContext("jack",10000,dollarStrategy);
//向Jack支付工资
ctx.pay();
//创建支付到银行账户的支付策略
PayStrategy accountStrategy = new AccountPay2("1234567890");
//准备上下文
ctx = new PayContext("小张",40000,accountStrategy);
//向小张的账户支付
ctx.pay();
}
}
两种实现的比较
扩展上下文的实现:
优点:具体的策略实现风格非常统一,策略实现所需要的数据都是从上下文中直接获取,而且这数据是其他策略实现可以共享的。
缺点:某些数据的存在只是服务特定策略场景,大部分策略实现并不需要,会有数据浪费之嫌,而且如果每次都直接上下文中扩展算法数据,长期来看会使上下文层次结构复杂,难以维护。
具体策略中扩展数据:
优点:简单,且非常容易实现。
缺点:会使整体的策略实现风格不一致,一部分数据来自上下文中,一部分数据来自策略实现本身,外部在使用策略实现的时候和其他策略实现不一致,对于动态的切换策略不方便。
策略模式在JDK中的应用:
在多线程编程中,我们经常使用线程池来管理线程,减少线程的频繁地创建和销毁,消耗系统资源。在创建线程池的时候,我们经常使用Executor工厂类来创建线程池,实际上Executors的是使用ThreadPoolExecutor,它又一个构造函数。
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;
}
corePoolSize:线程池中核心线程数量,即使这些线程没有任务干,也会存在。
maximumPoolSize:线程池中最多能创建的线程数量。
keepAliveTime:当线程池中线程数量大于corePoolSize时,多余的线程等待任务的最长时间。
unit:keepAliveTime的时间单位
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);
}
该策略接口主要有四个在ThreadPoolExecutor类中实现的静态类部类。
AbortPolicy:该策略直接将提交的任务抛弃掉,并抛出RejectedExecutionException异常
/**
* A handler for rejected tasks that throws a
* {@code RejectedExecutionException}.
*/
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
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("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
DiscardPolicy:该策略也是将提交策略抛弃掉,不管不问,但是不抛出异常
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
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) {
}
}
DiscardIOldestPolicy:当执行器未关闭时,从任务队列中取出第一任务并抛弃掉,腾出空间给刚刚提交的任务。因为它会抛弃掉之前的任务,所以慎重使用。
/**
* A handler for rejected tasks that discards the oldest unhandled
* request and then retries {@code execute}, unless the executor
* is shut down, in which case the task is discarded.
*/
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} 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);
}
}
}
CallerRunsPolicy:该策略不会抛弃任何任务,因为当前线程池中已经没有多余的线程来执行任务,该策略是在当前的线程中直接调用。
/**
* 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();
}
}
}
类ThreadPoolExecutor中持有一个RejectedExecutionHandler的接口引用,以便可以在外部直接通过构造函数进行策略注入。
策略模式的优点
- 策略模式的功能就是将一组算法中的各个算法抽出共性的方法,再根据各个算法的特点封装自己特有的属性,如果有公共的属性可以封装为抽象类给子类继承。
- 策略模式的一系列算法是可以相互替换的,是平等的,写在一起就是if-else的结构
- 扩展性比较好,直接实现接口就可以直接替换使用。
策略模式的缺点
- 使用者必须了解所有的策略,清楚他们的不同
- 增加了对象的数量,因为策略模式是将每一个算法都单独的封装成具体的策略类,如果可选的策略很多,数量也会很多。
- 扁平的算法结构模式:各个算法的地位是平等是兄弟关系,限制了算法的使用层级不能嵌套。
策略模式的本质:分离算法,按需实现
如果仔细考虑策略模式的结构和功能的话,会发现没有上下文,我们就回到了面向接口的编程习惯,统一的策略接口和各个具体的算法策略实现,好像和上下文没啥关系,但是当我们用客户端进行操作的时候,客户端会直接与策略接口进行交互,当涉及到一些公共数据和方法的使用的时候会有大量的冗余数据,和复杂的操作的实现,而有了上下,这些操作就可以交给上下文来与客户端进行交互。
策略模式体现了设计的模式开闭原则和里氏替换原则。