# 策略模式

# 设计模式之策略模式+简单工厂模式

相信我们对策略模式都有耳闻,但是可能不知道它在项目中具体能有什么作用,我们需要在什么场景下才能去尽可能得去使用策略模式。

这里我简单的列出一个我之前在公司做的一个需求: 跟第三方oa系统对接接口,对方需要回调我们当前系统,但是是不同的业务接口回调,我们系统可以根据一个字段来区分需要走哪个业务分支,可能初级程序员刚接触这个需求的时候想法是, 多个接口回调,那就写多个接口罢了,强调接口隔离; 或者直接一个接口也行,if...else if 也很不错, 这里呢,为了彰显我们开发人员的逼格,我们可以基于一个接口外加设计模式之策略模式+简单工厂模式。 下面是一个简单的实现demo:

  • 首先是我们定义一个接口即起路由作用,我们具体的不同业务实现类来实现这个接口就可以;
public interface CalculationStrategy {
    /**
     * 策略接口
     */
    int operate(int num1, int num2);
}
@Component("add")
class AddCalculationStrategyImpl implements CalculationStrategy {
    @Override
    public int operate(int num1, int num2) {
        return num1 + num2;
    }
}
@Component("division")
class DivisionStrategyImpl implements CalculationStrategy {
    @Override
    public int operate(int num1, int num2) {
        return num1 / num2;
    }
}
@Component("multiple")
class MultiplicationStrategyImpl implements CalculationStrategy {
    @Override
    public int operate(int num1, int num2) {
        return num1 * num2;
    }
}
@Component("subtract")
class SubtractionStrategyImpl implements CalculationStrategy {

    @Override
    public int operate(int num1, int num2) {
        return num1 - num2;
    }
}

/**
 * 如果Component注解中不写标识会默认加载驼峰类名:testStrategyImpl
 */
@Component
class TestStrategyImpl implements CalculationStrategy {

    @Override
    public int operate(int num1, int num2) {
        return num1 - num2;
    }
}
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

不同的业务分支我就使用加减乘除来代替,也能起到同样的效果。

  • 第二步便是我们的策略上下文,我将其理解为策略工厂,这也是最核心的一个类; 这里我们项目启动的时候,第一步是初始化所有加了@component等类,我们的策略工厂的构造函数中有获取所有实现了路由规则的实现类名称,第二步便是将获取到的实现类名称放入到我们初始化的一个空的map中
@Component
public class CalculationFactory{
    /**
     *  把策略角色(类型)key,和参数value放到Map中
     *  key就是beanName(具体策略实现类中@Component的名字),value就是接口(具体的实现类)
     *  Maps是guava下的封装类型,实则是静态的创建了一个HashMap的对象,Maps可以根据key去获取value对象
     */
    public final Map<String, CalculationStrategy> calculationStrategyMap = Maps.newHashMapWithExpectedSize(4);

    /**
     * 利用构造函数在项目启动的时候将策略实现类注册到 map里
     * @param strategyMap
     */
    public CalculationFactory(Map<String, CalculationStrategy> strategyMap) {
        this.calculationStrategyMap.clear();
        this.calculationStrategyMap.putAll(strategyMap);
    }


