策略模式是什么
其思想是针对一组算法,将每一种算法都封装到具有共同接口的独立的类中,从而是它们可以相互替换。策略模式的最大特点是使得算法可以在不影响客户端的情况下发生变化,从而改变不同的功能。
策略模式 本质:分离算法,选择实现
主要解决
- 在有多种算法相似的情况下,使用 if…else 或 switch…case 所带来的复杂性和臃肿性。
优缺点
优点
- 算法多样性,且具备自由切换功能; 有效避免多重条件判断,增强了封装性,
- 简化了操作,降低出错概率; 扩展性良好,策略类遵顼
- 里氏替换原则,可以很方便地进行策略扩展;
缺点
- 策略类数量增多,且所有策略类都必须对外暴露,以便客户端能进行选择;
模式讲解
首先来看下 策略模式 的通用 UML 类图:
从 UML 类图中,我们可以看到,策略模式 主要包含三种角色:
-
上下文角色(Context):用来操作策略的上下文环境,屏蔽高层模块(客户端)对策略,算法的直接访问,封装可能存在的变化;
-
抽象策略角色(Strategy):规定策略或算法的行为;
-
具体策略角色(ConcreteStrategy):具体的策略或算法实现;
事例引入
1.商场折扣
现实生活中我们到商场买东西的时候,卖场往往根据不同的客户制定不同的报价策略,比如针对新客户不打折扣,针对老客户打9折,针对VIP客户打8折…
现在我们要做一个报价管理的模块,简要点就是要针对不同的客户,提供不同的折扣报价。 如果是有你来做,你会怎么做?
我们很有可能写出下面的代码:
import java.math.BigDecimal;
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;
}
}
经过测试,上面的代码工作的很好,可是上面的代码是有问题的。上面存在的问题:把不同客户的报价的算法都放在了同一个方法里面,使得该方法很是庞大(现在是只是一个演示,所以看起来还不是很臃肿)。
1.1改进代码
我们把不同客户的报价的算法都单独作为一个方法
package strategy.examp02;
import java.math.BigDecimal;
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;
}
}
2.存在问题
上面的代码比刚开始的时候要好一点,它把每个具体的算法都单独抽出来作为一个方法,当某一个具体的算法有了变动的时候,只需要修改响应的报价算法就可以了。
但是改进后的代码还是有问题的,那有什么问题呢?
- 1.当我们新增一个客户类型的时候,首先要添加一个该种客户类型的报价算法方法,然后再quote方法中再加一个else if的分支,是不是感觉很是麻烦呢?而且这也违反了设计原则之一的开闭原则(open-closed-principle)
- 2.我们经常会面临这样的情况,不同的时期使用不同的报价规则,比如在各个节假日举行的各种促销活动时、商场店庆时往往都有普遍的折扣,但是促销时间一旦过去,报价就要回到正常价格上来。按照上面的代码我们就得修改if
else里面的代 码很是麻烦
开闭原则
- 对于扩展是开放的(Open for
extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。 - 对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。
解决方法
策略模式代码的一般通用实现:
- 策略接口
//策略接口
public interface IStrategy {
//定义的抽象算法方法 来约束具体的算法实现方法
public void algorithmMethod();
}
- 具体的策略实现:
// 具体的策略实现1
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);
}
新客户报价策略实现:
import java.math.BigDecimal;
//新客户的报价策略实现类
public class NewCustomerQuoteStrategy implements IQuoteStrategy {
@Override
public BigDecimal getPrice(BigDecimal originalPrice) {
System.out.println("抱歉!新客户没有折扣!");
return originalPrice;
}
}
老客户报价策略实现:
import java.math.BigDecimal;
//老客户的报价策略实现
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;
}
}
VIP客户报价策略实现:
import java.math.BigDecimal;
//VIP客户的报价策略实现
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;
}
}
报价上下文:
import java.math.BigDecimal;
//报价上下文角色
public class QuoteContext {
//持有一个具体的报价策略
private IQuoteStrategy quoteStrategy;
//注入报价策略
public QuoteContext(IQuoteStrategy quoteStrategy){
this.quoteStrategy = quoteStrategy;
}
//回调具体报价策略的方法
public BigDecimal getPrice(BigDecimal originalPrice){
return quoteStrategy.getPrice(originalPrice);
}
}
外部客户端:
import java.math.BigDecimal;
//外部客户端
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);
}
}
控制台输出:
恭喜!老客户享有9折优惠!
折扣价为:90.00
这个时候,商场营销部新推出了一个客户类型–MVP用户(Most Valuable Person),可以享受折扣7折优惠,那该怎么办呢?
这个很容易,只要新增一个报价策略的实现,然后外部客户端调用的时候,创建这个新增的报价策略实现,并设置到策略上下文就可以了,对原来已经实现的代码没有任何的改动。
MVP用户的报价策略实现:
import java.math.BigDecimal;
//MVP客户的报价策略实现
public class MVPCustomerQuoteStrategy implements IQuoteStrategy {
@Override
public BigDecimal getPrice(BigDecimal originalPrice) {
System.out.println("哇偶!MVP客户享受7折优惠!!!");
originalPrice = originalPrice.multiply(new BigDecimal(0.7)).setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
}
外部客户端:
import java.math.BigDecimal;
//外部客户端
public class Client {
public static void main(String[] args) {
//创建MVP客户的报价策略
IQuoteStrategy mvpQuoteStrategy = new MVPCustomerQuoteStrategy();
//创建报价上下文对象,并设置具体的报价策略
QuoteContext quoteContext = new QuoteContext(mvpQuoteStrategy);
//调用报价上下文的方法
BigDecimal price = quoteContext.getPrice(new BigDecimal(100));
System.out.println("折扣价为:" +price);
}
}
控制台输出:
哇偶! MVP客户享受7折优惠!!!
折扣价为:70.00
刘备取西川
三国刘备取西川时,谋士庞统给的上、中、下三个计策:
- 上策:挑选精兵,昼夜兼行直接偷袭成都,可以一举而定,此为上计计也。
- 中策:杨怀、高沛是蜀中名将,手下有精锐部队,而且据守关头,我们可以装作要回荆州,引他们轻骑来见,可就此将其擒杀,而后进兵成都,此为中计。
- 下策:退还白帝,连引荆州,慢慢进图益州,此为下计。
这三个计策都是取西川的计策,也就是攻取西川这个问题的具体的策略算法,刘备可以采用上策,可以采用中策,当然也可以采用下策,由此可见策略模式的各种具体的策略算法都是平等的,可以相互替换。
那谁来选择具体采用哪种计策(算法)?
在这个故事中当然是刘备选择了,也就是外部的客户端选择使用某个具体的算法,然后把该算法(计策)设置到上下文当中;
还有一种情况就是客户端不选择具体的算法,把这个事交给上下文,这相当于刘备说我不管有哪些攻取西川的计策,我只要结果(成功的拿下西川),具体怎么攻占(有哪些计策,怎么选择)由参谋部来决定(上下文)。
下面我们演示下这种情景:
//攻取西川的策略
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("拿下西川");
//这个人人有赏,让士兵有动力啊
context.occupationWestOfSichuan("拿下西川之后,人人有赏!");
}
}
控制台输出: 由于计划泄露,上上计策失败!
杨怀、高沛是蜀中名将,手下有精锐部队,而且据守关头,我们可以装作要回荆州,引他们轻骑来见,可就此将其擒杀,而后进兵成都,此为中计。
挑选精兵,昼夜兼行直接偷袭成都,可以一举而定,此为上计计也!