策略模式(Strategy)

1 场景问题

1.1 报价管理

向客户报价,对于销售部门的人来讲,这是一个非常重大、非常复杂的问题,对不同的客户要报不同的价格,比如:

对普通客户或者是新客户报的是全价
对老客户报的价格,根据客户年限,给予一定的折扣
对大客户报的价格,根据大客户的累计消费金额,给予一定的折扣

还要考虑客户购买的数量和金额,比如:虽然是新用户,但是一次购买的数量非常大,或者是总金额非常高,也会有一定的折扣

还有,报价人员的职务高低,也决定了他是否有权限对价格进行一定的浮动折扣

甚至在不同的阶段,对客户的报价也不同,一般情况是刚开始比较高,越接近成交阶段,报价越趋于合理。

总之,向客户报价是非常复杂的,因此在一些CRM(客户关系管理)的系统中,会有一个单独的报价管理模块,来处理复杂的报价功能。
为了演示的简洁性,假定现在需要实现一个简化的报价管理,实现如下的功能:

(1)对普通客户或者是新客户报全价
(2)对老客户报的价格,统一折扣5%
(3)对大客户报的价格,统一折扣10%

该怎么实现呢?

1.2 不用模式的解决方案

要实现对不同的人员报不同的价格的功能,无外乎就是判断起来麻烦点,也不多难,很快就有朋友能写出如下的实现代码,示例代码如下:

/**
 * 价格管理,主要完成计算向客户所报价格的功能
 */
public class Price {
    /**
     * 报价,对不同类型的,计算不同的价格
     * @param goodsPrice 商品销售原价
     * @param customerType 客户类型
     * @return 计算出来的,应该给客户报的价格
     */
    public double quote(double goodsPrice,String customerType){
        if(customerType.equals("普通客户 ")){
            System.out.println("对于新客户或者是普通客户,没有折扣 ");
            return goodsPrice;
        }else if(customerType.equals("老客户 ")){
            System.out.println("对于老客户,统一折扣 5%");
            return goodsPrice*(1-0.05);
        }else if(customerType.equals("大客户 ")){
            System.out.println("对于大客户,统一折扣 10%");
            return goodsPrice*(1-0.1);        
        }
        // 其余人员都是报原价
        return goodsPrice;
    }
}

1.3 有何问题

上面的写法是很简单的,也很容易想,但是仔细想想,这样实现,问题可不小,比如:第一个问题:价格类包含了所有计算报价的算法,使得价格类,尤其是报价这个方法比较庞杂,难以维护。
有朋友可能会想,这很简单嘛,把这些算法从报价方法里面拿出去,形成独立的方法不就可以解决这个问题了吗?据此写出如下的实现代码,示例代码如下:

/**
 * 价格管理,主要完成计算向客户所报价格的功能
 */
public class Price {
    /**
     * 报价,对不同类型的,计算不同的价格
     * @param goodsPrice 商品销售原价
     * @param customerType 客户类型
     * @return 计算出来的,应该给客户报的价格
     */
    public double quote(double goodsPrice,String customerType){
        if(customerType.equals("普通客户 ")){
            return this.calcPriceForNormal(goodsPrice);
        }else if(customerType.equals("老客户 ")){
            return this.calcPriceForOld(goodsPrice);
        }else if(customerType.equals("大客户 ")){
            return this.calcPriceForLarge(goodsPrice);       
        }
        //其余人员都是报原价
        return goodsPrice;
    }
    /**
     * 为新客户或者是普通客户计算应报的价格
     * @param goodsPrice 商品销售原价
     * @return 计算出来的,应该给客户报的价格
     */
    private double calcPriceForNormal(double goodsPrice){
        System.out.println("对于新客户或者是普通客户,没有折扣 ");
        return goodsPrice;
    }
    /**
     * 为老客户计算应报的价格
     * @param goodsPrice 商品销售原价
     * @return 计算出来的,应该给客户报的价格
     */
    private double calcPriceForOld(double goodsPrice){
        System.out.println("对于老客户,统一折扣 5%");
        return goodsPrice*(1-0.05);
    }
    /**
     * 为大客户计算应报的价格
     * @param goodsPrice 商品销售原价
     * @return 计算出来的,应该给客户报的价格
     */
    private double calcPriceForLarge(double goodsPrice){
        System.out.println("对于大客户,统一折扣 10%");
        return goodsPrice*(1-0.1); 
    }
}

这样看起来,比刚开始稍稍好点,计算报价的方法会稍稍简单一点,这样维护起来也稍好一些,某个算法发生了变化,直接修改相应的私有方法就可以了。扩展起来也容易一点,比如要增加一个“战略合作客户”的类型,报价为直接8折,就只需要在价格类里面新增加一个私有的方法来计算新的价格,然后在计算报价的方法里面新添一个else-if即可。看起来似乎很不错了。

真的很不错了吗?

再想想,问题还是存在,只不过从计算报价的方法挪动到价格类里面了,假如有100个或者更多这样的计算方式,这会让这个价格类非常庞大,难以维护。而且,维护和扩展都需要去修改已有的代码,这是很不好的,违反了开-闭原则。