    //可以使用@Getter注解代替,这样写方便读者理解在Service层调用Context执行策略
    public Map<String, CalculationStrategy> getCalculationStrategyMap() {
        return calculationStrategyMap;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • 第三步便是我们的路由接口实现,这一步便是具体路由的规则判断了 这里有一步我们需要对代码进行健壮性判断,以防map.get()结果为空导致系统报错,这里大家可以根据业务情况自行去处理。
@Service
public class CalculationService {

    @Autowired
    private CalculationFactory calculationFactory;

    public int operateByStrategy(String strategy, int num1, int num2) {
        // 获取入参,根据不同的参数类型去执行不同的策略,Context的get方法是在这个地方用到的,operate方法就是一开始定义的策略接口
        //calculationFactory.getCalculationStrategyMap().get(strategy)这里可能会出现空,所以要做一个容错处理
        return calculationFactory.getCalculationStrategyMap().get(strategy).operate(num1, num2);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 最后便是我们的测试接口了
@RestController
@RequestMapping("/strategy")
public class TestStrategyController {
    @Autowired
    private CalculationService calculationService;

    @GetMapping("/test/{operation}/{num1}/{num2}")
    public int testCalculation(@PathVariable String operation,@PathVariable  int num1, @PathVariable int num2) {
        // 省略参数判空
        return calculationService.operateByStrategy(operation, num1, num2);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

结果如图所示,大家可自行下去测试,

这样写的好处就是,如果系统还需要扩展其他业务类型的分支处理,那我们只需要将业务处理的实现类实现我们的路由接口,将这个实现类注册进去即可,其他地方都不用改,只需关注我们这个自身的业务分支的逻辑处理。方便了我们系统的后续扩展。

我这也只是一个简单的demo案例,我目前自身也是一个初窥门径的程序员,希望能得到大家更多更优秀的思想来提升自己。

# 策略模式和工厂模式共舞——支付路由设计

策略模式是一种行为型设计模式,它定义了一系列算法,并将每个算法封装到一个类中,使得它们可以相互替换。 这样可以让算法的变化独立于使用算法的客户端代码。策略模式的核心思想是将算法的实现和使用分离开来,从而提高代码的灵活性和可维护性。


>`工厂模式`则是一种创建型设计模式,它提供了一种方式来创建对象,而不需要直接调用构造函数或者其他创建对象的代码。工厂模式隐藏了对象的创建过程,使得客户端代码只关注于如何使用对象,而不需要知道对象的具体创建过程。工厂模式的核心思想是将对象的创建和使用分离开来,从而提高代码的复用性和可扩展性。
现在我们来看一个案例,假设我们正在开发一个电商网站,其中有一个订单类,它有不同的支付方式。我们希望能够让用户在下单时选择不同的支付方式,而且还要支持添加新的支付方式。

# 首先定义一个支付策略接口

package com.example.demo.strategy;

public interface PaymentStrategy {

    /**
     * 获取支付方式
     * @return
     */
    Integer getPayType();

    /**
     * 进行支付
     * @param amount
     */
    void pay(double amount);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 然后创建几个支付策略类

支付方式枚举类:

package com.example.demo.strategy;

public enum PayTypeEnum {
    ALI_PAY(1, "支付宝"),
    WECHAT_PAY(2, "微信支付");

    private final Integer code;
    private final String desc;

    PayTypeEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public Integer getCode() {
        return code;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

支付宝实现类:

package com.example.demo.strategy;

import org.springframework.stereotype.Component;

@Component("aliPay")
public class AliPayStrategy implements PaymentStrategy {
    @Override
    public Integer getPayType() {
        return PayTypeEnum.ALI_PAY.getCode();
    }

    @Override
    public void pay(double amount) {
        // 支付宝支付逻辑
        System.out.println("使用支付宝支付:" + amount + "元");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

微信支付实现类:

package com.example.demo.strategy;

import org.springframework.stereotype.Component;

@Component("weChatPay")
public class WeChatPayStrategy implements PaymentStrategy {


    @Override
    public Integer getPayType() {
        return PayTypeEnum.WECHAT_PAY.getCode();
    }

    @Override
    public void pay(double amount) {
        // 微信支付逻辑
        System.out.println("使用微信支付:" + amount + "元");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

接下来,我们创建一个工厂类来创建支付策略对象:

package com.example.demo.strategy;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class PaymentStrategyFactory implements InitializingBean {
    /**
     * 通过注解直接将所有PaymentStrategy组件注入到list中
     */
    @Autowired
    private List<PaymentStrategy> paymentStrategies;

    Map<Integer, PaymentStrategy> paymentStrategyMap = new HashMap<>();

    /**
     * 构建payType为key的paymentStrategyMap
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        for (PaymentStrategy paymentStrategy : paymentStrategies) {
            paymentStrategyMap.put(paymentStrategy.getPayType(), paymentStrategy);

        }
    }

    /**
     * 获取具体的实现类
     * @param payType
     * @return
     */
    public PaymentStrategy getPaymentStrategy(Integer payType) {
        return paymentStrategyMap.get(payType);
    }
}
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

# 最后在订单服务类将支付策略对象的创建过程交给工厂类完成

package com.example.demo.strategy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderService {
    @Autowired
    private PaymentStrategyFactory paymentStrategyFactory;

    public void payOrder(Integer payType, double amount) {
        /**
         * 获取支付渠道
         */
        PaymentStrategy paymentStrategy = paymentStrategyFactory.getPaymentStrategy(payType);
        paymentStrategy.pay(amount);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

bean配置类:

package com.example.demo.strategy;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.example.demo.strategy")
public class AppConfig {
    @Bean("aliPay")
    public PaymentStrategy aliPayStrategy() {
        return new AliPayStrategy();
    }

    @Bean("weChatPay")
    public PaymentStrategy weChatPayStrategy() {
        return new WeChatPayStrategy();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Main类:

package com.example.demo.strategy;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        OrderService orderService = context.getBean(OrderService.class);

        orderService.payOrder(PayTypeEnum.ALI_PAY.getCode(), 135.1);
        orderService.payOrder(PayTypeEnum.WECHAT_PAY.getCode(), 112.3);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

策略模式+工厂模式的运用,既简单又实用,是生产实践经常使用的模式,我们要对它有深刻的认识,并运用自如。

# 策略模式与SpringBoot的完美融合!

# 策略模式实践

在学习设计模式的过程中,我们常常难以将设计模式融入项目中,而没有实践我们就难以体会设计模式的优势。本文将会展现策略模式在 SpringBoot 项目中的运用方式。

# 策略模式简介及优点

  • Java 常用的策略模式写法
  • Java Stream API、Optional、InitializingBean 的使用
  • unmodifiableMap 避免并发安全问题

# 设计模式简介

「策略模式」会支持定义多个算法,将每个算法都封装起来,并且使它们之间可以相互转换。策略模式让算法独立于使用它的客户而变化。

优点:

  • 把变化的代码从不变的代码中分离出来,使算法可以自由切换
  • 针对接口编程而不是具体类,体现了「开闭原则」
  • 多用组合、聚合,少用继承,符合「合成符用原则」
  • 封装隐藏了复杂的算法代码,使代码清晰易懂

# 设计模式应用——以 IM 消息处理为例

背景

在即时通讯系统(即聊天系统)中,我们会处理各种各样的消息,如文本消息、图片消息、文件消息等。不同的消息有不同的业务逻辑。

不同的业务逻辑理应互不干涉,我们应该使用一种方式将不同的逻辑封装起来,这样会使得代码逻辑更清晰,同时保证不同的消息处理逻辑之间的低耦合。

# 使用策略模式

策略模式就是达成这一目标最简单的方式了!我们可以为不同的消息设计一个策略接口 IMessageHandler,通过接口来保证封装与抽象,而具体的业务逻辑代码封装在策略实现类中,例如用于处理文本消息的 TxtMessageHandler。

public interface IMessageHandler {
    void hanlde(Message msg);
    String supportType();
}

@Component
public class TxtMessageHandler implements IMessageHandler {
    
    @Override
    public void handle(Message msg) {
        // ...
    }
    
    @Override
    public String supportType() {
        // 可以使用枚举或常量代替
        return "txt";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

写好了这些,我们还需要有一个获取策略的地方,因此我们可以设计一个类,该类通过 Spring 构造方法注入获取所有 IMessageHandler 实现类,并通过 Java Stream API 将 List 转为 Map,进而管理所有 IMessageHandler 实现类。

@Component
public class MessageHandlerManager {
    
    private final Map<String, IMessageHandler> messageHandlers;

    // 构造方法注入所有 IMessageHandler
    public MessageHandlerManager(List<IMessageHandler> handlers) {
        // 转为 Map,用 supportType 作为 Key
        Map<String, IMessageHandler> map = handlers.stream()
            .collect(Collectors.toMap(
                IUplinkAdaptor::supportType, 
                Function.identity()
            ));
        // 让 Map 不可变更,避免并发安全问题
        messageHandlers = Collections.unmodifiableMap(map);
    }

    public IMessageHandler getHandler(String messageType) {
        return Optional.ofNullable(messageHandlers.get(messageType))  
            .orElseThrow(() -> new IllegalStateException("不支持的消息类型"));        
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

如此一来,当我们有新消息要处理的时候,就可以简单地通过 MessageHandlerManager 获取策略类了

# 构建静态 HandlerManager

如果我们的 MessageHandlerManager 想做成静态调用,那应该怎么办呢?我们无法使用 Spring 依赖注入了!那么就只好手动将一个个 MessageHandler 注入到 Manager 中。

问题在于:注入的时机是什么?如果在构造方法中注入,会将尚未构造完整(构造方法还没执行完)的对象暴露到其他对象中,导致「对象逸出」。这种做法是不稳妥的。

我们可以采用更好的办法:在类构造完成后注入!MessageHandler 是 SpringBean,可以采用 @PostConstruct 注解在对象构造后执行一些操作。我们可以在这里将对象注入到 Mannger 中。我们需要做如下更改:

public class MessageHandlerManager {
    
    private static final Map<String, IMessageHandler> messageHandlers = new ConcurrentHashMap<>();

    public static void register(String type, IMessageHandler handler) {
        messageHandlers.put(type, handler);
    }
}

@Component
public class TxtMessageHandler implements IMessageHandler {

    @PostConstruct
    public void afterInit() {
        // 不需要 supportType() 方法了
        // 但我们仍需要在注册策略时提供 type 参数
        MessageHandlerManager.register("txt", this);
    }
    
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# InitializingBean 的引入

使用 @PostConstruct 缺乏约束力,IMessageHandler 的实现类可以不写这个方法呀!这样的话程序员可能就会忘记这么做了!程序员最不喜欢的事情莫过于交接老项目了,因为老项目中有各种细节只有当初负责这个项目的人最清楚。

我们希望代码自身能够表达业务意图,例如告诉后面接手项目的程序员:「IMessageHandler 的实现类需要注册到 Manager 中」。而这个场景下,更好地做法是让 IMessageHandler 提供一个约束:所有的实现类都需要提供注册方法,并在对象实例构造后注册!有了 IMessageHandler 的约束(或者说提示),新老程序员就都会知道需要这么一步操作了。

在此处,我们可以使用 InitializingBean 接口,这是 Spring 框架提供的一种回调机制,可以让 Bean 在初始化完成后执行一些自定义的逻辑。实现了该接口的 Bean 会在 Spring 容器中进行初始化后自动调用它定义的 afterPropertiesSet() 方法。与 @PostConsruct 的区别不仅在于它触发的时机不同,还在于它是一个必须重写的接口方法!

public interface IMessageHandler extends InitializingBean {
    void hanlde(Message msg);
}

@Component
public class TxtMessageHandler implements IMessageHandler {
    
    @Override
    public void handle(Message msg) {
        // ...
    }

    @Override  
    public void afterPropertiesSet() throws Exception { 
        // 实例构造后自动注册 
        MessageHandlerManager.register("txt", this);  
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

当然,还有一种做法,那就是使用「抽象类」,在父类定义好子类应该执行的统一操作。此处不再赘述。

# 小结与反思

# 基于注解实现的策略模式,步骤简单,通俗易懂!

在项目开发的过程中,我们经常会遇到如下的一种场景:对于用户的请求需要根据不同的情况进行不同的处理。

最简单粗暴的一种处理方式是使用switch…case或者if…else。但是这样处理方式只适用于处理逻辑简单或者情况分类较少的情况,如学校发放校服,男同学发放男士衣服,女同学发放女士衣服。
但是,如果处理逻辑毕竟复杂,或者情况分类较多,甚至未来有可能增加情况分类,上一种处理方式就会显得力不从心。此时使用策略模式将会是一种更优的处理方式。

# 基础配置&步骤

以下的方案是基于注解实现的策略模式。基础步骤&配置如下:

  • 定义策略名称:该项使用枚举实现
  • 定义策略名称注解:使用注解进行定义
  • 定义策略行为接口:该接口定义了策略行为
  • 定义策略处理器:包含策略名称的注解,并实现了策略行为接口
  • 定义策略容器:此处使用map作为策略容器,key为策略名称注解,value为策略处理器的实例
  • 初始化策略:容器初始化时候,从容器中读取包含策略名称注解的实例,并将其放入到策略容器中。

# 代码实现

在以下的例子中,会针对用户请求的Msg进行解析,msgType有两种:一种是聊天消息ChatMsg,还有一种是系统消息SysMsg。实现方式如下所示:

# 定义策略名称

public enum MsgType {

    CHAT_MSG,
    SYS_MSG;
}
1
2
3
4
5

# 定义策略名称注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Type {
    MsgType value();
}
1
2
3
4
5
6
7

# 定义策略行为接口

public interface BaseMsgHandler {
    void handleMsg(String content);
}
1
2
3

# 定义策略处理器

@Component
@Type(value = MsgType.CHAT_MSG)
public class ChatMsgHandler implements BaseMsgHandler{

    @Override
    public void handleMsg(String msg) {
        System.out.println("This is chatMsg. Detail msg information is :" + msg);
    }
}

@Component
@Type(value = MsgType.SYS_MSG)
public class SysMsgHandler implements BaseMsgHandler{

    @Override
    public void handleMsg(String msg) {
        System.out.println("This is sysMsg. Detail msg information is :" + msg);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 定义策略容器

public static final Map<Type, BaseMsgHandler> msgStrategyMap = new HashMap<>();
1

# 初始化策略

@Component
public class MsgConfig implements ApplicationContextAware {

    public static final Map<Type, BaseMsgHandler> msgStrategyMap = new HashMap<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        applicationContext.getBeansWithAnnotation(Type.class).entrySet().iterator().forEachRemaining(entrySet ->{
            msgStrategyMap.put(entrySet.getKey().getClass().getAnnotation(Type.class),
                    (BaseMsgHandler) entrySet.getValue());
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

上述准备动作完成后,就可以编写调用代码了:

import lombok.Data;

@Data
public class Msg{
	private String content;
	private MsgType msgType;
}

@RestController
@RequestMapping("/")
public class MsgController {

    @RequestMapping("msg")
    public void handleMsg(@RequestBody Msg msg){
        BaseMsgHandler handler = MsgConfig.msgStrategyMap.get(msg.getMsgType());
        handler.handleMsg(msg.getContent());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18