浅谈设计模式之策略模式在积分系统中的应用

浅谈设计模式之策略模式在积分系统中的应用

Tans 1,889 2022-05-28

策略模式在积分系统中的应用

测试链接:https://points.tans.fun/

前言

引言: 互联网平台积分体系是一个独立、完整的系统模块,主要用于激励和回馈用户在平台的消费行为和活动行为,通过积分体系可以激发与引导用户在平台的活跃行为,逐步形成用户对平台的依赖性和习惯性,提升用户对平台的黏度和使用率。

最近大三课程软件系统实践项目主要是构建一套网站积分体系,根据用户不同的行为进行积分管理。同时每个行为所对应的积分有如下特点:

  • 积分分为可兑换积分(存在失效日期)和成长积分(不存在失效日期)
  • 不同行为对应的积分分数不同
  • 不同行为积分都存在有一定的限制,也就是限制每日、每月、每年获取积分上限次数。

由于项目使用的是Spring Web进行开发,容易想到的就是在每个Controller中主要逻辑实现完成后再进行积分策略。但是此方法有如下缺

  • 项目逻辑耦合度增大,过多非主要业务会嵌入主要业务逻辑,导致后期维护难度增大,系统可维护性下降。
  • 积分体系通常作为辅助业务,其产生异常可能导致其他重要业务也发生异常,系统可靠性降低
  • 积分具体逻辑代码可能涉及到与数据库打交道,因此过多积分逻辑的织入可能造成系统总体性能下降,导致系统的QPS、TPS下降。

那么,存在其他方法来解决此问题吗,经过无数次Google,最终得到了一套可行的方案:

  • 引入策略模式来管理具体策略 ==> 实现业务代码和积分业务代码逻辑分离,增加可维护性
  • 引入MQ ==> 实现主要业务和积分业务的系统分离,异步请求降低接口耗时
  • 引入Redis缓存,将静态页面、Session以及以及对象加入缓存 ==> 吞吐量提升,缓解垃圾服务器磁盘IO上压力 (当然页面缓存也可以用Nginx做)

具体实现

对于MQ和Redis缓存实现,本文主要阐述如何使用策略模式来进行积分逻辑代码和业务主逻辑的分离,提高代码的可扩展性。读者只需要了解Spring Web中 HandlerInterceptor即可,本质上是靠一系列拦截器来实现积分逻辑的。在此系统中我们需要三个部件:

关于策略模式,读者可以查阅这篇文章 : 策略模式

  • 策略类:策略实施者,用来实现真正的积分变动业务。
  • 策略注解:和Controller中的GetMapping("url=...")一样,用来注明不同策略类对应的需要起作用的URL
  • 拦截器:策略管理类,用来注册所有策略类,简单来说,就是管理从 URL到相应策略的映射。

1. 策略注解

/**
 * @author tyf
 * @description 策略注解
 * @date 10:20 2022/5/12
 **/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserTaskStrategyType {
    /**
     * 需要拦截的 URL
     *
     * @return uri数组
     */
    String[] uri();
}

2. 策略类实现

为了降低耦合度,采用 接口–>抽象类–>具体类的结构来降低代码耦合度。

I: 策略类接口

/**
 * @author tyf
 * @description define the strategy method
 * @date 10:23 2022/5/12
 **/
public interface IUserTaskStrategy {

    /**
     * 主要声明具体增加积分逻辑
     * @param user  用户对象
     * @param token
     */
    void finishedUserIntegralTask(User user, String token, HttpServletRequest request);
}

II: 策略类抽象类

/**
 * @describe: 用户逻辑抽象类
 * @author: tyf
 * @createTime: 2022/5/18 11:51
 **/
public abstract class UserTaskStrategy implements IUserTaskStrategy {

    @Autowired
    UserService userService;

    @Autowired
    CreditTransactionService creditTransactionService;

    protected Event event;
    
    public UserTaskStrategy(Event event) {
        this.event = event;
    }

    public UserTaskStrategy() {}
    
    /**
     * @author tyf
     * @description 策略前置操作:通常是取得事件类型等
     * @date 17:26 2022/5/20
     **/
    public void preOperation(User user, String token, HttpServletRequest request){
        return;
    }

    /**
     * @author tyf
     * @description 积分策略后置操作
     * @date 17:26 2022/5/20
     **/
    public void completeOperation(User user,String token, HttpServletRequest request){
        return;
    }