第二个问题:经常会有这样的需要,在不同的时候,要使用不同的计算方式。

比如:在公司周年庆的时候,所有的客户额外增加3%的折扣;在换季促销的时候,普通客户是额外增加折扣2%,老客户是额外增加折扣3%,大客户是额外增加折扣5%。这意味着计算报价的方式会经常被修改,或者被切换。

通常情况下应该是被切换,因为过了促销时间,又还回到正常的价格体系上来了。而现在的价格类中计算报价的方法,是固定调用各种计算方式,这使得切换调用不同的计算方式很麻烦,每次都需要修改if-else里面的调用代码。

看到这里,可能有朋友会想, 那么到底应该如何实现,才能够让价格类中的计算报价的算法,能很容易的实现可维护、可扩展,又能动态的切换变化呢?

2 解决方案

2.1 策略模式来解决

用来解决上述问题的一个合理的解决方案就是策略模式。那么什么是策略模式呢?
策略模式定义

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

应用策略模式来解决的思路

仔细分析上面的问题,先来把它抽象一下,各种计算报价的计算方式就好比是具体的算法,而使用这些计算方式来计算报价的程序,就相当于是使用算法的客户。

再分析上面的实现方式,为什么会造成那些问题,根本原因,就在于算法和使用算法的客户是耦合的,甚至是密不可分的,在上面实现中,具体的算法和使用算法的客户是同一个类里面的不同方法。
现在要解决那些问题,按照策略模式的方式,应该先把所有的计算方式独立出来,每个计算方式做成一个单独的算法类,从而形成一系列的算法,并且为这一系列算法定义一个公共的接口,这些算法实现是同一接口的不同实现,地位是平等的,可以相互替换。这样一来,要扩展新的算法就变成了增加一个新的算法实现类,要维护某个算法,也只是修改某个具体的算法实现即可,不会对其它代码造成影响。也就是说这样就解决了可维护、可扩展的问题。

为了实现让算法能独立于使用它的客户,策略模式引入了一个上下文的对象,这个对象负责持有算法,但是不负责决定具体选用哪个算法,把选择算法的功能交给了客户,由客户选择好具体的算法后,设置到上下文对象里面,让上下文对象持有客户选择的算法,当客户通知上下文对象执行功能的时候,上下文对象会去转调具体的算法。这样一来,具体的算法和直接使用算法的客户是分离的。

具体的算法和使用它的客户分离过后,使得算法可独立于使用它的客户而变化,并且能够动态的切换需要使用的算法,只要客户端动态的选择使用不同的算法,然后设置到上下文对象中去,实际调用的时候,就可以调用到不同的算法。

2.2 模式结构和说明

策略模式的结构示意图如图所示:


策略模式结构示意图

Strategy:策略接口,用来约束一系列具体的策略算法。Context使用这个接口来调用具体的策略实现定义的算法。
ConcreteStrategy:具体的策略实现,也就是具体的算法实现。
Context:上下文,负责和具体的策略类交互,通常上下文会持有一个真正的策略实现,上下文还可以让具体的策略类来获取上下文的数据,甚至让具体的策略类来回调上下文的方法。

2.3 策略模式示例代码

首先来看策略,也就是定义算法的接口,示例代码如下:

/**
* 策略,定义算法的接口
*/
public interface Strategy {
  /**
   * 某个算法的接口,可以有传入参数,也可以有返回值
   */
  public void algorithmInterface();
}

该来看看具体的算法实现了,定义了三个,分别是ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC,示例非常简单,由于没有具体算法的实现,三者也就是名称不同,示例代码如下:

/**
* 实现具体的算法
*/
public class ConcreteStrategyA implements Strategy {
  public void algorithmInterface() {
      //具体的算法实现   
  }
}

/**
* 实现具体的算法
*/
public class ConcreteStrategyB implements Strategy {
  public void algorithmInterface() {
      //具体的算法实现   
  }
}

/**
* 实现具体的算法
*/
public class ConcreteStrategyC implements Strategy {
  public void algorithmInterface() {
      //具体的算法实现   
  }
}

再来看看上下文的实现,示例代码如下:

/**
* 上下文对象,通常会持有一个具体的策略对象
*/
public class Context {
  /**
   * 持有一个具体的策略对象
   */
  private Strategy strategy;
  /**
   * 构造方法,传入一个具体的策略对象
   * @param aStrategy 具体的策略对象
   */
  public Context(Strategy aStrategy) {
      this.strategy = aStrategy;
  }
  /**
   * 上下文对客户端提供的操作接口,可以有参数和返回值
   */
  public void contextInterface() {
      //通常会转调具体的策略对象进行算法运算
      strategy.algorithmInterface();
  }
}

2.4 使用策略模式重写示例

要使用策略模式来重写前面报价的示例,大致有如下改变:
首先需要定义出算法的接口。
然后把各种报价的计算方式单独出来,形成算法类。

对于Price这个类,把它当做上下文,在计算报价的时候,不再需要判断,直接使用持有的具体算法进行运算即可。选择使用哪一个算法的功能挪出去,放到外部使用的客户端去。
这个时候,程序的结构如图所示:


