设计模式 - 策略模式

谁都会走 提交于 2019-12-03 22:35:42

在理解策略模式之前我们假设有这样一个需求场景:我们在写订单支付场景的代码时,客户可以选择多种支付方式,有银联支付、支付宝支付、微信支付、京东白条等等。然后我们就很可能就会编写出类似下面这样的代码:

/**
 * 订单类,拥有一个支付方法
 *
 * @author eamon.zhang
 * @date 2019-11-06 上午9:18
 */
public class Order {
    // 订单id
    private String orderId;
    // 支付方式
    private String payType;
    // 订单金额
    private long amount;

    public Order(String orderId, String payType, long amount) {
        this.orderId = orderId;
        this.payType = payType;
        this.amount = amount;
    }

    /**
     * 订单支付方法
     *
     * @return
     */
    public boolean pay() {
        // 是否支付成功
        boolean payment = false;
        if ("aliPay".equals(payType)) {
            System.out.println("用户选择 支付宝 支付,订单号为:" + orderId + " ,支付金额:" + amount);
            payment = true;
        } else if ("unionPay".equals(payType)) {
            System.out.println("用户选择 银联 支付,订单号为:" + orderId + " ,支付金额:" + amount);
            payment = true;
        } else if ("jdPay".equals(payType)) {
            System.out.println("用户选择 京东 支付,订单号为:" + orderId + " ,支付金额:" + amount);
            payment = true;
        } else if ("wechatPay".equals(payType)) {
            System.out.println("用户选择 微信 支付,订单号为:" + orderId + " ,支付金额:" + amount);
            payment = true;
        }

        return payment;
    }

}

客户端:

@Test
public void test() {
    String orderId = "123";
    String payType = "aliPay";
    long amount = 200;
    // 创建订单
    Order order = new Order(orderId, payType, amount);
    // 支付
    order.pay();
}

结果:

用户选择 支付宝 支付,订单号为:123 ,支付金额:200

可以看出这段代码在逻辑上没有问题,也能够很好的运行;

但是存在一个问题:将所有的支付方式都写在同一个方法里面,显得有点臃肿,还带来了一个扩展的问题,如果我们再增加一种支付方式,我们就不得不修改这段代码,再增加一条 if...else,这就降低了代码的可维护性。违背了开闭原则

那能否有一种方法能让我们既不修改主要逻辑代码的前提下让代码变得更优雅也能很好的对其进行扩展呢?那不防我们一起来看看今天的主角:策略模式

定义

策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化,也就是在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。

结构

策略模式中一般会涉及到三个角色:

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

类图结构:

简单代码实现

先创建抽象策略接口 IStrategy

/**
 * 策略类抽象接口,具体策略实现由其子类来实现
 *
 * @author EamonZzz
 * @date 2019-11-02 16:12
 */
public interface IStrategy {

    /**
     * 定义的抽象算法方法 来约束具体的算法实现方法
     */
    void algorithmMethod();
}

创建具体的策略实现类 ConcreteStrategyA

/**
 * 策略具体实现类A
 *
 * @author EamonZzz
 * @date 2019-11-02 16:48
 */
public class ConcreteStrategyA implements IStrategy {

    /**
     * 具体的算法体现
     */
    @Override
    public void algorithmMethod() {
        System.out.println("this is ConcreteStrategyA method...");
    }
}

创建具体的策略实现类 ConcreteStrategyB:

/**
 * 策略具体实现类B
 *
 * @author EamonZzz
 * @date 2019-11-02 16:48
 */
public class ConcreteStrategyB implements IStrategy {

    /**
     * 具体的算法体现
     */
    @Override
    public void algorithmMethod() {
        System.out.println("this is ConcreteStrategyB method...");
    }
}

创建具体的策略实现类 ConcreteStrategyC:

/**
 * 策略具体实现类C
 *
 * @author EamonZzz
 * @date 2019-11-02 16:48
 */
public class ConcreteStrategyC implements IStrategy {

    /**
     * 具体的算法体现
     */
    @Override
    public void algorithmMethod() {
        System.out.println("this is ConcreteStrategyC method...");
    }
}

创建策略上下文 StrategyContext

/**
 * 策策略上下文,负责和具体的策略实现交互,通常策略上下文对象会持有一个真正的策略实现对象,
 * 策略上下文还可以让具体的策略实现从其中获取相关数据,回调策略上下文对象的方法。
 *
 * @author EamonZzz
 * @date 2019-11-02 16:11
 */
public class StrategyContext {
    /**
     * 策略实现的引用
     */
    private IStrategy strategy;

    /**
     * 构造器注入具体的策略类
     *
     * @param iStrategy 策略实现的引用
     */
    public StrategyContext(IStrategy iStrategy) {
        this.strategy = iStrategy;
    }

    public void contextMethod() {
        // 调用策略实现的方法
        strategy.algorithmMethod();
    }
}

