0
点赞
收藏
分享

微信扫一扫

深入理解策略设计模式

目录

​​一、what is 策略?​​

​​二、what is 策略模式?​​

​​2.1  策略模式中的角色​​

​​2.2  策略模式通用实现​​

​​2.3  案例改写​​

​​三、策略模式核心思想​​

​​四、策略上下文角色的作用​​

​​4.1  从策略上下文获取数据​​

​​4.2  策略实现类自己添加所需的数据​​

​​五、策略模式优缺点​​

一、what is 策略?

大家看到一堆if-else嵌套都会觉得很烦很low,策略设计模式可以帮你干掉这些if-else,还给你清爽高逼格。

那什么是策略,百度百科给了“策略”很精简的解释:

深入理解策略设计模式_父类

大白话就是完成目标的方式有很多,根据条件因地制宜选择合适的方式,只要能完成目标就好。“条条大道通罗马”也差不多是这个意思。 

二、what is 策略模式?

知道了策略是啥,策略模式又是啥?策略模式定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立,根据不同的需求选择不同的算法(实现方式)。它的中心是算法如何组织和调用。

上面的定义还是有些抽象,下面我们化抽象为具体:

我们逛街吃饭结账的时候应该都用过信用卡(没用过信用卡的土豪忽略),商家对于不同银行的信用卡的优惠力度往往是不同的,如果你去结账,收银员会根据你出示的信用卡选择不同的优惠政策(中国银行7折、农业银行8折、农业银行9折、其他银行原价无优惠),不用策略模式我们可能会这样写:

/**
* Feng, Ge 2020/3/11 0011 15:55
*/
public class OriginalDemo {

private static Double originalPrice = 100.0;

private static Double getDiscount(String bankName) {
if ("中国银行".equals(bankName)) {
System.out.println("中国银行7折!");
return 0.7 * originalPrice;
} else if ("农业银行".equals(bankName)) {
System.out.println("农业银行8折!");
return 0.8 * originalPrice;
} else if ("光大银行".equals(bankName)) {
System.out.println("农业银行9折!");
return 0.9 * originalPrice;
}
return originalPrice;
}

public static void main(String[] args) {
Double price = getDiscount("中国银行");
System.out.println("总共: " + price + " 元");
}
}
中国银行7折!
总共: 70.0 元

上面的代码肯定是可以实现所需功能的,但是这个getDiscount()方法是把所有的优惠方式算法都写在里面了,若还有其他优惠形式,那就是不停的else-if,代码显得臃肿不简洁(显得low)。

那可能会这样去改进:

/**
* Feng, Ge 2020/3/11 0011 15:55
*/
public class OriginalBetterDemo {

private static Double originalPrice = 100.0;

private static Double getDiscount(String bankName) {
if ("中国银行".equals(bankName)) {
return getBOC();
} else if ("农业银行".equals(bankName)) {
return getABC();
} else if ("光大银行".equals(bankName)) {
return getCEB();
}
return originalPrice;
}

private static Double getBOC(){
System.out.println("中国银行7折!");
return 0.7 * originalPrice;
}

private static Double getABC(){
System.out.println("农业银行8折!");
return 0.8 * originalPrice;
}

private static Double getCEB(){
System.out.println("光大银行9折!");
return 0.9 * originalPrice;
}

public static void main(String[] args) {
Double price = getDiscount("中国银行");
System.out.println("总共: " + price + " 元");
}
}

现在是把各种优惠方式(算法)单独抽取出来了,这个优化方式已经带来了好处:当某个银行的优惠力度改变时,只需要改变对应银行的优惠算法即可,比如中国银行现在优惠变成5折了,只需要修改getBOC()即可,不会影响别的银行的优惠方式。

这个改进看着还不错哈,但是假如现在商家要和工商合作了,给工商银行信用卡的优惠为6折,该怎么办?那就需要新增一个getICBC()方法,同时在getDiscount(String bankName) 方法中增加条件语句。这么一处理,就会觉得好像哪里不对劲,仔细一想这样违背了开闭原则:对修改关闭,对扩展开放。扩展一个优惠对象除了新增算法,还需要改判断逻辑,这显然不是最好的处理方式。有没有更好的处理方式呢?当然有,就是下面要说的策略模式。

