策略模式
既然是详解,就不以案例开头了,直奔主题,先来看看什么是策略模式。
模式定义
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式 使得算法可独立于使用它的客户而变化。
结构
Strategy(策略接口):
- 用来约束一系列具体的策略算法。
- 定义了算法的公共接口,使得算法可以互换使用。
ConcreteStrategy(具体策略实现):
- 具体的算法实现,继承自策略接口。
- 每个具体策略类实现了策略接口中定义的算法
Context(上下文):
- 负责和具体的策略类交互。
- 通常上下文会持有一个真正的策略实现。
- 上下文可以让具体的策略类来获取上下文的数据。
- 甚至可以让具体的策略类来回调上下文的方法。
样例代码:
// 策略接口
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的项目实现
// 策略接口
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中。
// 抽象策略类,包含公共功能
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的关系分析,基于将上下文当作参数传递给策略对象的方式以策略模式实现上述需求。
先定义工资支付的策略接口,也就是定义一个支付工资的方法:
/**
* 支付工资的策略接口,公司有多种支付工资的算法
* 比如,现金、银行卡、现金加股票、现金加期权、美元支付等
*/
public interface PaymentStrategy {
/**
* 公司给某人真正支付工资
* @param ctx 支付工资的上下文,里面包含算法需要的数据
*/
public void pay(PaymentContext ctx);
}
这里先简单实现人民币现金支付和美元现金支付方式,当然并不是真地去实现跟银行的交互,只是示意一下:
/**
* 人民币现金支付
*/
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() + "元");
}
}
下面是上下文的实现以及使用:
/**
* 支付工资的上下文,每个人的工资不同,支付方式也不同
*/
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)来准备新的算法需要的数据
/**
* 扩展的支付上下文对象
*/
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();
}
}
通过代码实现,可以看出,这种扩展方式是新增加一种支付到银行卡的策略实现,然后通过继承来扩展支付上下文,其中添加新的支付方式需要的新数据,比如银行卡账户,并在客户端使便用新的上下文和新的策略实现就可以了,这样已有的实现都不需要改变,完全遵循开一闭原则。
扩展方式二:通过策略的构造方法传入新算法所需数据
/**
* 支付到银行卡
*/
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语句来选择这些行为的情况下,可以使用策略模式来代替这些条件语句。