最后编写测试类来测试一下结果

/**
 * @author EamonZzz
 * @date 2019-11-02 16:53
 */
public class StrategyContextTest {

    @Test
    public void test(){
        // 1. 创建具体的策略实现
        IStrategy strategy = new ConcreteStrategyA();
        // 2. 在创建策略上下文的同时,将具体的策略实现对象注入到策略上下文当中
        StrategyContext ctx = new StrategyContext(strategy);
        // 3. 调用上下文对象的方法来完成对具体策略实现的回调
        ctx.contextMethod();
    }

}

控制台输出:

this is ConcreteStrategyA method...

改造

在简单的了解了策略模式之后,再看看文章开头的实例场景,我们使用策略模式来对其进行改造:

我们将订单支付逻辑中的各种支付场景算法单独抽离出来:

先创建抽象的支付接口 Payment ,让各种平台的支付逻辑类都实现该接口,达到统一调用的目的:

/**
 * 统一支付接口
 *
 * @author eamon.zhang
 * @date 2019-11-06 上午9:44
 */
public interface Payment {
    boolean pay(String orderId, long amount);
}

然后分别创建支付宝支付类 AliPay

/**
 * 支付宝支付
 *
 * @author eamon.zhang
 * @date 2019-11-06 上午9:48
 */
public class AliPay implements Payment {
    @Override
    public boolean pay(String orderId, long amount) {
        System.out.println("用户选择 支付宝 支付,订单号为:" + orderId + " ,支付金额:" + amount);
        return true;
    }
}

创建微信支付类 WeChatPay

/**
 * 微信支付
 *
 * @author eamon.zhang
 * @date 2019-11-06 上午9:49
 */
public class WeChatPay implements Payment {
    @Override
    public boolean pay(String orderId, long amount) {
        System.out.println("用户选择 微信 支付,订单号为:" + orderId + " ,支付金额:" + amount);
        return true;
    }
}

创建银联支付类 UnionPay

/**
 * 银联支付
 *
 * @author eamon.zhang
 * @date 2019-11-06 上午9:50
 */
public class UnionPay implements Payment {
    @Override
    public boolean pay(String orderId, long amount) {
        System.out.println("用户选择 银联 支付,订单号为:" + orderId + " ,支付金额:" + amount);
        return true;
    }
}

然后创建订单类 Order :

/**
 * 订单类,相当于 策略上下文角色
 *
 * @author eamon.zhang
 * @date 2019-11-06 上午9:43
 */
public class Order {
    // 订单id
    private String orderId;
    // 金额
    private long amount;
    // 具体支付类型的引用
    private Payment payType;

    public Order(String orderId, Payment payType, long amount) {
        this.orderId = orderId;
        this.payType = payType;
        this.amount = amount;
    }

    /**
     * 订单支付方法
     *
     * @return
     */
    public boolean pay() {
        boolean paySuccess;
        // 调用支付接口
        paySuccess = payType.pay(orderId, amount);

        if (!paySuccess) {
            // 支付失败逻辑
            System.out.println("支付失败!");
        }
        return paySuccess;
    }
}

最后创建我们的客户端模拟调用:

@Test
public void test() {
    // 创建支付策略
    Payment weChatPay = new WeChatPay();

    // 创建策略上下文(订单),并将具体的策略实现注入
    String orderId = "123456";
    long amount = 150;
    Order order = new Order(orderId, weChatPay, amount);

    // 调用具体支付策略逻辑
    order.pay();
}

运行结果:

用户选择 微信 支付,订单号为:123456 ,支付金额:150

这样就对订单支付场景完成了一个基本的改造,订单支付的选择权直接在用户选择支付方式时创建,订单支付方法中统一进行调用;当我们需要新增加一种支付方式时,就可以直接继承 Payment 抽象支付策略接口,然后实现支付方法,比如我们现在增加了一种京东白条支付 JdPay 就可以这样写:

/**
 * 京东支付
 *
 * @author eamon.zhang
 * @date 2019-11-06 上午9:49
 */
public class JdPay implements Payment {
    @Override
    public boolean pay(String orderId, long amount) {
        System.out.println("用户选择 京东 支付,订单号为:" + orderId + " ,支付金额:" + amount);
        return true;
    }
}

同样的在客户端调用:

/**
 * @author eamon.zhang
 * @date 2019-11-06 上午10:00
 */
public class OrderTest {

    @Test
    public void test() {
        // 创建支付策略
        Payment jdPay = new JdPay();

        // 创建策略上下文(订单),并将具体的策略实现注入
        String orderId = "123456";
        long amount = 150;
        Order order = new Order(orderId, jdPay, amount);

        // 调用具体支付策略逻辑
        order.pay();
    }

}

运行结果:

用户选择 京东 支付,订单号为:123456 ,支付金额:150