2.1  策略模式中的角色

  1. 策略接口角色:用来约束一系列具体的策略算法,策略上下文角色ConcreteStrategy使用此策略接口来调用具体的策略所实现的算法。
  2. 具体策略实现角色:具体的策略实现,即具体的算法实现。
  3. 策略上下文角色Context:策略上下文,负责和具体的策略实现交互,通常策略上下文对象会持有一个真正的策略实现对象,策略上下文还可以让具体的策略实现从其中获取相关数据,回调策略上下文对象的方法。

实际上策略模式中每个算法对应一个具体策略实类,通过策略上下文类实现动态逻辑(持有谁就选择谁的算法)。

2.2  策略模式通用实现

策略接口:

public interface StrategyInterface {
void algorithm();
}

具体策略实现A:

public class ConcreteStrategyA implements StrategyInterface {
@Override
public void algorithm() {
System.out.println("具体策略类A");
}
}

具体策略实现B:

public class ConcreteStrategyB implements StrategyInterface {
@Override
public void algorithm() {
System.out.println("具体策略类B");
}
}

策略上下文类:

public class StrategyContext {
private StrategyInterface strategy;

public StrategyContext(StrategyInterface strategy) {
this.strategy = strategy;
}

public void contextMethod(){
strategy.algorithm();
}
}

测试类:

public class CommonClient {
public static void main(String[] args) {
StrategyInterface strategy = new ConcreteStrategyA();
StrategyContext context = new StrategyContext(strategy);
context.contextMethod();
}
}

2.3  案例改写

知道了策略模式的主体结构就可以改写最开始的例子了:

 策略接口:

/**
* Feng, Ge 2020/3/12 0012 8:37
*/
public interface Strategy {
Double discountAlgorithm(Double originalPrice);
}

具体策略实现ABC:

public class StrategyABC implements Strategy{

@Override
public Double discountAlgorithm(Double originalPrice) {
System.out.println("农业银行8折!");
return 0.8 * originalPrice;
}
}

具体策略实现BOC:

public class StrategyBOC implements Strategy{

@Override
public Double discountAlgorithm(Double originalPrice) {
System.out.println("中国银行7折!");
return 0.7 * originalPrice;
}
}

具体策略实现CEB:

public class StrategyCEB implements Strategy{

@Override
public Double discountAlgorithm(Double originalPrice) {
System.out.println("光大银行9折!");
return 0.9 * originalPrice;
}
}

策略上下文类:

public class Context {
private Strategy strategy;
private static Double originnalPrice = 100.0;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public Double getDiscount() {
return strategy.discountAlgorithm(originnalPrice);
}
}

测试类:

public class Client {
public static void main(String[] args) {
// 顾客出示农业银行信用卡
Strategy strategy = new StrategyABC();
// 选择农业银行的优惠策略
Context context = new Context(strategy);
// 计算具体价格
Double price = context.getDiscount();
System.out.println("您共消费:" + price +"元!");
}
}
农业银行8折!
您共消费:80.0元!

这么写有什么好处?那就是很好的践行了“开闭原则”,易于扩展,不用改变原来的逻辑条件。比如现在商家要和工商合作了,给工商银行信用卡的优惠为6折,那只需要增加工商银行的具体策略实现类即可。其余不用变:

public class StrategyICBC implements Strategy{

@Override
public Double discountAlgorithm(Double originalPrice) {
System.out.println("工商银行6折!");
return 0.6 * originalPrice;
}
}

这个在支付方式或者优惠方式很多很复杂的情况下,比如淘宝双十一的各种优惠,要是用if--else去嵌套写,估计没人能看懂没人能维护。

三、策略模式核心思想

通过上面的例子,可以进一步总结一下策略模式:

它就是把具体的算法实现从业务逻辑中剥离出来,成为一系列独立算法类,使得它们可以根据需要被动态选择。

但是需要注意的是:策略模式的重心不是考虑算法如何实现,而是关心这些算法如何组织和调用。

四、策略上下文角色的作用

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

实际上你也可以选择去掉上下文类,把处理流程放在Client客户端,如下:

public class ClientDemo {
private static Strategy strategy;
private static Double originnalPrice = 100.0;

public static Double getDiscount() {
return strategy.discountAlgorithm(originnalPrice);
}

public static void main(String[] args) {
strategy = new StrategyABC();
Double price = getDiscount();
System.out.println("您共消费:" + price +"元!");
}
}