使用策略模式实现示例的结构示意图

先看策略接口,示例代码如下:

/**
* 策略,定义计算报价算法的接口
*/
public interface Strategy {
  /**
   * 计算应报的价格
   * @param goodsPrice 商品销售原价
   * @return 计算出来的,应该给客户报的价格
   */
  public double calcPrice(double goodsPrice);
}

接下来看看具体的算法实现,不同的算法,实现也不一样,先看为新客户或者是普通客户计算应报的价格的实现,示例代码如下:

/**
* 具体算法实现,为新客户或者是普通客户计算应报的价格
*/
public class NormalCustomerStrategy implements Strategy{
  public double calcPrice(double goodsPrice) {
      System.out.println("对于新客户或者是普通客户,没有折扣");
      return goodsPrice;
  }
}

再看看为老客户计算应报的价格的实现,示例代码如下:

/**
* 具体算法实现,为老客户计算应报的价格
*/
public class OldCustomerStrategy implements Strategy{
  public double calcPrice(double goodsPrice) {
      System.out.println("对于老客户,统一折扣5%");
      return goodsPrice*(1-0.05);
  }
}

再看看为大客户计算应报的价格的实现,示例代码如下:

/**
* 具体算法实现,为大客户计算应报的价格
*/
public class LargeCustomerStrategy implements Strategy{
  public double calcPrice(double goodsPrice) {
      System.out.println("对于大客户,统一折扣10%");
      return goodsPrice*(1-0.1);
  }
}

接下来看看上下文的实现,也就是原来的价格类,它的变化比较大,主要有:

原来那些私有的,用来做不同计算的方法,已经去掉了,独立出去做成了算法类
原来报价方法里面,对具体计算方式的判断,去掉了,让客户端来完成选择具体算法的功能
新添加持有一个具体的算法实现,通过构造方法传入
原来报价方法的实现,变化成了转调具体算法来实现

示例代码如下:

/**
* 价格管理,主要完成计算向客户所报价格的功能
*/
public class Price {
  /**
   * 持有一个具体的策略对象
   */
  private Strategy strategy = null;
  /**
   * 构造方法,传入一个具体的策略对象
   * @param aStrategy 具体的策略对象
   */
  public Price(Strategy aStrategy){
      this.strategy = aStrategy;
  }  
  /**
   * 报价,计算对客户的报价
   * @param goodsPrice 商品销售原价
   * @return 计算出来的,应该给客户报的价格
   */
  public double quote(double goodsPrice){
      return this.strategy.calcPrice(goodsPrice);
  }
}

写个客户端来测试运行一下,好加深体会,示例代码如下:

public class Client {
  public static void main(String[] args) {
      //1:选择并创建需要使用的策略对象
      Strategy strategy = new LargeCustomerStrategy ();
      //2:创建上下文
      Price ctx = new Price(strategy);

      //3:计算报价
      double quote = ctx.quote(1000);
      System.out.println("向客户报价:"+quote);
  }
}

3 模式讲解

3.1 认识策略模式

策略模式的功能

策略模式的功能是把具体的算法实现,从具体的业务处理里面独立出来,把它们实现成为单独的算法类,从而形成一系列的算法,并让这些算法可以相互替换。

策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、具有更好的维护性和扩展性。

策略模式和if-else语句

看了前面的示例,很多朋友会发现,每个策略算法具体实现的功能,就是原来在if-else结构中的具体实现。

没错,其实多个if-elseif语句表达的就是一个平等的功能结构,你要么执行if,要不你就执行else,或者是elseif,这个时候,if块里面的实现和else块里面的实现从运行地位上来讲就是平等的。

而策略模式就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文来与具体的策略类进行交互。

因此多个if-else语句可以考虑使用策略模式。

算法的平等性

策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正是因为这个平等性,才能实现算法之间可以相互替换。

所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。
所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现

谁来选择具体的策略算法

在策略模式中,可以在两个地方来进行具体策略的选择。
一个是在客户端,在使用上下文的时候,由客户端来选择具体的策略算法,然后把这个策略算法设置给上下文。前面的示例就是这种情况。

还有一个是客户端不管,由上下文来选择具体的策略算法,这个在后面讲容错恢复的时候给大家演示一下。

Strategy的实现方式

在前面的示例中,Strategy都是使用的接口来定义的,这也是常见的实现方式。但是如果多个算法具有公共功能的话,可以把Strategy实现成为抽象类,然后把多个算法的公共功能实现到Strategy里面

运行时策略的唯一性

运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态的在不同的策略实现中切换,但是同时只能使用一个。

增加新的策略

在前面的示例里面,体会到了策略模式中切换算法的方便,但是增加一个新的算法会怎样呢?比如现在要实现如下的功能:对于公司的“战略合作客户”,统一8折。

其实很简单,策略模式可以让你很灵活的扩展新的算法。具体的做法是:先写一个策略算法类来实现新的要求,然后在客户端使用的时候指定使用新的策略算法类就可以了。