可以看到,在经过使用 策略模式 改造之后,我们的订单支付的扩展变得非常的容易,增加支付方式时,直接创建一个类并实现支付逻辑即可,不需要再修改我们的主类 Order。这就遵循了 开闭原则

再改造

上面第一次改造,只能勉强说明 策略模式 给实际业务带来的好处,但是回到我们现实的支付场景中,用户选择支付方式并且支付的操作都是在前端页面进行的,而且现在大都使用 前后端分离 的模式来进行开发,并不能像 JSP 那样,可以在页面中创建 Java 对象,在前后端分离的场景中,所有参数都是从页面构建好键值对传入,其基本类型为数字、字符串等等。所以我们可以再结合之前说的 工厂模式 进行改造,使其更适合现实场景。

创建支付策略的工厂类 PayStrategyFactory

/**
 * 创建支付策略的工厂类
 *
 * @author eamon.zhang
 * @date 2019-11-06 上午10:32
 */
public class PayStrategyFactory {

    // 支付方式常量
    public static final String ALI_PAY = "aliPay";
    public static final String JD_PAY = "jdPay";
    public static final String WECHAT_PAY = "wechatPay";
    public static final String UNION_PAY = "unionPay";

    // 支付方式管理集合
    private static Map<String, Payment> PAYMENT_STRATEGY_MAP = new HashMap<>();

    static {
        PAYMENT_STRATEGY_MAP.put(ALI_PAY, new AliPay());
        PAYMENT_STRATEGY_MAP.put(JD_PAY, new JdPay());
        PAYMENT_STRATEGY_MAP.put(WECHAT_PAY, new WeChatPay());
        PAYMENT_STRATEGY_MAP.put(UNION_PAY, new UnionPay());
    }

    /**
     * 获取支付方式类
     *
     * @param payType 前端传入支付方式
     * @return
     */
    public static Payment getPayment(String payType) {
        Payment payment = PAYMENT_STRATEGY_MAP.get(payType);
        if (payment == null) {
            throw new NullPointerException("支付方式选择错误!");
        }
        return payment;
    }
}

然后结合实际情况对 Order 类进行修改,使其支付方式的选择权交由用户做支付动作时进行选择,因为提交订单后可以选择不支付,这时候订单可以先创建:

/**
 * 订单类,相当于 策略上下文角色
 *
 * @author eamon.zhang
 * @date 2019-11-06 上午9:43
 */
public class Order {
    // 订单id
    private String orderId;
    // 金额
    private long amount;


    public Order(String orderId, long amount) {
        this.orderId = orderId;
        this.amount = amount;
    }

    /**
     * 订单支付方法
     *
     * @return
     */
    public boolean pay(String payType) {
        boolean paySuccess;
        Payment payment = PayStrategyFactory.getPayment(payType);
        // 调用支付接口
        paySuccess = payment.pay(orderId, amount);

        if (!paySuccess) {
            // 支付失败逻辑
            System.out.println("支付失败!");
        }
        return paySuccess;
    }
}

最后创建测试代码:

@Test
public void test() {
    // 前端传入的参数
    String orderId = "01000005";
    String payType = PayStrategyFactory.ALI_PAY;
    long amount = 190;

    // 创建策略上下文(订单),并将具体的策略实现注入
    Order order = new Order(orderId, amount);
    // 实际情况是 在支付的时候选择支付方式,因为有可能先提交了订单,后面再付款
    order.pay(payType);
}

测试结果:

用户选择 支付宝 支付,订单号为:01000005 ,支付金额:190

这样才算完成了一个比较友好且更贴合实际业务情况的业务代码。当然这只是简单的一个示例,现实中,代码的逻辑会非常复杂;现实中各种设计模式通常我们会配合进行使用,策略模式的使用频率也非常的高,希望大家看完之后能够理解并运用。

总结

应用场景

  1. 假如系统中有很多类,而他们的区别仅仅只是他们之间的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为
  2. 一个系统需要动态地在几种算法中选择一种
  3. 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现

常用来解决的问题

在有多种算法相似的情况下,解决使用 if...else 所带来的复杂和难以维护的问题

在JDK中的体现

在 JDK 源码中也有非常多的 策略模式 的运用,比较常用的就是 Comparator 接口,它有非常多的实现方法,源码中也有很多地方对其进行引用

如果有兴趣,可以使用工具跟进查看一下,这里就不做过多的分析了

优缺点

优点

  1. 策略模式符合开闭原则
  2. 避免了代码中过多的 if...else 和switch 语句的出现
  3. 使用策略模式可以提高算法的保密性和安全性

缺点

  1. 客户端必须知道素有的策略,并决定使用哪一种
  2. 代码中会出现比较多的策略类,增加维护难度

源码

本文源码:https://git.io/JeaYZ

欢迎star


推荐阅读和参考资料:

https://www.cnblogs.com/java-my-life/archive/2012/05/10/2491891.html

https://www.cnblogs.com/lewis0077/p/5133812.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!