Spring 環(huán)境下實(shí)現(xiàn)策略模式的示例
背景
最近在忙一個(gè)需求,大致就是給滿足特定條件的用戶發(fā)營(yíng)銷(xiāo)郵件,但是用戶的來(lái)源有很多方式:從 ES 查詢的、從 csv 導(dǎo)入的、從 MongoDB 查詢….. 需求很簡(jiǎn)單,但是怎么寫(xiě)的優(yōu)雅,方便后續(xù)擴(kuò)展,就存在很多門(mén)道了。
我們的項(xiàng)目是基于 Spring Boot 開(kāi)發(fā)的,因此這篇文章也會(huì)基于 Spring Boot 作為基礎(chǔ)框架,教你如何使用 Spring 依賴注入的特性,優(yōu)雅的實(shí)現(xiàn)策略模式。
1. 簡(jiǎn)單粗暴
最簡(jiǎn)單粗暴直接的方式莫過(guò)于 if...else… 了,偽代碼如下:
if(來(lái)源 == ES){
// TODO: ES Query
}else if(來(lái)源 == CSV){
// TODO: Read CSV File
}else if(來(lái)源 == MongoDB){
// TODO: MongoDB Query
}
如果后面還需要從其他平臺(tái)獲取,那就在接著添加 else if...,這種方式固然簡(jiǎn)單直接,但是當(dāng)后續(xù)擴(kuò)展的方式越來(lái)越多,相應(yīng)的if...else...也會(huì)越來(lái)越長(zhǎng),emmm….. 怎么說(shuō)呢,黑貓白貓,能抓到老鼠的就是好貓。
2. 策略模式
在 Spring 環(huán)境下實(shí)現(xiàn)策略模式異常簡(jiǎn)單,畢竟 Spring 提供的依賴注入簡(jiǎn)直就是開(kāi)發(fā)利器~
既然是策略模式,那么定義策略肯定是首當(dāng)其沖,策略我們使用枚舉實(shí)現(xiàn)最佳。
public enum GroupType {
/**
* 從 ES 查詢
*/
ES,
/**
* 從 MongoDB 查詢
*/
MONGODB,
/**
* 從 文件 讀取
*/
FILE
}
下一步,我們定義一個(gè)接口,用于抽象通用的功能。
public interface IGroupSelect {
/**
* 人群獲取方式
*
* @return 人群選擇枚舉
*/
GroupType type();
/**
* 查詢滿足條件的用戶
*
* @param groupQuery 查詢條件
* @return 滿足條件的用戶
*/
default List<GroupUser> queryUser(GroupQuery groupQuery) {
checkQueryCondition(groupQuery);
return doQuery(groupQuery);
}
/**
* 事前校驗(yàn)查詢條件
*
* @param groupQuery 查詢條件
* @throws IllegalArgumentException 參數(shù)異常
*/
void checkQueryCondition(GroupQuery groupQuery) throws IllegalArgumentException;
/**
* 真正的查詢方法
*
* @param groupQuery 查詢條件
* @return 滿足條件的用戶
*/
List<GroupUser> doQuery(GroupQuery groupQuery);
}
這一步,小伙伴們有沒(méi)有發(fā)現(xiàn)里面也包含了模板方法模式呢?
然后就是不同策略的具體實(shí)現(xiàn)了。
- ES 策略
@Slf4j
@Service
public class EsGroupSelect implements IGroupSelect {
/**
* 人群獲取方式
*
* @return 人群選擇枚舉
*/
@Override
public GroupType type() {
return GroupType.ES;
}
/**
* 事前校驗(yàn)查詢條件
*
* @param groupQuery 查詢條件
* @throws IllegalArgumentException 參數(shù)異常
*/
@Override
public void checkQueryCondition(GroupQuery groupQuery) throws IllegalArgumentException {
log.info("groupQuery = {}", groupQuery);
}
/**
* 查詢滿足條件的用戶
*
* @param groupQuery 查詢條件
* @return 滿足條件的用戶
*/
@Override
public List<GroupUser> doQuery(GroupQuery groupQuery) {
List<GroupUser> result = new ArrayList<>();
// TODO:
// 1. 復(fù)雜的 ES 查詢邏輯
// 2. 根據(jù)條件篩選滿足條件的用戶數(shù)據(jù)
for (int i = 1; i <= 15; i++) {
result.add(GroupUser.of("ES用戶" + i, i + "@es.com"));
}
return result;
}
}
- 文件策略
@Slf4j
@Service
public class FileGroupSelect implements IGroupSelect {
/**
* 人群獲取方式
*
* @return 人群選擇枚舉
*/
@Override
public GroupType type() {
return GroupType.FILE;
}
/**
* 事前校驗(yàn)查詢條件
*
* @param groupQuery 查詢條件
* @throws IllegalArgumentException 參數(shù)異常
*/
@Override
public void checkQueryCondition(GroupQuery groupQuery) throws IllegalArgumentException {
log.info("groupQuery = {}", groupQuery);
}
/**
* 查詢滿足條件的用戶
*
* @param groupQuery 查詢條件
* @return 滿足條件的用戶
*/
@Override
public List<GroupUser> doQuery(GroupQuery groupQuery) {
List<GroupUser> result = new ArrayList<>();
// TODO:
// 1. 復(fù)雜的解析、讀文件
// 2. 根據(jù)條件篩選滿足條件的用戶數(shù)據(jù)
for (int i = 1; i <= 3; i++) {
result.add(GroupUser.of("文件讀取用戶" + i, i + "@file.com"));
}
return result;
}
}
- MongoDB 策略
@Slf4j
@Service
public class MongoGroupSelect implements IGroupSelect {
/**
* 人群獲取方式
*
* @return 人群選擇枚舉
*/
@Override
public GroupType type() {
return GroupType.MONGODB;
}
/**
* 事前校驗(yàn)查詢條件
*
* @param groupQuery 查詢條件
* @throws IllegalArgumentException 參數(shù)異常
*/
@Override
public void checkQueryCondition(GroupQuery groupQuery) throws IllegalArgumentException {
log.info("groupQuery = {}", groupQuery);
}
/**
* 查詢滿足條件的用戶
*
* @param groupQuery 查詢條件
* @return 滿足條件的用戶
*/
@Override
public List<GroupUser> doQuery(GroupQuery groupQuery) {
List<GroupUser> result = new ArrayList<>();
// TODO:
// 1. 復(fù)雜的 MongoDB 查詢邏輯
// 2. 根據(jù)條件篩選滿足條件的用戶數(shù)據(jù)
for (int i = 1; i <= 7; i++) {
result.add(GroupUser.of("MongoDB用戶" + i, i + "@mongo.com"));
}
return result;
}
}
現(xiàn)在到了最后一步,就是如何通過(guò) Spring 優(yōu)雅的實(shí)現(xiàn)策略模式的選擇呢?敲黑板,考試必考!
我們通過(guò)定義一個(gè)工廠類(lèi),然后使用 Spring 的依賴注入特性,可以注入一個(gè)接口的多個(gè)實(shí)現(xiàn),這里采用 List<IGroupSelect> 的形式注入,Spring 也支持通過(guò) Map<String,IGroupSelect> 的形式注入,如果使用 Map 注入,那么 key 就是類(lèi)名,小伙伴們自己也可以測(cè)試一下~
@Service
public class GroupSelectFactory {
@Autowired
private List<IGroupSelect> groupSelectList;
/**
* 根據(jù)人群類(lèi)型選擇具體的實(shí)現(xiàn)類(lèi)
*
* @param type 人群類(lèi)型
* @return 人群選擇具體實(shí)現(xiàn)類(lèi)
*/
public IGroupSelect getGroupSelect(GroupType type) {
Optional<IGroupSelect> groupSelectOptional = groupSelectList.stream().filter(t -> t.type() == type).findAny();
return groupSelectOptional.orElseThrow(() -> new IllegalArgumentException("暫不支持該人群方式"));
}
}
最后寫(xiě)個(gè)定時(shí)任務(wù)測(cè)試一下吧。
@Autowired
private GroupSelectFactory groupSelectFactory;
/**
* 模擬定時(shí)發(fā)送營(yíng)銷(xiāo)郵件
*/
@Scheduled(cron = "0/10 * * * * ?")
public void sendEmailTask() {
List<SendEmailTask> taskList = new ArrayList<>();
for (GroupType groupType : GroupType.values()) {
GroupQuery groupQuery = new GroupQuery("虛頭巴腦的 " + groupType.name() + " 查詢條件");
taskList.add(SendEmailTask.of(groupType, groupQuery));
}
taskList.forEach(task -> {
List<GroupUser> groupUsers = groupSelectFactory.getGroupSelect(task.getType()).queryUser(task.getQuery());
log.info("groupUsers = {}", groupUsers);
});
}
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
static class SendEmailTask implements Serializable {
private static final long serialVersionUID = -3461263089669779193L;
private GroupType type;
private GroupQuery query;
}
觀察控制臺(tái),看看日志輸出吧~
總結(jié)
- 本文使用策略模式實(shí)現(xiàn)不同人群的查詢,后續(xù)如果要增加短信、微信、釘釘?shù)南l(fā)送,是不是也可以用策略模式實(shí)現(xiàn)呢?
- 使用 Spring 的依賴注入特性,可以注入一個(gè)接口的多個(gè)實(shí)現(xiàn),很容易就實(shí)現(xiàn)了策略模式的選擇,這樣后續(xù)添加一種策略的時(shí)候,完全不需要改動(dòng)主要邏輯,只需添加具體實(shí)現(xiàn)即可。
- 細(xì)心的小伙伴可以發(fā)現(xiàn),本文雖然是講策略模式,其實(shí)里面還包含了模板方法、工廠模式,多種設(shè)計(jì)模式的協(xié)同作戰(zhàn),食用味道更佳喲~
配套代碼:https://github.com/xkcoding/practice_demo/tree/master/strategy-design-pattern-in-spring
以上就是Spring 環(huán)境下實(shí)現(xiàn)策略模式的示例的詳細(xì)內(nèi)容,更多關(guān)于Spring 實(shí)現(xiàn)策略模式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Java編寫(xiě)一個(gè)限流工具類(lèi)RateLimiter
這篇文章主要為大家詳細(xì)介紹了如何基于Java編寫(xiě)一個(gè)限流工具類(lèi)RateLimiter,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01
聊聊@RequestBody和Json之間的關(guān)系
這篇文章主要介紹了@RequestBody和Json之間的關(guān)系,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
Java中將多個(gè)PDF文件合并為一個(gè)PDF的方法步驟
這篇文章主要給大家介紹了關(guān)于Java中將多個(gè)PDF文件合并為一個(gè)PDF的方法步驟, Java PDF合并是指將多個(gè)PDF文件合并成一個(gè)PDF文件的過(guò)程,需要的朋友可以參考下2023-09-09
以Java?Web項(xiàng)目為例淺談前后端分離開(kāi)發(fā)模式
這篇文章主要介紹了以Java?Web項(xiàng)目為例淺談前后端分離開(kāi)發(fā)模式,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08
K8S環(huán)境下如何驗(yàn)證RocketMQ擴(kuò)縮容
文章主要內(nèi)容驗(yàn)證了K8S環(huán)境下RocketMQ的擴(kuò)縮容特性,包括序號(hào)變化、命名規(guī)則以及節(jié)點(diǎn)重建后序號(hào)保持不變,StatefulSet確保Pod序號(hào)在重建后保持穩(wěn)定,而Deployment創(chuàng)建的Pod名稱是隨機(jī)的2025-01-01
spring boot配合前端實(shí)現(xiàn)跨域請(qǐng)求訪問(wèn)
本篇文章主要介紹了spring boot配合前端實(shí)現(xiàn)跨域請(qǐng)求訪問(wèn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04
java實(shí)現(xiàn)搶紅包算法(公平版和手速版)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)搶紅包算法,分為公平版和手速版,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09
Dubbo新版本zk注冊(cè)中心連接問(wèn)題及解決
這篇文章主要介紹了Dubbo新版本zk注冊(cè)中心連接問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
Java使用PreparedStatement接口及ResultSet結(jié)果集的方法示例
這篇文章主要介紹了Java使用PreparedStatement接口及ResultSet結(jié)果集的方法,結(jié)合實(shí)例形式分析了PreparedStatement接口及ResultSet結(jié)果集的相關(guān)使用方法與操作注意事項(xiàng),需要的朋友可以參考下2018-07-07
Mybatis插入語(yǔ)句默認(rèn)值不生效的問(wèn)題及解決
這篇文章主要介紹了Mybatis插入語(yǔ)句默認(rèn)值不生效的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07