还是通过示例来说明。先添加一个实现要求的策略类,示例代码如下:

/**
* 具体算法实现,为战略合作客户客户计算应报的价格
*/
public class CooperateCustomerStrategy implements Strategy{
  public double calcPrice(double goodsPrice) {
      System.out.println("对于战略合作客户,统一8折");
      return goodsPrice*0.8;
  }
}

然后在客户端指定使用策略的时候指定新的策略算法实现,示例如下:

public class Client2 {
  public static void main(String[] args) {
      //1:选择并创建需要使用的策略对象
      Strategy strategy = new CooperateCustomerStrategy ();
      //2:创建上下文
      Price ctx = new Price(strategy);

      //3:计算报价
      double quote = ctx.quote(1000);
      System.out.println("向客户报价:"+quote);
  }
}

策略模式调用顺序示意图

策略模式的调用顺序,有两种常见的情况,一种如同前面的示例,具体如下:

先是客户端来选择并创建具体的策略对象
然后客户端创建上下文
接下来客户端就可以调用上下文的方法来执行功能了,在调用的时候,从客户端传入算法需要的参数
上下文接到客户的调用请求,会把这个请求转发给它持有的Strategy

这种情况的调用顺序示意图如图所示:


策略模式调用顺序示意图一

策略模式调用还有一种情况,就是把Context当做参数来传递给Strategy
,这种方式的调用顺序图,在讲具体的Context和Strategy的关系时再给出。

3.2 容错恢复机制

容错恢复机制是应用程序开发中非常常见的功能。那么什么是容错恢复呢?简单点说就是:程序运行的时候,正常情况下应该按照某种方式来做,如果按照某种方式来做发生错误的话,系统并不会崩溃,也不会就此不能继续向下运行了,而是有容忍出错的能力,不但能容忍程序运行出现错误,还提供出现错误后的备用方案,也就是恢复机制,来代替正常执行的功能,使程序继续向下运行。

举个实际点的例子吧,比如在一个系统中,所有对系统的操作都要有日志记录,而且这个日志还需要有管理界面,这种情况下通常会把日志记录在数据库里面,方便后续的管理,但是在记录日志到数据库的时候,可能会发生错误,比如暂时连不上数据库了,那就先记录在文件里面,然后在合适的时候把文件中的记录再转录到数据库中。

对于这样的功能的设计,就可以采用策略模式,把日志记录到数据库和日志记录到文件当作两种记录日志的策略,然后在运行期间根据需要进行动态的切换。

在这个例子的实现中,要示范由上下文来选择具体的策略算法,前面的例子都是由客户端选择好具体的算法,然后设置到上下文中

先定义日志策略接口,很简单,就是一个记录日志的方法,示例代码如下:

/**
* 日志记录策略的接口
*/
public interface LogStrategy {
  /**
   * 记录日志
   * @param msg 需记录的日志信息
   */
  public void log(String msg);
}

实现日志策略接口,先实现默认的数据库实现,假设如果日志的长度超过长度就出错,制造错误的是一个最常见的运行期错误,示例代码如下:

/**
* 把日志记录到数据库
*/
public class DbLog implements LogStrategy{
  public void log(String msg) {     
      //制造错误
      if(msg!=null && msg.trim().length()>5){
          int a = 5/0;
      }
      System.out.println("现在把 '"+msg+"' 记录到数据库中");
  }
}

接下来实现记录日志到文件中去,示例代码如下:

/**
* 把日志记录到文件
*/
public class FileLog implements LogStrategy{
  public void log(String msg) {
      System.out.println("现在把 '"+msg+"' 记录到文件中");
  }
}

接下来定义使用这些策略的上下文,注意这次是在上下文里面实现具体策略算法的选择,所以不需要客户端来指定具体的策略算法了,示例代码如下:

看看现在的客户端,没有了选择具体实现策略算法的工作,变得非常简单,故意多调用一次,可以看出不同的效果,示例代码如下:

Paste_Image.png

小结一下,通过上面的示例,会看到策略模式的一种简单应用,也顺便了解一下基本的容错恢复机制的设计和实现。在实际的应用中,需要设计容错恢复的系统一般要求都比较高,应用也会比较复杂,但是基本的思路是差不多的。

3.3 Context和Strategy的关系

在策略模式中,通常是上下文使用具体的策略实现对象,反过来,策略实现对象也可以从上下文获取所需要的数据,因此可以将上下文当参数传递给策略实现对象,这种情况下上下文和策略实现对象是紧密耦合的。

在这种情况下,上下文封装着具体策略对象进行算法运算所需要的数据,具体策略对象通过回调上下文的方法来获取这些数据。

甚至在某些情况下,策略实现对象还可以回调上下文的方法来实现一定的功能,这种使用场景下,上下文变相充当了多个策略算法实现的公共接口,在上下文定义的方法可以当做是所有或者是部分策略算法使用的公共功能。

但是请注意,由于所有的策略实现对象都实现同一个策略接口,传入同一个上下文,可能会造成传入的上下文数据的浪费,因为有的算法会使用这些数据,而有的算法不会使用,但是上下文和策略对象之间交互的开销是存在的了。