虽然这样也没啥大问题,但仔细想想还是有问题的,在一个使用策略模式的系统中,当存在的策略很多时,客户端管理所有策略算法将变得很复杂,如果在环境类中使用策略工厂模式来管理这些策略类将大大减少客户端的工作复杂度,因此策略上下文这个角色还是很必要的。

4.1  从策略上下文获取数据

“策略上下文还可以让具体的策略实现从其中获取相关数据,回调策略上下文对象的方法”是怎么一回事?其实就是将上下文对象作为参数传递到具体策略实现类中。例如我们在之前的例子中originalPrice是参数,现在我们可以把整个上下文对象都传到具体策略实现类中:

public interface Strategy {
Double discountAlgorithm(Context context);
}
public class StrategyABC implements Strategy {

@Override
public Double discountAlgorithm(Context context) {
System.out.println("农业银行8折!");
System.out.println("您的卡号:" + context.getCardNo());
return 0.8 * context.getOriginnalPrice();
}
}
public class StrategyBOC implements Strategy {

@Override
public Double discountAlgorithm(Context context) {
System.out.println("中国银行7折!");
System.out.println("您的卡号:" + context.getCardNo());
return 0.7 * context.getOriginnalPrice();
}
}
public class StrategyCEB implements Strategy{

@Override
public Double discountAlgorithm(Double originalPrice) {
System.out.println("光大银行9折!");
return 0.9 * originalPrice;
}
}

策略上下文:

public class Context {
private Strategy strategy;
private Double originnalPrice;
private String cardNo;

public String getCardNo() {
return cardNo;
}

public Double getOriginnalPrice() {
return originnalPrice;
}

public Context(Strategy strategy, Double originnalPrice, String cardNo) {
this.strategy = strategy;
this.originnalPrice = originnalPrice;
this.cardNo = cardNo;
}

public Double getDiscount() {
return strategy.discountAlgorithm(this);
}
}
public class Client {
public static void main(String[] args) {
// 顾客出示农业银行信用卡
Strategy strategy = new StrategyABC();
// 选择农业银行的优惠策略
Context context = new Context(strategy, 100.0, "123456789");
// 计算具体价格
Double price = context.getDiscount();
System.out.println("您共消费:" + price +"元!");
}
}
农业银行8折!
您的卡号:123456789
您共消费:80.0元!

这里策略上下文类增加了银行卡号,策略实现类中接收上下文对象,就可以取到客户端构造上下文对象时传入的值。

这样做有什么好处?策略实现所需要的数据都是从上下文中获取的,在上下文中添加的数据,可以视为公共的数据,其他的策略实现也可以使用;但是也有不好的地方,所有策略实现类共有一个上下文数据,当策略实现类很多时,上下文类会变得复杂,此外如果某些数据只是特定的策略实现需要,大部分的策略实现不需要,就会造成资源浪费。

另外如果每次添加算法数据都扩展上下文,很容易导致上下文的层级很是复杂。

4.2  策略实现类自己添加所需的数据

上面说到所有策略实现类统一从上下文中获取数据是有缺陷的,某些策略实现类需要一些特定的数据但其他策略实现类不需要时可以在将数据直接添加在某些特定的策略实现类中,比如上面的卡号信息,可以定义在策略实现类中:

public class StrategyABC implements Strategy {
private String cardNo;

public StrategyABC(String cardNo) {
this.cardNo = cardNo;
}

public void setCardNo(String cardNo) {
this.cardNo = cardNo;
}

public String getCardNo() {
return cardNo;
}

@Override
public Double discountAlgorithm(Context context) {
System.out.println("农业银行8折!");
System.out.println("您的卡号:" + cardNo);
return 0.8 * context.getOriginnalPrice();
}
}
public class Context {
private Strategy strategy;
private Double originnalPrice;

public Double getOriginnalPrice() {
return originnalPrice;
}

public Context(Strategy strategy, Double originnalPrice) {
this.strategy = strategy;
this.originnalPrice = originnalPrice;
}

public Double getDiscount() {
return strategy.discountAlgorithm(this);
}
}
public class Client {
public static void main(String[] args) {
// 顾客出示农业银行信用卡
Strategy strategy = new StrategyABC("123456");
// 选择农业银行的优惠策略
Context context = new Context(strategy, 100.0);
// 计算具体价格
Double price = context.getDiscount();
System.out.println("您共消费:" + price +"元!");
}
}
农业银行8折!
您的卡号:123456
您共消费:80.0元!