     //具体逻辑执行类
    @Override
    public void finishedUserIntegralTask(User user, String token, HttpServletRequest request) {
        preOperation(user, token, request);
        creditTransactionService.insert(user, event);
        completeOperation(user, token, request);
    }
}

III: 具体策略类

这里用具体的更新用户个人信息增加积分举例

/**
 * @describe: 用户更新个人信息策略
 * @author: tyf
 * @createTime: 2022/5/18 09:48
 **/
@Component
@UserTaskStrategyType(uri = {"/u/update"})
public class UpdateInfoStrategy extends UserTaskStrategy {
     //这里也可以重写finishedUserIntegralTask方法进行重写策略
    public UpdateInfoStrategy() {
        super(Event.UPDATE_USER_DETAIL);
    }

}

3. 拦截器

/**
 * @Describe: 用户任务拦截器,主要通过userTaskStrategyMap来维护不同的积分策略
 * @Author: tyf
 * @CreateTime: 2022/5/12
 **/
@Component
public class UserTaskInterceptor implements HandlerInterceptor{

    private Map<String, IUserTaskStrategy> userTaskStrategyMap = new HashMap<>();

    @Autowired
    UserService userService;

    @Autowired
    LoginStrategy loginUserTaskStrategy;

    /**
     * 生成对应 Map
     * @param iUserTaskStrategies
     */
    @Autowired
    public void setUserTaskStrategyMap(List<IUserTaskStrategy> iUserTaskStrategies){
        iUserTaskStrategies.forEach(iUserTaskStrategy -> {
            String[] uris = Objects.requireNonNull(AnnotationUtils.findAnnotation(iUserTaskStrategy.getClass(),
                    UserTaskStrategyType.class).uri());
            userTaskStrategyMap.putAll(Arrays.stream(uris).collect(Collectors.toMap(uri->uri, uri-> iUserTaskStrategy)));
        });
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        String uri = request.getRequestURI();
        String token = RequestUtil.getTokenByRequest(request);
        User user = userService.getByToken(response, token);
        //每个接口都可以使用每日登陆积分策略拦截器
        loginUserTaskStrategy.finishedUserIntegralTask(user, token, request);
        //获取对应的积分策略拦截器
        IUserTaskStrategy userStrategy = userTaskStrategyMap.get(uri);
        //其他接口过滤器
        if (userStrategy != null) {
            userStrategy.finishedUserIntegralTask(user, token, request);
        }
    }

}

4. 将拦截器注册到MVC配置类中

/**
 * @Describe: web全局配置类
 * @Author: tyf
 * @CreateTime: 2022/4/17
 **/
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private UserArgumentResolver userArgumentResolver;
    @Autowired
    private AuthInterceptor authInterceptor;
    private List<String> uriList = new ArrayList<>();
    @Autowired
    private UserTaskInterceptor userTaskInterceptor;


    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        //注入自定义的解析器
        argumentResolvers.add(userArgumentResolver);
    }
     
     //提取所有需要拦截的URL
    @Autowired
    public void setUriList(List<IUserTaskStrategy> iUserTaskStrategies){
        iUserTaskStrategies.forEach(iUserTaskStrategy -> {
            String[] uris = Objects.requireNonNull(AnnotationUtils.findAnnotation(iUserTaskStrategy.getClass(),
                    UserTaskStrategyType.class).uri());
            uriList.addAll(Arrays.asList(uris));
        });
    }

     //将相应的URL配置到对应的拦截器中
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userTaskInterceptor).addPathPatterns(uriList);
    }
}

由此可见,策略模式已经完美落地到项目中,并且后续如需要有定义其他的积分项目,只需实现新的策略类并添加相应策略注解即可。而前文所述对应其他MQ、Redis中可以在具体的策略中运用。

笔者通过这次实验学到了很多,不得不说:设计模式,真精髓也!

具体项目实操: 积分管理系统

总结

本文通过积分业务说起,引入策略模式

  • 设计策略注解,主要包含被此注解标记的策略类的适用URL
  • 设计策略接口、策略类,并且通过策略注解注明适配的URL
  • 生成Interceptor, 内部维护一个Map<URL, Interceptor>结构,将具体url与对应策略类绑定
  • 将上一步生成的Interceptor注册进系统MVC配置拦截器链中

扩展方面,如果需要扩展积分业务,只需要:

  • 新建策略类继承策略抽象类,重写相关策略积分方法。
  • 在策略类的使用注解来说明拦截的URL
  • 使用

可以看到,这个模式下的扩展性还是很OK的,符合开闭原则,不会对已有功能造成影响。

其他文章

  1. 设计模式之装饰器模式