设计模式学习之策略模式

策略模式(Strategy Pattern)我觉得在23中常见的设计模式里面是一种比较简单的,使用场景也很多。在策略模式中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象,策略对象改变 context 对象的执行算法。

介绍

意图:

定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

主要解决:

在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。

何时使用:

一个系统有许多许多类,而区分它们的只是他们直接的行为。

如何解决:

将这些算法封装成一个一个的类,任意地替换。

关键代码:

需要实现同一个接口。

应用实例:
  • 诸葛亮的锦囊妙计,每一个锦囊就是一个策略。
  • 旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。
  • JAVA AWT 中的 LayoutManager。
优点:
  • 算法可以自由切换。
  • 避免使用多重条件判断。
  • 扩展性良好。
缺点:
  • 策略类会增多。
  • 所有策略类都需要对外暴露(这个也不一定)。
使用场景:
  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  • 一个系统需要动态地在几种算法中选择一种。
  • 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

注意: 如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。

业务场景

以抽奖活动发奖为例,奖品多种多样,可能是现金奖,话费奖品,实物奖等等,每种奖品的发放方式都不一样,比如现金是直接转账,话费奖品是调用运营商提供接口发放,实物奖需要人工快递寄送。在未采用策略模式之前,少不了使用 if…else…来判断发放,当增加一种奖品类型时,就需要增加 if 判断。而采取策略模式之后,只需实现一个策略类即可,对原来的逻辑无需做任何改动,也不会影响其他策略的正常逻辑。

UML图

策略模式UML图

代码实现

