设计模式:详细拆解策略模式

策略模式

既然是详解,就不以案例开头了,直奔主题,先来看看什么是策略模式。

模式定义

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

结构

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%。

根据策略模式的思路,我们大致要做如下内容:

  1. 首先需要定义出算法的接口。
  2. 然后把各种报价的计算方式单独出来,形成算法类。
  3. 对于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();
    }
}
  • 对于扩展上下文的方式:

    • 优点:所有策略的实现风格更统一,策略需要的数据都统一从上下文来获取,这样在使用方法上也很统一;在上下文中添加新的数据,别的相应算法也可以用得上,可以视为公共的数据。
    • 缺点:如果这些数据只有一个特定的算法来使用,那么这些数据有些浪费;每次添加新的算法都去扩展上下文,容易形成复杂的上下文对象层次,也未见得有必要。
  • 对于在策略算法的实现上添加自己需要的数据的方式:

    • 优点:实现起来简单,容易理解。
    • 缺点:实现风格与其他策略不一致,其他策略都是从上下文中来获取数据,而这个策略的实现一部分数据来自上下文,一部分数据来自自己,有些不统一;外部使用这些策略算法的时候也不一,难于以一个统一的方式来动态切换策略算法。

适用策略模式的场景

  1. 当出现有许多相关的类,仅仅是行为有差别的情况下,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法动态切换。

  2. 当出现同一个算法,有很多不同实现的情况下,可以使用策略模式来把这些"不同的实现"实现成为一个算法的类层次。

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

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

相关推荐
lxyzcm11 小时前
深入理解C++23的Deducing this特性(上):基础概念与语法详解
开发语言·c++·spring boot·设计模式·c++23
越甲八千11 小时前
重温设计模式--单例模式
单例模式·设计模式
Vincent(朱志强)12 小时前
设计模式详解(十二):单例模式——Singleton
android·单例模式·设计模式
诸葛悠闲13 小时前
设计模式——桥接模式
设计模式·桥接模式
捕鲸叉17 小时前
C++软件设计模式之外观(Facade)模式
c++·设计模式·外观模式
小小小妮子~18 小时前
框架专题:设计模式
设计模式·框架
先睡18 小时前
MySQL的架构设计和设计模式
数据库·mysql·设计模式
waicsdn_haha18 小时前
Postman最新详细安装及使用教程【附安装包】
测试工具·api·压力测试·postman·策略模式·get·delete
Damon_X1 天前
桥接模式(Bridge Pattern)
设计模式·桥接模式
越甲八千1 天前
重温设计模式--享元模式
设计模式·享元模式