likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 14

策略模式实践

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

  1. 策略模式简介及优点
  2. Java 常用的策略模式写法
  3. Java Stream API、Optional、InitializingBean 的使用
  4. unmodifiableMap 避免并发安全问题

设计模式简介

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

优点

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

设计模式应用——以 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";
    }
}

写好了这些,我们还需要有一个获取策略的地方,因此我们可以设计一个类,该类通过 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("不支持的消息类型"));        
    }
}

如此一来,当我们有新消息要处理的时候,就可以简单地通过 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);
    }
    
    ...
}

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);  
    }
}

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

小结与反思

上述内容可以帮助开发者更好地理解策略模式和 SpringBoot,其中提及到的一些小知识也不仅仅可以运用在策略模式中,例如关于策略实现类的不同注入方式,在非策略模式的场景中也可以用到。

希望本文对你有所帮助!如果喜欢还请不吝点赞收藏,也欢迎在评论区交流~