还是通过例子来说明。

工资支付的实现思路

考虑这样一个功能:工资支付方式的问题,很多企业的工资支付方式是很灵活的,可支付方式是比较多的,比如:人民币现金支付、美元现金支付、银行转账到工资帐户、银行转账到工资卡;一些创业型的企业为了留住骨干员工,还可能有:工资转股权等等方式。总之一句话,工资支付方式很多。

随着公司的发展,会不断有新的工资支付方式出现,这就要求能方便的扩展;另外工资支付方式不是固定的,是由公司和员工协商确定的,也就是说可能不同的员工采用的是不同的支付方式,甚至同一个员工,不同时间采用的支付方式也可能会不同,这就要求能很方便的切换具体的支付方式。
要实现这样的功能,策略模式是一个很好的选择。在实现这个功能的时候,不同的策略算法需要的数据是不一样,比如:现金支付就不需要银行帐号,而银行转账就需要帐号。这就导致在设计策略接口中的方法时,不太好确定参数的个数,而且,就算现在把所有的参数都列上了,今后扩展呢?难道再来修改策略接口吗?如果这样做,那无异于一场灾难,加入一个新策略,就需要修改接口,然后修改所有已有的实现,不疯掉才怪!那么到底如何实现,在今后扩展的时候才最方便呢?

解决方案之一,就是把上下文当做参数传递给策略对象,这样一来,如果要扩展新的策略实现,只需要扩展上下文就可以了,已有的实现不需要做任何的修改。

这样是不是能很好的实现功能,并具有很好的扩展性呢?还是通过代码示例来具体的看。假设先实现人民币现金支付和美元现金支付这两种支付方式,然后就进行使用测试,然后再来添加银行转账到工资卡的支付方式,看看是不是能很容易的与已有的实现结合上。

实现代码示例
(1)先定义工资支付的策略接口,就是定义一个支付工资的方法,示例代码如下:

/**
* 支付工资的策略的接口,公司有多种支付工资的算法
* 比如:现金、银行卡、现金加股票、现金加期权、美元支付等等
*/
public interface PaymentStrategy {
  /**
   * 公司给某人真正支付工资
   * @param ctx 支付工资的上下文,里面包含算法需要的数据
   */
  public void pay(PaymentContext ctx);
}

(2)定义好了工资支付的策略接口,该来考虑如何实现这多种支付策略了。
为了演示的简单,这里先简单实现人民币现金支付和美元现金支付方式,当然并不真的去实现跟银行的交互,只是示意一下。
人民币现金支付的策略实现,示例代码如下:

/**
* 人民币现金支付
*/
public class RMBCash implements PaymentStrategy{
  public void pay(PaymentContext ctx) {
      System.out.println("现在给"+ctx.getUserName()+"人民币现金支付"+ctx.getMoney()+"元");
  }
}

同样的实现美元现金支付的策略,示例代码如下:

/**
* 美元现金支付
*/
public class DollarCash implements PaymentStrategy{
  public void pay(PaymentContext ctx) {
      System.out.println("现在给"+ctx.getUserName()+"美元现金支付"+ctx.getMoney()+"元");
  }
}

(3)该来看支付上下文的实现了,当然这个使用支付策略的上下文,是需要知道具体使用哪一个支付策略的,一般由客户端来确定具体使用哪一个具体的策略,然后上下文负责去真正执行。因此,这个上下文需要持有一个支付策略,而且是由客户端来配置它。示例代码如下:

/**
* 支付工资的上下文,每个人的工资不同,支付方式也不同
*/
public class PaymentContext {
  /**
   * 应被支付工资的人员,简单点,用姓名来代替
   */
  private String userName = null;
  /**
   * 应被支付的工资的金额
   */
  private double money = 0.0;
  /**
   * 支付工资的方式策略的接口
   */
  private PaymentStrategy strategy = null;
  /**
   * 构造方法,传入被支付工资的人员,应支付的金额和具体的支付策略
   * @param userName 被支付工资的人员
   * @param money 应支付的金额
   * @param strategy 具体的支付策略
   */
  public PaymentContext(String userName,double money,PaymentStrategy strategy){
      this.userName = userName;
      this.money = money;
      this.strategy = strategy;
  }
  public String getUserName() {
      return userName;
  }
  public double getMoney() {
      return money;
  }
  /**
   * 立即支付工资
   */
  public void payNow(){
      //使用客户希望的支付策略来支付工资
      this.strategy.pay(this);
  }
}

(4)准备好了支付工资的各种策略,下面看看如何使用这些策略来真正支付工资,很简单,客户端是使用上下文来使用具体的策略的,而且是客户端来确定具体的策略,就是客户端创建哪个策略,最终就运行哪一个策略,各个策略之间是可以动态切换的,示例代码如下:

public class Client {
  public static void main(String[] args) {
      //创建相应的支付策略
      PaymentStrategy strategyRMB = new RMBCash();
      PaymentStrategy strategyDollar = new DollarCash();

      //准备小李的支付工资上下文
      PaymentContext ctx1 = new PaymentContext("小李",5000,strategyRMB);
      //向小李支付工资
      ctx1.payNow();

      //切换一个人,给petter支付工资
      PaymentContext ctx2 = new PaymentContext("Petter",8000,strategyDollar);
      ctx2.payNow();
  }
}

