策略模式
既然是详解,就不以案例开头了,直奔主题,先来看看什么是策略模式。
模式定义
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式 使得算法可独立于使用它的客户而变化。
结构
Strategy(策略接口):
- 用来约束一系列具体的策略算法。
- 定义了算法的公共接口,使得算法可以互换使用。
ConcreteStrategy(具体策略实现):
- 具体的算法实现,继承自策略接口。
- 每个具体策略类实现了策略接口中定义的算法
Context(上下文):
- 负责和具体的策略类交互。
- 通常上下文会持有一个真正的策略实现。
- 上下文可以让具体的策略类来获取上下文的数据。
- 甚至可以让具体的策略类来回调上下文的方法。
样例代码:
java
// 策略接口
interface Strategy {
void algorithmInterface();
}
// 具体策略A
class ConcreteStrategyA implements Strategy {
@Override
public void algorithmInterface() {
System.out.println("执行策略A的算法实现");
}
}
// 具体策略B
class ConcreteStrategyB implements Strategy {
@Override
public void algorithmInterface() {
System.out.println("执行策略B的算法实现");
}
}
// 具体策略C
class ConcreteStrategyC implements Strategy {
@Override
public void algorithmInterface() {
System.out.println("执行策略C的算法实现");
}
}
// 上下文类
class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public void contextInterface() {
strategy.algorithmInterface();
}
}
// 客户端代码
public class StrategyPatternDemo {
public static void main(String[] args) {
Context context = new Context(new ConcreteStrategyA());
context.contextInterface();
context.setStrategy(new ConcreteStrategyB());
context.contextInterface();
context.setStrategy(new ConcreteStrategyC());
context.contextInterface();
}
}
策略模式实现案例
我们以CRM中的报价场景为例,来看一下策略模式的使用,简化一下场景,会有如下的报价方案:
- 对普通客户或者是新客户报全价:
- 对老客户报的价格,统一折扣5%;
- 对大客户报的价格,统一折扣10%。
根据策略模式的思路,我们大致要做如下内容:
- 首先需要定义出算法的接口。
- 然后把各种报价的计算方式单独出来,形成算法类。
- 对于Price类,把它当做上下文,在计算报价的时候,不再需要判断,直接使 用持有的具体算法进行运算即可。具体选择使用哪一个算法的功能挪出去,放到外部使 用的客户端去。
策略模式形成的类图如下:
基于SpringBoot的项目实现
java
// 策略接口
public interface Strategy {
double calcPrice(double goodsPrice);
}
// 普通客户策略
@Commpont
public class NormalCustomerStrategy implements Strategy {
@Override
public double calcPrice(double goodsPrice) {
// 普通客户不打折
return goodsPrice;
}
}
// 老客户策略
@Commpont
public class OldCustomerStrategy implements Strategy {
@Override
public double calcPrice(double goodsPrice) {
// 老客户享受5%的折扣
return goodsPrice * 0.95;
}
}
// 大客户策略
@Commpont
public class LargeCustomerStrategy implements Strategy {
@Override
public double calcPrice(double goodsPrice) {
// 大客户享受10%的折扣
return goodsPrice * 0.90;
}
}
// 报价上下文类
public class Price {
private Strategy strategy;
// 构造函数,初始化策略
public Price(Strategy strategy) {
this.strategy = strategy;
}
// 设置策略
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
// 计算并返回报价
public double quote(double goodsPrice) {
return strategy.calcPrice(goodsPrice);
}
}
public enum ClientType{
//正常客户
normal,
//老客户
old,
//大客户
large;
}
@Service
public class ClientServiceImpl implement ClientService{
@Autowired
private ApplicationContext context;
//......
public ResData createQutePrice(String clientId){
ResData result = new ResData();
Client client = ClientMapper.findById(clientId);
//......客户其他认证逻辑
Double nowPrice = XXMapper.queryNowPrice();
result.setPrice(getPriceUtil(client.getClientType()).qute(nowPrice ));
return result;
}
private Price getPriceUtil(ClientType type){
Strategy strategy = null;
switch(type){
case old:
strategy = (Strategy)context.getBean(OldCustomerStrategy.class);
break;
case large:
strategy = (Strategy)context.getBean(LargeCustomerStrategy.class);
break;
default:
strategy = (Strategy)context.getBean(NormalCustomerStrategy.class);
break;
}
Price price = new Price(strategy);
return price;
}
}
使用该写法,虽然相比if-else来讲要更为麻烦,但是随着报价场景的增加,我们仅仅通过新增实现类和调整getPriceUtil方法即可完成扩展。
策略模式详解
策略模式的本质
策略模式的本质是:
分离算法,选择实现
策略模式的功能是把具体的算法实现从具体的业务处理中独立出来,把它们实现成 为单独的算法类,从而形成一系列的算法,并让这些算法可以相互替换。 策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。
策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法, 大家的地位是完全一样的,正是因为这个平等性,才能实现算法之间可以相互替换。 所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。 所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。
从设计原则角度
- 策略模式很好地体现了开一闭原则。策略模式通过把一系列可变的算法进行封装,并定义出合理的使用结构,使得在系统出现新算法的时候,能够很容易地把新的算法加入到已有的系统中,而已有的实现不需要做任何修改。这在前面的示例中已经体现出来了,好好体会一下。
- 策略模式还很好地体现了里氏替换原则。策略模式是一个扁平结构,一系列的实现算法其实是兄弟关系,都是实现同一个接口或者继承的同一个父类。这样只要使用策略的客户保持面向抽象类型编程,就能够使用不同策略的具体实现对象来配置它,从而实现一系列算法可以相互替换。
策略模式与If-else
看了前面的示例,很多朋友会发现,每个策略算法具体实现的功能,就是原来在f-else 结构中的具体实现。 没错,其实多个if-elseif语句表达的就是一个平等的功能结构,你要么执行if,要么 执行else,或者是elseif,这个时候,if块中的实现和else块中的实现从运行地位上来讲是平等的。 而策略模式就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下 文来与具体的策略类进行交互。所以其实很多地方都在讲策略模式能消除遍地if-else是不准确的,if-else并没有消失,只是代码形式改变了,但是使用策略模式替换if-else的优势是不可否认的,将原本if-else内的逻辑抽离出来,可以达到修改具体内容而不破坏外层框架的目的。因此多个f-else语句可以考虑使用策略模式。
Strategy的扩展
在前面的示例中,Strategy都是使用接口来定义的,这也是常见的实现方式。但是如 果多个算法具有公共功能的话,可以把Strategy实现成为抽象类,然后把多个算法的公 共功能实现到Strategy中。
java
// 抽象策略类,包含公共功能
public abstract class Strategy {
// 公共方法,所有策略类都会用到
public void commonMethod() {
System.out.println("这是一个公共方法");
}
// 抽象方法,具体的策略算法实现
public abstract void algorithmInterface();
}
// 具体策略A
public class ConcreteStrategyA extends Strategy {
@Override
public void algorithmInterface() {
System.out.println("执行策略A的算法实现");
}
}
// 具体策略B
public class ConcreteStrategyB extends Strategy {
@Override
public void algorithmInterface() {
System.out.println("执行策略B的算法实现");
}
}
// 具体策略C
public class ConcreteStrategyC extends Strategy {
@Override
public void algorithmInterface() {
System.out.println("执行策略C的算法实现");
}
}
Context与Strategy的关系
在策略模式中,通常是上下文(Context)使用具体的策略实现对象。反过来,策略实现对象也 可以从上下文获取所需要的数据。因此可以将上下文当作参数传递给策略实现对象,这 种情况下上下文和策略实现对象是紧密耦合的。 在这种情况下,上下文封装看具体策略对象进行算法运算所需要的数据,具体策略 对象通过回调上下文的方法来获取这些数据。 甚至在某些情况下,策略实现对象还可以回调上下文的方法来实现一定的功能,这种使用场景下,上下文变相充当了多个策略算法实现的公共接口。在上下文定义的方法 可以当作是所有或者是部分策略算法使用的公共功能。
但是需要注意,由于所有的策略实现对象都实现同一个策略接口,传入同一个上 下文,可能会造成传入的上下文数据的浪费,因为有的算法会使用这些数据, 而有的算法不会使用,但是上下文和策略对象之间交互的开销是存在的。
以一个公司结算的场景案例来说:
很多企业的工资支付方式是很灵活的,可支付方式是比较多的,比如,人民币现金支付、美元现金支付、银行转账到工资账户、银行转账到工资卡;一些创业型的企业为了留住骨干员工,还可能有工资转股权等方式。总之一句话,工资支付方式很多。
随着公司的发展,会不断有新的工资支付方式出现,这就要求能方便地扩展;另外工资支付方式不是固定的,是由公司和员工协商确定的,也就是说可能不同的员工采用的是不同的支付方式,甚至同一个员工,不同时间采用的支付方式也可能会不同,这就要求能很方便地切换具体的支付方式。
要实现这样的功能,显然策略模式是一个很好的选择。在实现这个功能的时候,不同的策略算法需要的数据是不一样,比如,现金支付就不需要银行账号,而银行转账就需要账号。
这就导致在设计策略接口中的方法时,不太好确定参数的个数,而且,就算现在把所有的参数都列上了,扩展性难以保障,加入一个新策略,就需要修改接口。我们根据上面对于Context与Strategy的关系分析,基于将上下文当作参数传递给策略对象 的方式以策略模式实现上述需求**。**
先定义工资支付的策略接口,也就是定义一个支付工资的方法:
java
/**
* 支付工资的策略接口,公司有多种支付工资的算法
* 比如,现金、银行卡、现金加股票、现金加期权、美元支付等
*/
public interface PaymentStrategy {
/**
* 公司给某人真正支付工资
* @param ctx 支付工资的上下文,里面包含算法需要的数据
*/
public void pay(PaymentContext ctx);
}
这里先简单实现人民币现金支付和美元现金支付方式,当然并不是真地去实现跟银行的交互,只是示意一下:
java
/**
* 人民币现金支付
*/
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() + "元");
}
}
下面是上下文的实现以及使用:
java
/**
* 支付工资的上下文,每个人的工资不同,支付方式也不同
*/
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;
}
// 只有getter方法,让策略算法在实现的时候,根据需要来获取上下文中的数据
public String getUserName() {
return userName;
}
public double getMoney() {
return money;
}
/**
* 立即支付工资
*/
public void payNow() {
// 使用客户希望的支付策略来支付工资
this.strategy.pay(this);
}
}
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();
}
}
基本的策略模式框架已经搭建完成,如果接下来需要增加一种支付方式,要求能支付到银行,基于以上代码,扩展方式有两种。
扩展方式一:通过扩展上下文对象(Context)来准备新的算法需要的数据
java
/**
* 扩展的支付上下文对象
*/
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();
PaymentContext2 ctx3 = new PaymentContext2("小王", 9000, "010998877656", strategyCard);
ctx3.payNow();
}
}
通过代码实现,可以看出,这种扩展方式是新增加一种支付到银行卡的策略实现,然后通过继承来扩展支付上下文,其中添加新的支付方式需要的新数据,比如银行卡账户,并在客户端使便用新的上下文和新的策略实现就可以了,这样已有的实现都不需要改变,完全遵循开一闭原则。
扩展方式二:通过策略的构造方法传入新算法所需数据
java
/**
* 支付到银行卡
*/
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() + "元");
// 连接银行,进行转账,就不去管了
}
}
// 直接在客户端测试就可以了。示例代码如下:
public class Client {
public static void main(String[] args) {
// 测试新添加的支付方式
PaymentStrategy strategyCard2 = new Card2("010998877656");
PaymentContext ctx4 = new PaymentContext("小", 9000, strategyCard2);
ctx4.payNow();
}
}
-
对于扩展上下文的方式:
- 优点:所有策略的实现风格更统一,策略需要的数据都统一从上下文来获取,这样在使用方法上也很统一;在上下文中添加新的数据,别的相应算法也可以用得上,可以视为公共的数据。
- 缺点:如果这些数据只有一个特定的算法来使用,那么这些数据有些浪费;每次添加新的算法都去扩展上下文,容易形成复杂的上下文对象层次,也未见得有必要。
-
对于在策略算法的实现上添加自己需要的数据的方式:
- 优点:实现起来简单,容易理解。
- 缺点:实现风格与其他策略不一致,其他策略都是从上下文中来获取数据,而这个策略的实现一部分数据来自上下文,一部分数据来自自己,有些不统一;外部使用这些策略算法的时候也不一,难于以一个统一的方式来动态切换策略算法。
适用策略模式的场景
-
当出现有许多相关的类,仅仅是行为有差别的情况下,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法动态切换。
-
当出现同一个算法,有很多不同实现的情况下,可以使用策略模式来把这些"不同的实现"实现成为一个算法的类层次。
-
当需要封装算法中,有与算法相关数据的情况下,可以使用策略模式来避免暴露这些跟算法相关的数据结构。
-
当出现抽象一个定义了很多行为的类,并且是通过多个f-else语句来选择这些行为的情况下,可以使用策略模式来代替这些条件语句。