客户端需要判断要使用哪一个具体的策略类,若还是按照传统的方法 if…else…来判断策略模式就没有意义了,因此策略模式一般都是结合其他模式共同使用。本文中策略类使用 Byte 来标识,也可以在策略类中增加抽象方法,返回值为枚举类型。

  1. 定义策略接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    package com.ydstudio.flashsale.service;

    /**
    * 策略接口
    *
    * @author 刘洋 Sam
    * @date 2020/10/24
    * @since 1.0.0
    */
    public interface RewardSendStrategy {
    /**
    * 发放奖励的类型,通过这个方法来标示不同的策略
    * @return
    */
    Byte type();

    /**
    * 方法奖励的方法
    */
    void sendReward();
    }
  2. 发送现金奖励策略实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    package com.ydstudio.flashsale.service.impl;

    import com.ydstudio.flashsale.common.enums.RewardTypeEnum;
    import com.ydstudio.flashsale.service.RewardSendStrategy;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;

    /**
    * 发送现金奖品策略
    *
    * @author 刘洋 Sam
    * @date 2020/10/24
    * @since 1.0.0
    */
    @Slf4j
    @Service
    public class CashRewardSendStrategy implements RewardSendStrategy {
    @Override
    public Byte type() {
    return RewardTypeEnum.CASH.getCode();
    }

    @Override
    public void sendReward() {
    System.out.println("发放现金奖品");
    }
    }
  3. 发放积分奖品策略实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    package com.ydstudio.flashsale.service.impl;

    import com.ydstudio.flashsale.common.enums.RewardTypeEnum;
    import com.ydstudio.flashsale.service.RewardSendStrategy;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;

    /**
    * 发放积分奖品策略
    *
    * @author 刘洋 Sam
    * @date 2020/10/24
    * @since 1.0.0
    */
    @Slf4j
    @Service
    public class PointRewardSendStrategy implements RewardSendStrategy {
    @Override
    public Byte type() {
    return RewardTypeEnum.POINT.getCode();
    }

    @Override
    public void sendReward() {
    System.out.println("发放积分奖品");
    }
    }
  4. 发放优惠券奖品策略实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
        
    package com.ydstudio.flashsale.service.impl;

    import com.ydstudio.flashsale.common.enums.RewardTypeEnum;
    import com.ydstudio.flashsale.service.RewardSendStrategy;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;

    /**
    * 发送优惠券策略
    *
    * @author 刘洋 Sam
    * @date 2020/10/24
    * @since 1.0.0
    */
    @Slf4j
    @Service
    public class CouponRewardSendStrategy implements RewardSendStrategy {
    @Override
    public Byte type() {
    return RewardTypeEnum.COUPON.getCode();
    }

    @Override
    public void sendReward() {
    System.out.println("发放优惠券奖品");
    }
    }
  5. 发放谢谢参与策略实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    package com.ydstudio.flashsale.service.impl;

    import com.ydstudio.flashsale.common.enums.RewardTypeEnum;
    import com.ydstudio.flashsale.service.RewardSendStrategy;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;

    /**
    * 谢谢参与奖品发放策略
    *
    * @author 刘洋 Sam
    * @date 2020/10/24
    * @since 1.0.0
    */
    @Slf4j
    @Service
    public class ThankYouRewardSendStrategy implements RewardSendStrategy {
    @Override
    public Byte type() {
    return RewardTypeEnum.THANK_YOU.getCode();
    }

    @Override
    public void sendReward() {
    System.out.println("谢谢参与");
    }
    }
  1. 实现对抽象策略封装的上下文对象

    通常 JavaWeb 开发使用 Spring 框架的比较多,在这里我们用Spring 提供的@Autowired注解的一个小技巧,来更方便的实现封装。不用再使用 java RewardSendStrategyContext 去实现 java org.springframework.context.ApplicationContextAware 或者每个策略类再实现 java org.springframework.beans.factory接口然后将策略类添加到 RewardSendStrategyContext 中的

    属性中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45

    package com.ydstudio.flashsale.service;

    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.stereotype.Component;

    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;

    /**
    *
    * @author 刘洋 Sam
    * @date 2020/10/25
    * @since 1.0.0
    */
    @Component
    public class RewardSendStrategyContext implements ApplicationContextAware {

    private final static Map<Byte, RewardSendStrategy> strategyMap = new ConcurrentHashMap<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

    Map<String, RewardSendStrategy> beans = applicationContext.getBeansOfType(RewardSendStrategy.class);
    beans.values().forEach(bean -> strategyMap.put(bean.type(), bean));
    }

    /**
    * 通过 type 获取奖励实现
    *
    * @param type
    * @return
    */
    public static RewardSendStrategy getRewardSendStrategy(Byte type) {

    RewardSendStrategy sendStrategy = strategyMap.get(type);
    if (sendStrategy == null) {
    throw new UnsupportedOperationException("没有此种奖励的发放实现");
    }

    return sendStrategy;
    }
    }

    @Autowried 小技巧,看下这个注解的上的注释,我下面来通过举例子说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /*
    * <p>In case of a {@link java.util.Collection} or {@link java.util.Map}
    * dependency type, the container will autowire all beans matching the
    * declared value type. In case of a Map, the keys must be declared as
    * type String and will be resolved to the corresponding bean names.
    *
    */
    @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Autowired {

    /**
    * Declares whether the annotated dependency is required.
    * <p>Defaults to {@code true}.
    */
    boolean required() default true;

    }

    举例说明:

    1
    2
    3
    4
    5
    6
    7
    # 这样定义属性的时候,Spring 会自动的将这个接口的实现类bean全都自动添加到这个 rewardSendStrategyList 中
    @Autowired
    private List<RewardSendStrategy> rewardSendStrategyList;

    # 这样定义属性的时候,Spring 会自动的以实现类 beanName 作为 key,bean 作为 value 添加到这个 Map 中
    @Autowired
    private Map<String, RewardSendStrategy> strategyMap;

    所以策略类上下文对象应该这样去编码实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    package com.ydstudio.flashsale.service;

    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.util.List;
    import java.util.concurrent.ConcurrentMap;
    import java.util.stream.Collectors;

    /**
    * 发送奖励的 Context 类
    *
    * @author 刘洋 Sam
    * @date 2020/10/25
    * @since 1.0.0
    */
    @Slf4j
    @Service
    public class RewardSendStrategyContext {

    @Autowired
    private List<RewardSendStrategy> rewardSendStrategyList;

    /**
    * 通过 type 获取奖励实现
    *
    * @param type
    * @return
    */
    public RewardSendStrategy getRewardSendStrategy(Byte type) {

    if (CollectionUtils.isEmpty(rewardSendStrategyList)) {
    throw new RuntimeException("RewardSendStrategy 注入失败");
    }
    ConcurrentMap<Byte, RewardSendStrategy> strategyMap = rewardSendStrategyList.stream()
    .collect(Collectors.toConcurrentMap(RewardSendStrategy::type, RewardSendStrategy -> RewardSendStrategy));

    RewardSendStrategy sendStrategy = strategyMap.get(type);
    if (sendStrategy == null) {
    throw new UnsupportedOperationException("没有此种奖励的发放实现");
    }

    return sendStrategy;
    }
    }

    这样写是不是很优雅!!!那调用方如何使用呢?很简单的直接注入就能使用了,哈哈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    @Autowired
    private RewardSendStrategyContext strategyContext;

    public void test(){
    Byte aByte = new Byte("1");

    RewardSendStrategy rewardSendStrategy = strategyContext.getRewardSendStrategy(aByte);
    log.info("奖励发放实现 {}",sendStrategy);
    }

总结

到此策略模式的概念、结构和代码实现都说完了,是不是很简单!大家在撸码的过程中,如果遇到类似的场景可以使用策略模式的时候,一定要尝试去使用策略模式去优化自己代码,这样既锻炼了自己,也写出了牛逼的代码何乐而不为呢?难道你一直想当 CURD boy 么????

  • 作者: Sam
  • 发布时间: 2020-10-24 11:58:02
  • 最后更新: 2020-12-06 22:04:33
  • 文章链接: https://ydstudios.gitee.io/post/170b3d6a.html
  • 版权声明: 本网所有文章除特别声明外, 禁止未经授权转载,违者依法追究相关法律责任!