扩展示例,实现方式一
经过上面的测试可以看出,通过使用策略模式,已经实现好了两种支付方式了。如果现在要增加一种支付方式,要求能支付到银行卡,该怎么扩展最简单呢?
应该新增加一种支付到银行卡的策略实现,然后通过继承来扩展支付上下文,在里面添加新的支付方式需要的新的数据,比如银行卡账户,然后在客户端使用新的上下文和新的策略实现就可以了,这样已有的实现都不需要改变,完全遵循开-闭原则。
先看看扩展的支付上下文对象的实现,示例代码如下:

/**
* 扩展的支付上下文对象
*/
public class PaymentContext2 extends PaymentContext {
  /**
   * 银行帐号
   */
  private String account = null;
  /**
   * 构造方法,传入被支付工资的人员,应支付的金额和具体的支付策略
   * @param userName 被支付工资的人员
   * @param money 应支付的金额
   * @param account 支付到的银行帐号
   * @param strategy 具体的支付策略
   */
  public PaymentContext2(String userName,double money,String account,PaymentStrategy strategy){
      super(userName,money,strategy);
      this.account = account;
  }
  public String getAccount() {
      return account;
  }
}

然后看看新的策略算法的实现,示例代码如下:

/**
* 支付到银行卡
*/
public class Card implements PaymentStrategy{
  public void pay(PaymentContext ctx) {
      // 这个新的算法自己知道要使用扩展的支付上下文,所以强制造型一下
      PaymentContext2 ctx2 = (PaymentContext2)ctx;
      System.out.println(" 现在给 "+ctx2.getUserName()+" 的 "+ctx2.getAccount()+" 帐号支付了 "+ctx2.getMoney()+" 元 ");
      // 连接银行,进行转帐,就不去管了
  }
}

最后看看客户端怎么使用这个新的策略呢?原有的代码不变,直接添加新的测试就可以了,示例代码如下:

public class Client {
  public static void main(String[] args) {
      //创建相应的支付策略
      PaymentStrategy strategyRMB = new RMBCash();
      PaymentStrategy strategyDollar = new DollarCash();

      //准备小李的支付工资上下文
      PaymentContext ctx1 = new PaymentContext("小李 ",5000,strategyRMB);
      //向小李支付工资
      ctx1.payNow();

      //切换一个人,给 petter支付工资
      PaymentContext ctx2 = new PaymentContext("Petter",8000,strategyDollar);
      ctx2.payNow();

      // 测试新添加的支付方式
      PaymentStrategy strategyCard = new Card();
      PaymentContext ctx3 = new PaymentContext2("小王",9000,"010998877656",strategyCard);
      ctx3.payNow();
  }
}

扩展示例,实现方式二

(1)上面这种实现方式,是通过扩展上下文对象来准备新的算法需要的数据。还有另外一种方式,那就是通过策略的构造方法来传入新算法需要的数据。这样实现的话,就不需要扩展上下文了,直接添加新的策略算法实现就好了。示例代码如下:

/**
* 支付到银行卡
*/
public class Card2 implements PaymentStrategy{
  /**
   * 帐号信息
   */
  private String account = "";
  /**
   * 构造方法,传入帐号信息
   * @param account 帐号信息
   */
  public Card2(String account){
      this.account = account;
  }
  public void pay(PaymentContext ctx) {
      System.out.println(" 现在给 "+ctx.getUserName()+" 的 "+this.account+" 帐号支付了 "+ctx.getMoney()+" 元 ");
      // 连接银行,进行转帐,就不去管了
  }
}

(2)直接在客户端测试就可以了,测试示例代码如下:

public class Client {
  public static void main(String[] args) {
      //测试新添加的支付方式
      PaymentStrategy strategyCard2 = new Card2("010998877656");
      PaymentContext ctx4 = new PaymentContext("小张",9000,strategyCard2);
      ctx4.payNow();
  }
}

(3)现在有这么两种扩展的实现方式,到底使用哪一种呢?或者是哪种实现更好呢?下面来比较一下:
对于扩展上下文的方式:
这样实现,所有策略的实现风格更统一,策略需要的数据都统一从上下文来获取,这样在使用方法上也很统一;另外,在上下文中添加新的数据,别的相应算法也可以用得上,可以视为公共的数据。但缺点也很明显,如果这些数据只有一个特定的算法来使用,那么这些数据有些浪费;另外每次添加新的算法都去扩展上下文,容易形成复杂的上下文对象层次,也未见得有必要。
对于在策略算法的实现上添加自己需要的数据的方式:
这样实现,比较好想,实现简单。但是缺点也很明显,跟其它策略实现的风格不一致,其它策略都是从上下文中来获取数据,而这个策略的实现一部分数据来自上下文,一部分数据来自自己,有些不统一;另外,这样一来,外部使用这些策略算法的时候也不一样了,不太好以一个统一的方式来动态切换策略算法。
两种实现各有优劣,至于如何选择,那就具体问题,具体的分析了。

