前言

风、宇的个人博客中针对第三方登录、文件上传等功能使用了策略模式+模板方法模式进行编写,是非常良好的编程习惯,这样在新增策略时只需要添加新类即可,同时也避免了复杂的条件判断。

这里总结一下策略模式与模板方法模式的结合使用。

值得提到的是,对于设计模式的学习我更偏向于结合具体的场景进行理解,以便今后灵活应用,而不是死板地记忆各种类角色。

策略模式

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

例如我们要创建一个计算策略,通过传入不同的策略执行不同的逻辑:

  1. 创建一个接口
1
2
3
public interface Strategy {
public int doOperation(int num1, int num2);
}
  1. 创建实现接口的实体类,封装了具体地执行逻辑,也就是不同的策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class OperationMultiply implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
  1. 创建 Context 类,此类帮助我们注入不同的策略,并执行策略内容
1
2
3
4
5
6
7
8
9
10
11
public class Context {
private Strategy strategy;

public Context(Strategy strategy){
this.strategy = strategy;
}

public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}
  1. 当我们使用不同的策略时,执行逻辑也不同
1
2
3
4
5
6
7
8
9
10
11
12
public class StrategyPatternDemo {
public static void main(String[] args) {
Context context = new Context(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

context = new Context(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

context = new Context(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}

输出:

1
2
3
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50

你可能会疑惑为什么要使用策略模式,上面的内容其实使用if判断也可以得到想要的内容,但设计模式的初衷在于如何让代码变得整洁优雅,实现设计模式的终极目标——对扩展开放,对修改封闭。使用if判断的缺点在于代码变得冗长晦涩,更糟糕的是在我们需要添加逻辑时需要修改if判断,对原有代码造成侵入。

模板方法模式

在模板方法模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

模板方法模式的主要目的是将方法调用中的共有部分抽离出来,独有部分做成暴露出来,供子类重写。其终极目标不言而喻——代码复用。

例如我们模拟一个玩游戏的过程:

  1. 创建一个抽象类,它的模板方法被设置为 final,防止被重写,固定的流程(开始游戏与结束游戏)也做成final函数。startPlay()为抽象函数,必须子类实现。
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
public abstract class Game {

abstract void startPlay();

void final initialize(){
System.out.println("初始化游戏");
}

void final endPlay(){
System.out.println("结束游戏");
}

//模板
public final void play(){

//初始化游戏
initialize();

//开始游戏
startPlay();

//结束游戏
endPlay();
}
}
  1. 实现不同的游戏,即玩的是不同的游戏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Cricket extends Game {

@Override
void startPlay() {
System.out.println("Cricket Game Started. Enjoy the game!");
}

}
public class Football extends Game {

@Override
void startPlay() {
System.out.println("Football Game Started. Enjoy the game!");
}

}
  1. 模拟玩游戏的过程
1
2
3
4
5
6
7
8
public class TemplatePatternDemo {
public static void main(String[] args) {
Game game = new Cricket();
game.play();
game = new Football();
game.play();
}
}

输出:

1
2
Cricket Game Started. Enjoy the game!
Football Game Started. Enjoy the game!

以上就是模拟玩游戏的全过程,我们将玩游戏的流程抽离出来,将固定的流程做成模板,而灵活可变的流程留给子类实现,达到了代码复用的功效。

策略模式+模板方法模式

策略模式与模板方法模式的结合使用,综合了这两种设计模式的优点,这里以风、宇博客中的第三方登录为例:

  1. 策略接口,只定义一个登录接口
1
2
3
4
5
6
7
8
9
10
11
public interface SocialLoginStrategy {

/**
* 登录
*
* @param data 数据
* @return {@link UserInfoDTO} 用户信息
*/
UserInfoDTO login(String data);

}
  1. 模板方法模式,抽取三方登录固有流程,其中抽象方法必须子类实现(黄色背景)
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public abstract class AbstractSocialLoginStrategyImpl implements SocialLoginStrategy {
@Autowired
private UserAuthDao userAuthDao;
@Autowired
private UserInfoDao userInfoDao;
@Autowired
private UserRoleDao userRoleDao;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Resource
private HttpServletRequest request;

@Override
public UserInfoDTO login(String data) {
// 创建登录信息
UserDetailDTO userDetailDTO;
// 获取第三方token信息
SocialTokenDTO socialToken = getSocialToken(data);
// 获取用户ip信息
String ipAddress = IpUtils.getIpAddress(request);
String ipSource = IpUtils.getIpSource(ipAddress);
// 判断是否已注册
UserAuth user = getUserAuth(socialToken);
if (Objects.nonNull(user)) {
// 返回数据库用户信息
userDetailDTO = getUserDetail(user, ipAddress, ipSource);
} else {
// 获取第三方用户信息,保存到数据库返回
userDetailDTO = saveUserDetail(socialToken, ipAddress, ipSource);
}
// 判断账号是否禁用
if (userDetailDTO.getIsDisable().equals(TRUE)) {
throw new BizException("账号已被禁用");
}
// 将登录信息放入springSecurity管理
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetailDTO, null, userDetailDTO.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
// 返回用户信息
return BeanCopyUtils.copyObject(userDetailDTO, UserInfoDTO.class);
}

/**
* 获取第三方token信息
*
* @param data 数据
* @return {@link SocialTokenDTO} 第三方token信息
*/
public abstract SocialTokenDTO getSocialToken(String data);

/**
* 获取第三方用户信息
*
* @param socialTokenDTO 第三方token信息
* @return {@link SocialUserInfoDTO} 第三方用户信息
*/
public abstract SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO);

/**
* 获取用户账号
*
* @return {@link UserAuth} 用户账号
*/
private UserAuth getUserAuth(SocialTokenDTO socialTokenDTO) {
return userAuthDao.selectOne(new LambdaQueryWrapper<UserAuth>()
.eq(UserAuth::getUsername, socialTokenDTO.getOpenId())
.eq(UserAuth::getLoginType, socialTokenDTO.getLoginType()));
}

/**
* 获取用户信息
*
* @param user 用户账号
* @param ipAddress ip地址
* @param ipSource ip源
* @return {@link UserDetailDTO} 用户信息
*/
private UserDetailDTO getUserDetail(UserAuth user, String ipAddress, String ipSource) {
// 更新登录信息
userAuthDao.update(new UserAuth(), new LambdaUpdateWrapper<UserAuth>()
.set(UserAuth::getLastLoginTime, LocalDateTime.now())
.set(UserAuth::getIpAddress, ipAddress)
.set(UserAuth::getIpSource, ipSource)
.eq(UserAuth::getId, user.getId()));
// 封装信息
return userDetailsService.convertUserDetail(user, request);
}

/**
* 新增用户信息
*
* @param socialToken token信息
* @param ipAddress ip地址
* @param ipSource ip源
* @return {@link UserDetailDTO} 用户信息
*/
private UserDetailDTO saveUserDetail(SocialTokenDTO socialToken, String ipAddress, String ipSource) {
// 获取第三方用户信息
SocialUserInfoDTO socialUserInfo = getSocialUserInfo(socialToken);
// 保存用户信息
UserInfo userInfo = UserInfo.builder()
.nickname(socialUserInfo.getNickname())
.avatar(socialUserInfo.getAvatar())
.build();
userInfoDao.insert(userInfo);
// 保存账号信息
UserAuth userAuth = UserAuth.builder()
.userInfoId(userInfo.getId())
.username(socialToken.getOpenId())
.password(socialToken.getAccessToken())
.loginType(socialToken.getLoginType())
.lastLoginTime(LocalDateTime.now(ZoneId.of(SHANGHAI.getZone())))
.ipAddress(ipAddress)
.ipSource(ipSource)
.build();
userAuthDao.insert(userAuth);
// 绑定角色
UserRole userRole = UserRole.builder()
.userId(userInfo.getId())
.roleId(RoleEnum.USER.getRoleId())
.build();
userRoleDao.insert(userRole);
return userDetailsService.convertUserDetail(userAuth, request);
}

}
  1. 第三方登录实现
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public class QQLoginStrategyImpl extends AbstractSocialLoginStrategyImpl {
@Autowired
private QQConfigProperties qqConfigProperties;
@Autowired
private RestTemplate restTemplate;

@Override
public SocialTokenDTO getSocialToken(String data) {
QQLoginVO qqLoginVO = JSON.parseObject(data, QQLoginVO.class);
// 校验QQ token信息
checkQQToken(qqLoginVO);
// 返回token信息
return SocialTokenDTO.builder()
.openId(qqLoginVO.getOpenId())
.accessToken(qqLoginVO.getAccessToken())
.loginType(LoginTypeEnum.QQ.getType())
.build();
}

@Override
public SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO) {
// 定义请求参数
Map<String, String> formData = new HashMap<>(3);
formData.put(QQ_OPEN_ID, socialTokenDTO.getOpenId());
formData.put(ACCESS_TOKEN, socialTokenDTO.getAccessToken());
formData.put(OAUTH_CONSUMER_KEY, qqConfigProperties.getAppId());
// 获取QQ返回的用户信息
QQUserInfoDTO qqUserInfoDTO = JSON.parseObject(restTemplate.getForObject(qqConfigProperties.getUserInfoUrl(), String.class, formData), QQUserInfoDTO.class);
// 返回用户信息
return SocialUserInfoDTO.builder()
.nickname(Objects.requireNonNull(qqUserInfoDTO).getNickname())
.avatar(qqUserInfoDTO.getFigureurl_qq_1())
.build();
}

/**
* 校验qq token信息
*
* @param qqLoginVO qq登录信息
*/
private void checkQQToken(QQLoginVO qqLoginVO) {
// 根据token获取qq openId信息
Map<String, String> qqData = new HashMap<>(1);
qqData.put(SocialLoginConst.ACCESS_TOKEN, qqLoginVO.getAccessToken());
try {
String result = restTemplate.getForObject(qqConfigProperties.getCheckTokenUrl(), String.class, qqData);
QQTokenDTO qqTokenDTO = JSON.parseObject(CommonUtils.getBracketsContent(Objects.requireNonNull(result)), QQTokenDTO.class);
// 判断openId是否一致
if (!qqLoginVO.getOpenId().equals(qqTokenDTO.getOpenid())) {
throw new BizException(QQ_LOGIN_ERROR);
}
} catch (Exception e) {
e.printStackTrace();
throw new BizException(QQ_LOGIN_ERROR);
}
}

}
public class WeiboLoginStrategyImpl extends AbstractSocialLoginStrategyImpl {
@Autowired
private WeiboConfigProperties weiboConfigProperties;
@Autowired
private RestTemplate restTemplate;

@Override
public SocialTokenDTO getSocialToken(String data) {
WeiboLoginVO weiBoLoginVO = JSON.parseObject(data, WeiboLoginVO.class);
// 获取微博token信息
WeiboTokenDTO weiboToken = getWeiboToken(weiBoLoginVO);
// 返回token信息
return SocialTokenDTO.builder()
.openId(weiboToken.getUid())
.accessToken(weiboToken.getAccess_token())
.loginType(LoginTypeEnum.WEIBO.getType())
.build();
}

@Override
public SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO) {
// 定义请求参数
Map<String, String> data = new HashMap<>(2);
data.put(UID, socialTokenDTO.getOpenId());
data.put(ACCESS_TOKEN, socialTokenDTO.getAccessToken());
// 获取微博用户信息
WeiboUserInfoDTO weiboUserInfoDTO = restTemplate.getForObject(weiboConfigProperties.getUserInfoUrl(), WeiboUserInfoDTO.class, data);
// 返回用户信息
return SocialUserInfoDTO.builder()
.nickname(Objects.requireNonNull(weiboUserInfoDTO).getScreen_name())
.avatar(weiboUserInfoDTO.getAvatar_hd())
.build();
}

/**
* 获取微博token信息
*
* @param weiBoLoginVO 微博登录信息
* @return {@link WeiboTokenDTO} 微博token
*/
private WeiboTokenDTO getWeiboToken(WeiboLoginVO weiBoLoginVO) {
// 根据code换取微博uid和accessToken
MultiValueMap<String, String> weiboData = new LinkedMultiValueMap<>();
// 定义微博token请求参数
weiboData.add(CLIENT_ID, weiboConfigProperties.getAppId());
weiboData.add(CLIENT_SECRET, weiboConfigProperties.getAppSecret());
weiboData.add(GRANT_TYPE, weiboConfigProperties.getGrantType());
weiboData.add(REDIRECT_URI, weiboConfigProperties.getRedirectUrl());
weiboData.add(CODE, weiBoLoginVO.getCode());
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(weiboData, null);
try {
return restTemplate.exchange(weiboConfigProperties.getAccessTokenUrl(), HttpMethod.POST, requestEntity, WeiboTokenDTO.class).getBody();
} catch (Exception e) {
throw new BizException(WEIBO_LOGIN_ERROR);
}
}

}
  1. 暴露context供用户调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SocialLoginStrategyContext {

@Autowired
private Map<String, SocialLoginStrategy> socialLoginStrategyMap;

/**
* 执行第三方登录策略
*
* @param data 数据
* @param loginTypeEnum 登录枚举类型
* @return {@link UserInfoDTO} 用户信息
*/
public UserInfoDTO executeLoginStrategy(String data, LoginTypeEnum loginTypeEnum) {
return socialLoginStrategyMap.get(loginTypeEnum.getStrategy()).login(data);
}

}