前言
风、宇的个人博客中针对第三方登录、文件上传等功能使用了策略模式+模板方法模式进行编写,是非常良好的编程习惯,这样在新增策略时只需要添加新类即可,同时也避免了复杂的条件判断。
这里总结一下策略模式与模板方法模式的结合使用。
值得提到的是,对于设计模式的学习我更偏向于结合具体的场景进行理解,以便今后灵活应用,而不是死板地记忆各种类角色。
策略模式
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
例如我们要创建一个计算策略,通过传入不同的策略执行不同的逻辑:
- 创建一个接口
1 2 3
| public interface Strategy { public int doOperation(int num1, int num2); }
|
- 创建实现接口的实体类,封装了具体地执行逻辑,也就是不同的策略
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; } }
|
- 创建 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 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)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
模板方法模式的主要目的是将方法调用中的共有部分抽离出来,独有部分做成暴露出来,供子类重写。其终极目标不言而喻——代码复用。
例如我们模拟一个玩游戏的过程:
- 创建一个抽象类,它的模板方法被设置为 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 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 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 2 3 4 5 6 7 8 9 10 11
| public interface SocialLoginStrategy {
UserInfoDTO login(String data);
}
|
- 模板方法模式,抽取三方登录固有流程,其中抽象方法必须子类实现(黄色背景)
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; SocialTokenDTO socialToken = getSocialToken(data); 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("账号已被禁用"); } UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetailDTO, null, userDetailDTO.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); return BeanCopyUtils.copyObject(userDetailDTO, UserInfoDTO.class); }
public abstract SocialTokenDTO getSocialToken(String data);
public abstract SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO);
private UserAuth getUserAuth(SocialTokenDTO socialTokenDTO) { return userAuthDao.selectOne(new LambdaQueryWrapper<UserAuth>() .eq(UserAuth::getUsername, socialTokenDTO.getOpenId()) .eq(UserAuth::getLoginType, socialTokenDTO.getLoginType())); }
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); }
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 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); checkQQToken(qqLoginVO); 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()); 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(); }
private void checkQQToken(QQLoginVO qqLoginVO) { 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); 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); WeiboTokenDTO weiboToken = getWeiboToken(weiBoLoginVO); 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(); }
private WeiboTokenDTO getWeiboToken(WeiboLoginVO weiBoLoginVO) { MultiValueMap<String, String> weiboData = new LinkedMultiValueMap<>(); 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); } }
}
|
- 暴露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;
public UserInfoDTO executeLoginStrategy(String data, LoginTypeEnum loginTypeEnum) { return socialLoginStrategyMap.get(loginTypeEnum.getStrategy()).login(data); }
}
|