另一种策略模式调用顺序示意图
策略模式调用还有一种情况,就是把Context当做参数来传递给Strategy,也就是本例示范的这种方式,这个时候策略模式的调用顺序如图所示:

策略模式调用顺序示意图二

3.4 策略模式结合模板方法模式
在实际应用策略模式的过程中,经常会出现这样一种情况,就是发现这一系列算法的实现上存在公共功能,甚至这一系列算法的实现步骤都是一样的,只是在某些局部步骤上有所不同
,这个时候,就需要对策略模式进行些许的变化使用了。
对于一系列算法的实现上存在公共功能的情况,策略模式可以有如下三种实现方式:
一个是在上下文当中实现公共功能,让所有具体的策略算法回调这些方法。
另外一种情况就是把策略的接口改成抽象类,然后在里面实现具体算法的公共功能。
还有一种情况是给所有的策略算法定义一个抽象的父类,让这个父类去实现策略的接口,然后在这个父类里面去实现公共的功能。

更进一步,如果这个时候发现“一系列算法的实现步骤都是一样的,只是在某些局部步骤上有所不同”的情况,那就可以在这个抽象类里面定义算法实现的骨架,然后让具体的策略算法去实现变化的部分。这样的一个结构自然就变成了策略模式来结合模板方法模式了,那个抽象类就成了模板方法模式的模板类

我们讨论过模板方法模式来结合策略模式的方式,也就是主要的结构是模板方法模式,局部采用策略模式
。而这里讨论的是策略模式来结合模板方法模式,也就是主要的结构是策略模式,局部实现上采用模板方法模式
。通过这个示例也可以看出来,模式之间的结合是没有定势的,要具体问题具体分析。
此时策略模式结合模板方法模式的系统结构如下图所示:


策略模式结合模板方法模式的结构示意图

还是用实际的例子来说吧,比如上面那个记录日志的例子,如果现在需要在所有的消息前面都添加上日志时间,也就是说现在记录日志的步骤变成了:第一步为日志消息添加日志时间;第二步具体记录日志。
那么该怎么实现呢?
记录日志的策略接口没有变化,为了看起来方便,还是示例一下,示例代码如下:

/**
* 日志记录策略的接口
*/
public interface LogStrategy {
  /**
   * 记录日志
   * @param msg 需记录的日志信息
   */
  public void log(String msg);
}

增加一个实现这个策略接口的抽象类,在里面定义记录日志的算法骨架,相当于模板方法模式的模板,示例代码如下:

/**
* 实现日志策略的抽象模板,实现给消息添加时间
*/
public abstract class LogStrategyTemplate implements LogStrategy {
  public final void log(String msg) {
      //第一步:给消息添加记录日志的时间
      DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
      msg = df.format(new java.util.Date())+" 内容是:"+ msg;
      //第二步:真正执行日志记录
      doLog(msg);
  }
  /**
   * 真正执行日志记录,让子类去具体实现
   * @param msg 需记录的日志信息
   */                                 
  protected abstract void doLog(String msg);
}

这个时候那两个具体的日志算法实现也需要做些改变,不再直接实现策略接口了,而是继承模板,实现模板方法了。这个时候记录日志到数据库的类,示例代码如下:

/**
* 把日志记录到数据库
*/
public class DbLog extends LogStrategyTemplate{
  //除了定义上发生了改变外,具体的实现没变
  public void doLog(String msg) {   
      //制造错误
      if(msg!=null && msg.trim().length()>5){
          int a = 5/0;
      }
      System.out.println("现在把 '"+msg+"' 记录到数据库中");
  }
}

同理实现记录日志到文件的类如下:

/**
* 把日志记录到数据库
*/
public class FileLog extends LogStrategyTemplate{
  public void doLog(String msg) {
      System.out.println("现在把 '"+msg+"' 记录到文件中");
  }
}

算法实现的改变不影响使用算法的上下文,上下文跟前面一样,示例代码如下:

/**
* 日志记录的上下文
*/
public class LogContext {
  /**
   * 记录日志的方法,提供给客户端使用
   * @param msg 需记录的日志信息
   */
  public void log(String msg){
      //在上下文里面,自行实现对具体策略的选择
      //优先选用策略:记录到数据库
      LogStrategy strategy = new DbLog();
      try{
          strategy.log(msg);
      }catch(Exception err){
          //出错了,那就记录到文件中
          strategy = new FileLog();
          strategy.log(msg);
      }
  }  
}

客户端跟以前也一样,示例代码如下:

public class Client {
  public static void main(String[] args) {
      LogContext log = new LogContext();
      log.log("记录日志");
      log.log("再次记录日志");
  }
}

3.5 策略模式的优缺点

定义一系列算法

策略模式的功能就是定义一系列算法,实现让这些算法可以相互替换。所以会为这一系列算法定义公共的接口,以约束一系列算法要实现的功能。如果这一系列算法具有公共功能,可以把策略接口实现成为抽象类,把这些公共功能实现到父类里面,对于这个问题,前面讲了三种处理方法,这里就不罗嗦了。

