背景
最近在忙一个需求,大致就是给满足特定条件的用户发营销邮件,但是用户的来源有很多方式:从 ES 查询的、从 csv 导入的、从 MongoDB 查询…… 需求很简单,但是怎么写的优雅,方便后续扩展,就存在很多门道了。
我们的项目是基于 Spring Boot 开发的,因此这篇文章也会基于 Spring Boot 作为基础框架,教你如何使用 Spring 依赖注入的特性,优雅的实现策略模式。
1. 简单粗暴
最简单粗暴直接的方式莫过于 if...else…
了,伪代码如下:
1 2 3 4 5 6 7
| if(来源 == ES){ }else if(来源 == CSV){ }else if(来源 == MongoDB){ }
|
如果后面还需要从其他平台获取,那就在接着添加 else if...
,这种方式固然简单直接,但是当后续扩展的方式越来越多,相应的if...else...
也会越来越长,emmm……怎么说呢,黑猫白猫,能抓到老鼠的就是好猫。
2. 策略模式
在 Spring 环境下实现策略模式异常简单,毕竟 Spring 提供的依赖注入简直就是开发利器~
既然是策略模式,那么定义策略肯定是首当其冲,策略我们使用枚举实现最佳。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public enum GroupType {
ES,
MONGODB,
FILE }
|
下一步,我们定义一个接口,用于抽象通用的功能。
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
| public interface IGroupSelect {
GroupType type();
default List<GroupUser> queryUser(GroupQuery groupQuery) { checkQueryCondition(groupQuery); return doQuery(groupQuery); }
void checkQueryCondition(GroupQuery groupQuery) throws IllegalArgumentException;
List<GroupUser> doQuery(GroupQuery groupQuery);
}
|
这一步,小伙伴们有没有发现里面也包含了模板方法模式呢?
然后就是不同策略的具体实现了。
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
| @Slf4j @Service public class EsGroupSelect implements IGroupSelect {
@Override public GroupType type() { return GroupType.ES; }
@Override public void checkQueryCondition(GroupQuery groupQuery) throws IllegalArgumentException { log.info("groupQuery = {}", groupQuery); }
@Override public List<GroupUser> doQuery(GroupQuery groupQuery) { List<GroupUser> result = new ArrayList<>(); for (int i = 1; i <= 15; i++) { result.add(GroupUser.of("ES用户" + i, i + "@es.com")); } return result; } }
|
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
| @Slf4j @Service public class FileGroupSelect implements IGroupSelect {
@Override public GroupType type() { return GroupType.FILE; }
@Override public void checkQueryCondition(GroupQuery groupQuery) throws IllegalArgumentException { log.info("groupQuery = {}", groupQuery); }
@Override public List<GroupUser> doQuery(GroupQuery groupQuery) { List<GroupUser> result = new ArrayList<>(); for (int i = 1; i <= 3; i++) { result.add(GroupUser.of("文件读取用户" + i, i + "@file.com")); } return result; } }
|
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
| @Slf4j @Service public class MongoGroupSelect implements IGroupSelect {
@Override public GroupType type() { return GroupType.MONGODB; }
@Override public void checkQueryCondition(GroupQuery groupQuery) throws IllegalArgumentException { log.info("groupQuery = {}", groupQuery); }
@Override public List<GroupUser> doQuery(GroupQuery groupQuery) { List<GroupUser> result = new ArrayList<>(); for (int i = 1; i <= 7; i++) { result.add(GroupUser.of("MongoDB用户" + i, i + "@mongo.com")); } return result; } }
|
现在到了最后一步,就是如何通过 Spring 优雅的实现策略模式的选择呢?敲黑板,考试必考!
我们通过定义一个工厂类,然后使用 Spring 的依赖注入特性,可以注入一个接口的多个实现,这里采用 List<IGroupSelect>
的形式注入,Spring 也支持通过 Map<String,IGroupSelect>
的形式注入,如果使用 Map 注入,那么 key 就是类名,小伙伴们自己也可以测试一下~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Service public class GroupSelectFactory { @Autowired private List<IGroupSelect> groupSelectList;
public IGroupSelect getGroupSelect(GroupType type) { Optional<IGroupSelect> groupSelectOptional = groupSelectList.stream().filter(t -> t.type() == type).findAny(); return groupSelectOptional.orElseThrow(() -> new IllegalArgumentException("暂不支持该人群方式")); } }
|
最后写个定时任务测试一下吧。
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
| @Autowired private GroupSelectFactory groupSelectFactory;
@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; }
|
观察控制台,看看日志输出吧~
总结
本文使用策略模式实现不同人群的查询,后续如果要增加短信、微信、钉钉的消息发送,是不是也可以用策略模式实现呢?
使用 Spring 的依赖注入特性,可以注入一个接口的多个实现,很容易就实现了策略模式的选择,这样后续添加一种策略的时候,完全不需要改动主要逻辑,只需添加具体实现即可。
细心的小伙伴可以发现,本文虽然是讲策略模式,其实里面还包含了模板方法、工厂模式,多种设计模式的协同作战,食用味道更佳哟~
配套代码:https://github.com/xkcoding/practice_demo/tree/master/strategy-design-pattern-in-spring