1. 概述
先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。
作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea
进行开发,也可以使用eclipse
进行开发,也可以使用其他的一些开发工具。
定义:
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
2. 结构
策略模式的主要角色如下:
- 抽象策略(
Strategy
)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。 - 具体策略(
Concrete Strategy
)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。 - 环境(
Context
)类:持有一个策略类的引用,最终给客户端调用。
3. 案例实现
【例】促销活动
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:
聚合关系可以用带空心菱形的实线来表示。
代码如下:
定义百货公司所有促销活动的共同接口。
public interface Strategy {
void show();
}
定义具体策略角色(Concrete Strategy
):每个节日具体的促销活动。
//为春节准备的促销活动A
public class StrategyA implements Strategy {
public void show() {
System.out.println("买一送一");
}
}
//为中秋准备的促销活动B
public class StrategyB implements Strategy {
public void show() {
System.out.println("满200元减50元");
}
}
//为圣诞准备的促销活动C
public class StrategyC implements Strategy {
public void show() {
System.out.println("满1000元加一元换购任意200元以下商品");
}
}
定义环境角色(Context
):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员。
public class SalesMan {
//持有抽象策略角色的引用
private Strategy strategy;
public SalesMan(Strategy strategy) {
this.strategy = strategy;
}
//向客户展示促销活动
public void salesManShow(){
strategy.show();
}
}
4. 综合案例
4.1 概述
下图是gitee
的登录的入口,其中有多种方式可以进行登录。
-
用户名密码登录
-
短信验证码登录
-
微信登录
-
QQ
登录 -
…
4.2 目前已实现的代码
(1)登录接口
说明 | |
---|---|
请求方式 | POST |
路径 | /api/user/login |
参数 | LoginReq |
返回值 | LoginResp |
请求参数:LoginReq
@Data
public class LoginReq {
private String name;
private String password;
private String phone;
private String validateCode;//手机验证码
private String wxCode;//用于微信登录
/**
* account : 用户名密码登录
* sms : 手机验证码登录
* we_chat : 微信登录
*/
private String type;
}
响应参数:LoginResp
@Data
public class LoginResp{
private Integer userId;
private String userName;
private String roleCode;
private String token; //jwt令牌
private boolean success;
}
控制层LoginController
@RestController
@RequestMapping("/api/user")
public class LoginController {
@Autowired
private UserService userService;
@PostMapping("/login")
public LoginResp login(@RequestBody LoginReq loginReq){
return userService.login(loginReq);
}
}
业务层UserService
@Service
public class UserService {
public LoginResp login(LoginReq loginReq){
if(loginReq.getType().equals("account")){
System.out.println("用户名密码登录");
//执行用户密码登录逻辑
return new LoginResp();
}else if(loginReq.getType().equals("sms")){
System.out.println("手机号验证码登录");
//执行手机号验证码登录逻辑
return new LoginResp();
}else if (loginReq.getType().equals("we_chat")){
System.out.println("微信登录");
//执行用户微信登录逻辑
return new LoginResp();
}
LoginResp loginResp = new LoginResp();
loginResp.setSuccess(false);
System.out.println("登录失败");
return loginResp;
}
}
注意:我们重点讲的是设计模式,并不是登录的逻辑,所以以上代码并没有真正的实现登录功能。
(2)问题分析
- 业务层代码大量使用到了
if...else
,在后期阅读代码的时候会非常不友好,大量使用if...else
性能也不高。 - 如果业务发生变更,比如现在新增了
QQ
登录方式,这个时候需要修改业务层代码,违反了开闭原则。
解决:
使用工厂方法设计模式+策略模式解决。
4.3 代码改造(工厂+策略)
(1)整体思路
改造之后,不在service
中写业务逻辑,让service
调用工厂,然后通过service
传递不同的参数来获取不同的登录策略(登录方式)。
(2)具体实现
抽象策略类:UserGranter
/**
* 抽象策略类
*/
public interface UserGranter{
/**
* 获取数据
* @param loginReq 传入的参数
* @return map值
*/
LoginResp login(LoginReq loginReq);
}
具体的策略:AccountGranter
、SmsGranter
、WeChatGranter
/**
* 策略:账号登录
**/
@Component
public class AccountGranter implements UserGranter{
@Override
public LoginResp login(LoginReq loginReq) {
System.out.println("登录方式为账号登录" + loginReq);
// TODO
// 执行业务操作
return new LoginResp();
}
}
/**
* 策略:短信登录
*/
@Component
public class SmsGranter implements UserGranter{
@Override
public LoginResp login(LoginReq loginReq) {
System.out.println("登录方式为短信登录" + loginReq);
// TODO
// 执行业务操作
return new LoginResp();
}
}
/**
* 策略:微信登录
*/
@Component
public class WeChatGranter implements UserGranter{
@Override
public LoginResp login(LoginReq loginReq) {
System.out.println("登录方式为微信登录" + loginReq);
// TODO
// 执行业务操作
return new LoginResp();
}
}
工程类:UserLoginFactory
/**
* 操作策略的上下文环境类 工具类
* 将策略整合起来 方便管理
*/
@Component
public class UserLoginFactory implements ApplicationContextAware {
private static Map<String, UserGranter> granterPool = new ConcurrentHashMap<>();
@Autowired
private LoginTypeConfig loginTypeConfig;
/**
* 从配置文件中读取策略信息存储到map中
* {
* account:accountGranter,
* sms:smsGranter,
* we_chat:weChatGranter
* }
*
* @param applicationContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
loginTypeConfig.getTypes().forEach((k, y) -> {
granterPool.put(k, (UserGranter) applicationContext.getBean(y));
});
}
/**
* 对外提供获取具体策略
*
* @param grantType 用户的登录方式,需要跟配置文件中匹配
* @return 具体策略
*/
public UserGranter getGranter(String grantType) {
UserGranter tokenGranter = granterPool.get(grantType);
return tokenGranter;
}
}
在application.yml
文件中新增自定义配置
login:
types:
account: accountGranter
sms: smsGranter
we_chat: weChatGranter
新增读取数据配置类
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "login")
public class LoginTypeConfig {
private Map<String,String> types;
}
改造service
代码
@Service
public class UserService {
@Autowired
private UserLoginFactory factory;
public LoginResp login(LoginReq loginReq){
UserGranter granter = factory.getGranter(loginReq.getType());
if(granter == null){
LoginResp loginResp = new LoginResp();
loginResp.setSuccess(false);
return loginResp;
}
LoginResp loginResp = granter.login(loginReq);
return loginResp;
}
}
大家可以看到我们使用了设计模式之后,业务层的代码就清爽多了,如果后期有新的需求改动,比如加入了QQ
登录,我们只需要添加对应的策略就可以,无需再改动业务层代码。
4.4 举一反三
其实像这样的需求,在日常开发中非常常见,场景有很多,以下的情景都可以使用工厂模式+策略模式解决。比如:
- 订单的支付策略
- 支付宝支付
- 微信支付
- 银行卡支付
- 现金支付
- 解析不同类型
excel
xls
格式xlsx
格式
- 打折促销
- 满300元9折
- 满500元8折
- 满1000元7折
- 物流运费阶梯计算
- 5
kg
以下 - 5
kg
-10kg
- 10
kg
-20kg
- 20
kg
以上
- 5
一句话总结:只要代码中有冗长的if-else
或switch
分支判断都可以采用策略模式优化。