避免多重条件语句

根据前面的示例会发现,策略模式的一系列策略算法是平等的,可以互换的,写在一起就是通过if-else结构来组织,如果此时具体的算法实现里面又有条件语句,就构成了多重条件语句,使用策略模式能避免这样的多重条件语句。

更好的扩展性

在策略模式中扩展新的策略实现非常容易,只要增加新的策略实现类,然后在选择使用策略的地方选择使用这个新的策略实现就好了。

客户必须了解每种策略的不同

策略模式也有缺点,比如让客户端来选择具体使用哪一个策略,这就可能会让客户需要了解所有的策略,还要了解各种策略的功能和不同,这样才能做出正确的选择,而且这样也暴露了策略的具体实现。

增加了对象数目

由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。

只适合扁平的算法结构

策略模式的一系列算法地位是平等的,是可以相互替换的,事实上构成了一个扁平的算法结构,也就是在一个策略接口下,有多个平等的策略算法,就相当于兄弟算法。而且在运行时刻只有一个算法被使用,这就限制了算法使用的层级,使用的时候不能嵌套使用。
对于出现需要嵌套使用多个算法的情况,比如折上折、折后返卷等业务的实现,需要组合或者是嵌套使用多个算法的情况,可以考虑使用装饰模式、或是变形的职责链、或是AOP等方式来实现

3.6 思考策略模式

策略模式的本质

策略模式的本质:分离算法,选择实现。

仔细思考策略模式的结构和实现的功能,会发现,如果没有上下文,策略模式就回到了最基本的接口和实现了,只要是面向接口编程的,那么就能够享受到接口的封装隔离带来的好处。也就是通过一个统一的策略接口来封装和隔离具体的策略算法,面向接口编程的话,自然不需要关心具体的策略实现,也可以通过使用不同的实现类来实例化接口,从而实现切换具体的策略。

看起来好像没有上下文什么事情,但是如果没有上下文,那么就需要客户端来直接与具体的策略交互,尤其是当需要提供一些公共功能,或者是相关状态存储的时候,会大大增加客户端使用的难度。因此,引入上下文还是很必要的,有了上下文,这些工作就由上下文来完成了,客户端只需要与上下文交互就可以了,这样会让整个设计模式更独立、更有整体性,也让客户端更简单。

但纵观整个策略模式实现的功能和设计,它的本质还是“分离算法,选择实现”,因为分离并封装了算法,才能够很容易的修改和添加算法;也能很容易的动态切换使用不同的算法,也就是动态选择一个算法来实现需要的功能了。

对设计原则的体现

从设计原则上来看,策略模式很好的体现了开-闭原则。策略模式通过把一系列可变的算法进行封装,并定义出合理的使用结构,使得在系统出现新算法的时候,能很容易的把新的算法加入到已有的系统中,而已有的实现不需要做任何修改。这在前面的示例中已经体现出来了,好好体会一下。

从设计原则上来看,策略模式还很好的体现了里氏替换原则。策略模式是一个扁平结构,一系列的实现算法其实是兄弟关系,都是实现同一个接口或者继承的同一个父类。这样只要使用策略的客户保持面向抽象类型编程,就能够使用不同的策略的具体实现对象来配置它,从而实现一系列算法可以相互替换。

何时选用策略模式

建议在如下情况中,选用策略模式:

出现有许多相关的类,仅仅是行为有差别的情况,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法动态切换
出现同一个算法,有很多不同的实现的情况,可以使用策略模式来把这些“不同的实现”实现成为一个算法的类层次

需要封装算法中,与算法相关的数据的情况,可以使用策略模式来避免暴露这些跟算法相关的数据结构

出现抽象一个定义了很多行为的类,并且是通过多个if-else语句来选择这些行为的情况,可以使用策略模式来代替这些条件语句

3.7 相关模式

策略模式和状态模式

这两个模式从模式结构上看是一样的,但是实现的功能是不一样的。
状态模式是根据状态的变化来选择相应的行为,不同的状态对应不同的类,每个状态对应的类实现了该状态对应的功能,在实现功能的同时,还会维护状态数据的变化。这些实现状态对应的功能的类之间是不能相互替换的。

策略模式是根据需要或者是客户端的要求来选择相应的实现类,各个实现类是平等的,是可以相互替换的。

另外策略模式可以让客户端来选择需要使用的策略算法,而状态模式一般是由上下文,或者是在状态实现类里面来维护具体的状态数据,通常不由客户端来指定状态。

策略模式和模板方法模式

这两个模式可组合使用,如同前面示例的那样。
模板方法重在封装算法骨架,而策略模式重在分离并封装算法实现。

策略模式和享元模式

这两个模式可组合使用。

策略模式分离并封装出一系列的策略算法对象,这些对象的功能通常都比较单一,很多时候就是为了实现某个算法的功能而存在,因此,针对这一系列的、多个细粒度的对象,可以应用享元模式来节省资源,但前提是这些算法对象要被频繁的使用,如果偶尔用一次,就没有必要做成享元了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容