介绍
1.定义
策略模式是一种行为型设计模式,它允许在运行时选择算法或策略,从而使得算法的变化不会影响使用算法的客户。
策略模式的定义
策略模式(Strategy Pattern)定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码。这种模式主要用于解决多种相似算法存在时,使用条件语句(如if…else)导致的复杂性和难以维护的问题。
2.组成
策略模式通常包含三个角色
- 策略接口(Strategy):定义了一组算法或者行为的公共接口,所有具体策略都必须实现该接口
- 具体策略类(Concrete Strategy):实现了策略接口,提供了具体的算法或者行为。
- 上下文类(Context):封装了具体策略的执行逻辑,提供给客户端使用的接口,通常包含一个指向策略接口的引用,用于调用具体策略的方法。
优缺点
优点
- 避免多重条件语句:使用策略模式可以避免使用多重条件转移语句,使代码更清晰易于维护。
- 符合闭合原则:可以在不修改原代码的情况下,灵活增加新算法。
- 将算法的使用和实现分离,提高了算法的保密性和安全性。
缺点
- 策略类数量增多:每增加一个算法,就要增加一个策略类,可能导致类的数量增加,增加维护难度。
- 客户端复杂性:客户端必须理解所有策略算法的区别,以便选择适当的算法类。
应用场景
- 当存在多种实现方式,且需要在运行时动态选择具体实现时,例如支付方式选择、排序算法选择等等。
- 当存在一组类似的行为,只是实现细节略有不同,但又不希望通过继承来添加新的子类时。
代码实例
// 策略接口
public interface PaymentStrategy {
void pay(double amount);
}
// 具体策略类
public class CreditCardPayment implements PaymentStrategy {
public void pay(double amount) {
System.out.println("使用信用卡支付:" + amount);
}
}
public class WeChatPayment implements PaymentStrategy {
public void pay(double amount) {
System.out.println("使用微信支付:" + amount);
}
}
// 上下文类
public class PaymentContext {
private PaymentStrategy paymentStrategy;
public PaymentContext(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void executePayment(double amount) {
paymentStrategy.pay(amount);
}
}
通过使用策略模式,我们可以将算法或行为与具体的业务逻辑解耦,使得系统更加灵活和可扩展。
好啦,以上就是策略模式的大概介绍和使用方式了,如果大家感兴趣的话可以继续看下面在实际项目开发过程中策略模式的使用过程。
实际使用实例
这里再根据实际项目来演示下策略模式在开发过程中的使用,这里以小说精品屋项目为例,大家感兴趣的话可以去看下这个项目。
背景
该系统由由前台门户系统、作家后台管理系统、平台后台管理系统和爬虫管理系统多个子系统构成,是一个复杂的多系统环境,如何实现统一对这些子系统进行认证授权呢。
- if…else模式
因为前端在每个登录入口成功后都会获得后端返回的token,这个时候需要分别保存起来,在请求系统的后端接口时,通过请求携带上对应的token。后端则需要配置一个统一的拦截器,根据请求的URL识别出相应的系统类型,并对这些进行解析得到userId,这个时候就可以根据用户来鉴权,如果用户有权访问,则放行,否则,返回一个相应的错误码给前端。
public class AuthInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils;
private final ObjectMapper objectMapper;
@SuppressWarnings("NullableProblems")
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 校验登录 token
String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME);
if (!Objects.isNull(token)) {
String requestUri = request.getRequestURI();
if (requestUri.contains(ApiRouterConsts.API_FRONT_URL_PREFIX)) {
// 校验门户系统用户权限
Long userId = jwtUtils.parseToken(token, SystemConfigConsts.NOVEL_FRONT_KEY);
if (!Objects.isNull(userId)) {
// TODO 查询用户信息并校验账号状态是否正常
// TODO 其它权限校验
// 认证成功
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}else if (requestUri.contains(ApiRouterConsts.API_AUTHOR_URL_PREFIX)){
// TODO 校验作家后台管理系统用户权限
}else if (requestUri.contains(ApiRouterConsts.API_ADMIN_URL_PREFIX)){
// TODO 校验平台后台管理系统用户权限
}
//。。。更多系统权限校验
// 完整实现可能至少几百行的代码
}
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(objectMapper.writeValueAsString(RestResp.fail(ErrorCodeEnum.USER_LOGIN_EXPIRED)));
return false;
}
}
此时,可以简单的实现基本的权限拦截功能。但是因为所有系统的认证授权逻辑都在这一个方法中,代码极其臃肿难以维护。每当某一系统授权逻辑发生变化或者新增加了一个子系统,都需要修改此处的代码。修改之前不但必须先完全理解这一大段代码,正确定位到需要修改的位置,而且极其容易影响到不相干的其它系统认证授权功能。久而久之就没有人愿意维护这部分代码了。为了解决这个问题,下面我们使用策略模式来重构该功能。
重构步骤
- 定义策略接口
创建 AuthStrategy 接口,该接口定义了一个默认的方法实现用户端所有子系统都需要的统一账号认证逻辑和一个封装各个系统独立认证授权逻辑的待实现方法。
public interface AuthStrategy {
/**
* 用户认证授权
*
* @param token 登录 token
* @param requestUri 请求的 URI
* @throws BusinessException 认证失败则抛出业务异常
*/
void auth(String token, String requestUri) throws BusinessException;
/**
* 前台多系统单点登录统一账号认证(门户系统、作家系统以及后面会扩展的漫画系统和视频系统等)
*
* @param jwtUtils jwt 工具
* @param userInfoCacheManager 用户缓存管理对象
* @param token token 登录 token
* @return 用户ID
*/
default Long authSSO(JwtUtils jwtUtils, UserInfoCacheManager userInfoCacheManager,
String token) {
if (!StringUtils.hasText(token)) {
// token 为空
throw new BusinessException(ErrorCodeEnum.USER_LOGIN_EXPIRED);
}
Long userId = jwtUtils.parseToken(token, SystemConfigConsts.NOVEL_FRONT_KEY);
if (Objects.isNull(userId)) {
// token 解析失败
throw new BusinessException(ErrorCodeEnum.USER_LOGIN_EXPIRED);
}
UserInfoDto userInfo = userInfoCacheManager.getUser(userId);
if (Objects.isNull(userInfo)) {
// 用户不存在
throw new BusinessException(ErrorCodeEnum.USER_ACCOUNT_NOT_EXIST);
}
// 设置 userId 到当前线程
UserHolder.setUserId(userId);
// 返回 userId
return userId;
}
}
- 具体策略类
接着定义具体的策略类并实现上述策略接口
@Component
@RequiredArgsConstructor
public class FrontAuthStrategy implements AuthStrategy {
private final JwtUtils jwtUtils;
private final UserInfoCacheManager userInfoCacheManager;
@Override
public void auth(String token, String requestUri) throws BusinessException {
// 统一账号认证
authSSO(jwtUtils, userInfoCacheManager, token);
}
}
@Component
@RequiredArgsConstructor
public class AuthorAuthStrategy implements AuthStrategy {
private final JwtUtils jwtUtils;
private final UserInfoCacheManager userInfoCacheManager;
private final AuthorInfoCacheManager authorInfoCacheManager;
/**
* 不需要进行作家权限认证的 URI
*/
private static final List<String> EXCLUDE_URI = List.of(
ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/register",
ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/status"
);
@Override
public void auth(String token, String requestUri) throws BusinessException {
// 统一账号认证
Long userId = authSSO(jwtUtils, userInfoCacheManager, token);
if (EXCLUDE_URI.contains(requestUri)) {
// 该请求不需要进行作家权限认证
return;
}
// 作家权限校验
AuthorInfoDto authorInfo = authorInfoCacheManager.getAuthor(userId);
if (Objects.isNull(authorInfo)) {
// 作家账号不存在,无权访问作家专区
throw new BusinessException(ErrorCodeEnum.USER_UN_AUTH);
}
// 设置作家ID到当前线程
UserHolder.setAuthorId(authorInfo.getId());
}
}
@Component
@RequiredArgsConstructor
public class AdminAuthStrategy implements AuthStrategy {
@Override
public void auth(String token, String requestUri) throws BusinessException {
// TODO 平台后台 token 校验
}
}
- 定义上下文类
最后在拦截器中根据请求的URL调用具体的策略
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private final Map<String, AuthStrategy> authStrategy;
private final ObjectMapper objectMapper;
/**
* handle 执行前调用
*/
@SuppressWarnings("NullableProblems")
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 获取登录 JWT
String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME);
// 获取请求的 URI
String requestUri = request.getRequestURI();
// 根据请求的 URI 得到认证策略
String subUri = requestUri.substring(ApiRouterConsts.API_URL_PREFIX.length() + 1);
String systemName = subUri.substring(0, subUri.indexOf("/"));
String authStrategyName = String.format("%sAuthStrategy", systemName);
// 开始认证
try {
authStrategy.get(authStrategyName).auth(token, requestUri);
return HandlerInterceptor.super.preHandle(request, response, handler);
} catch (BusinessException exception) {
// 认证失败
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(
objectMapper.writeValueAsString(RestResp.fail(exception.getErrorCodeEnum())));
return false;
}
}
/**
* handler 执行后调用,出现异常不调用
*/
@SuppressWarnings("NullableProblems")
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* DispatcherServlet 完全处理完请求后调用,出现异常照常调用
*/
@SuppressWarnings("NullableProblems")
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 清理当前线程保存的用户数据
UserHolder.clear();
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}