五、策略模式JDK中的应用

ThreadPoolExecutor(线程池)都知道它有拒绝策略,就是线程数已经达到最大,任务队列也满了,这个时候又有新任务要采取的行为。

这个拒绝策略是 ThreadPoolExecutor 的一个属性,且有默认策略:

深入理解策略设计模式_数据_02

深入理解策略设计模式_数据_03

可以看下这个拒绝策略可以在线程池构造时就指定:

深入理解策略设计模式_数据_04

RejectedExecutionHandler,这个参数代表的是拒绝策略(有四种具体的实现:直接抛出异常、使用调用者的线程来处理、直接丢掉这个任务、丢掉最老的任务):

深入理解策略设计模式_数据_05

 上面四个就是 RejectedExecutionHandler 接口的实现类,也就是4个具体策略类。这四个类都在 ThreadPoolExecutor 线程池类里面,是它的静态内部类。由于策略模式将每个具体的算法都单独封装为一个策略类,这样处理的好处就是使得类文件不那么多:

深入理解策略设计模式_父类_06

深入理解策略设计模式_策略模式_07

深入理解策略设计模式_父类_08深入理解策略设计模式_策略模式_09

深入理解策略设计模式_数据_10

ok,策略接口 及 具体策略类都有了 ,那上下文呢?ThreadPoolExecutor 中有个 reject()方法,这个就是上下文:

深入理解策略设计模式_策略模式_11

 客户端具体的调用者就是 ThreadPoolExecutor 的核心任务执行方法  execute():

 深入理解策略设计模式_数据_12

六、策略模式优缺点

知道了策略模式的实现方式,下面讨论下它的优缺点:

优点:

  1. 策略模式的一系列算法是可以相互替换的、是平等的,写在一起就是if-else组织结构,如果算法实现里又有条件语句,就构成了多重条件语句,可以用策略模式,避免这样的多重条件语句。
  2. 扩展性更好:在策略模式中扩展策略实现非常的容易,只要新增一个策略实现类,然后在使用策略实现的地方,使用这个新的策略实现就好了。

缺点:

  1. 客户端必须了解所有的策略,清楚它们的不同:如果由客户端来决定使用何种算法,那客户端必须知道所有的策略,清楚各个策略的功能和不同,这样才能做出正确的选择,但是这暴露了策略的具体实现。
  2. 增加了对象的数量:由于策略模式将每个具体的算法都单独封装为一个策略类,如果可选的策略有很多的话,那对象的数量也会很多。
  3. 只适合偏平的算法结构:由于策略模式的各个策略实现是平等的关系(可相互替换),实际上就构成了一个扁平的算法结构。即一个策略接口下面有多个平等的策略实现(多个策略实现是兄弟关系),并且运行时只能有一个算法被使用。这就限制了算法的使用层级,且不能被嵌套。

策略模式体现了两个是设计原则:

  • 开闭原则:策略模式把一系列的可变算法进行封装,从而定义了良好的程序结构,在出现新的算法的时候,可以很容易的将新的算法实现加入到已有的系统中,而已有的实现不需要修改。
  • 里氏替换原则:里氏替换原则(LSP)指的是所有引用基类的地方都可以透明的使用其子类的对象。可以理解为:只要有父类出现的地方,都可以使用子类来替代。而且不会出现任何错误或者异常(但是反过来却不行。子类出现的地方,不能使用父类来替代)。策略模式是一个扁平的结构,各个策略实现都是兄弟关系,实现了同一个接口或者继承了同一个抽象类。这样只要使用策略的客户端保持面向抽象编程,就可以动态的切换不同的策略实现以进行替换。

ps:里氏替换原则

     具体约束

  1. 子类必须实现父类的抽象方法,但不得重写父类的非抽象(已实现的)方法。
  2. 子类中可增加自己特有的方法。(可以随时扩展)
  3. 当子类覆盖或者实现父类的方法时,方法的前置条件(方法形参)要比父类输入参数更加宽松。否则会调用到父类的方法。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。否则会调用到父类的方法。

我们最好将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时候,父类不能实例化。


举报

相关推荐

0 条评论