设计模式-策略模式

31次阅读

共计 8003 个字符,预计需要花费 21 分钟才能阅读完成。

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

/**
 * 订单类,拥有一个支付方法
 *
 * @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-…

https://www.cnblogs.com/lewis…

正文完
 0

设计模式策略模式

31次阅读

共计 2525 个字符,预计需要花费 7 分钟才能阅读完成。

《Head First 设计模式》笔记整理 … 欢迎交流 …

定义

定义了算法簇,分别封装起来,让他们之间可以 相互替换,此模式让算法的变化独立于使用算法的客户。

这里稍微提醒一下,策略模式是我们学习的第一个模式,理解它还是非常重要的。这个模式跟后面讲到的 模板模式 有些相似的地方,我在学习的时候,看到模板模式已经忘记什么是策略模式了,看的一脸懵逼。不过没关系,后面我会针对这两个进行整理,这里要记住的是,策略模式封装了一组可以 互换 的算法簇,是 针对接口编程 的。

OO 原则

  • 封装变化
  • 多用组合,少用继承
  • 针对接口编程,而不是针对实现编程

封装变化

为什么要封装变化?先来看下面的例子。

dock 类是所有鸭子的超类,其中,飞行(fly)行为和呱呱叫(quack)行为会随着鸭子的不同而改变。如果我们不区分变化,将所有的行为定义在超类里。如图:

显而易见的,这种设计会导致

  • 改变牵一发而动全身
  • 不利于复用,不利于维护
  • 代码在多个子类中重复
  • 很难知道鸭子的全部行为
  • 运行时代码不容易改变

为了便于以后可以轻易地改动和扩展此部分,而不会影响不需要改变的其它部分,我们把变化的部分取出并封装起来。

使用继承如何?设计一个 flyable 接口和一个 quackable 接口。

哇,一堆重复的代码,而且代码难以复用。甚至在会飞的鸭子中,飞行的动作也可能是千变万化的。如果我们想要更加弹性一点,在运行时也可以动态地选择行为,该怎么办呢?

针对接口编程

针对接口编程,而不是针对实现编程。这里所谓的“接口”有多个含义,接口可以是一个“概念”。”针对接口变成“,关键就在于 “多态”,可以更明确地说, 变量的声明类型应该是超类型。这也意味着,声明类是不必理会以后执行时的真正的对象类型。

如此,鸭子的行为被放在分开的类中,此类专门提供行为的接口实现。
这样,鸭子类就不再需要知道行为的实现细节。

多用组合,少用继承

当你讲两个类结合起来使用,就是组合。如图本列,将飞行行为和呱呱叫行为委托给 FlyBehavior 和 QuackBehavior 代为处理。组合建立系统有很大的弹性,不仅可将算法簇封装成类,更可以在运行时动态地改变行为,只要组合的行为对象符合正确的接口即可。

代码演示

publick abstract class Duck {
    // 为行为接口类型声明两个引用变量,所有鸭子子类都继承它们
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    public Duck() {}
    
    publick abstract void display();
    
    public void performFly() {flyBehavior.fly(); // 委托行为给类
    }
    
    public void performQuack() {quackBehavior.quack(); // 委托行为给类
    }
    
    public viod  setFlyBehavior(FlyBehavior fb) {flyBehavior = fb; // 动态设定飞行行为}
    
    public viod  setQuackBehavior(QuackBehavior qb) {quackBehavior = qb; // 动态设定呱呱叫行为}
}

-----------------------------------------------------------

public interface FlyBehavior {public void fly();
}

public class FlyWithWings implements FlyBehavior {public viod fly() {System.out.printIn("I'm flying!");
    }
}

public class FlyNoWay implements FlyBehavior {
    public void fly {System.out.printIn("I can't fly!");
    }
}

-----------------------------------------------------------

public class QuackBehavior {public void quack();
}

public class Quack implements QuackBehavior {public void quack() {// 此处省略}
}

public class MuteQuack implements QuackBehavior {public void quack() {// 此处省略}
}

public class Squack implements QuackBehavior {public void quack() {// 此处省略}
}
// 制造一个新的鸭子类型:模型鸭

public class ModelDuck extends Duck {public ModelDuck() {flyBehavior = new FlyNoWay();
        quackBehavior = new Quack();}
    
    public viod display () {System.out.printIn("I'm a model duck");
    }
    
}

--------------------------------------------------------
// 新建一个新的 FlyBehavior 类型

public class FlyRocketPowered implements FlyBehavior{public viod fly() {System.out.printIn("I'm flying with a rocket!");
    }
}

最后,来看看一个简单的测试

Duck model = new ModelDuck();
model.performFly(); // i can't fly!
model.setFlyBehavior(new FlyRocketPowered()); // 动态改变行为
model.performFly(); // I'm flying with a rocket!

如果我们不再把鸭子的行为看做“一组行为”,而是想成是“一簇算法”,将这些算法抽离并封装起来,委托给对象,通过组合实现运行时算法互换,这就是策略模式。

好了,你觉得我的整理如何呢?如果有什么理解不对的地方,或者你有更好的想法欢迎交流。

正文完
 0