策略模式与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";
}
}
写好了这些,我们还需要有一个获取策略的地方,因此我们可以设计一个类,该类通过 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,其中提及到的一些小知识也不仅仅可以运用在策略模式中,例如关于策略实现类的不同注入方式,在非策略模式的场景中也可以用到。
希望本文对你有所帮助!如果喜欢还请不吝点赞收藏,也欢迎在评论区交流~
转载自:https://juejin.cn/post/7234803427041493050