策略模式的介绍和具体实现
❤ 作者主页:李奕赫揍小邰的博客
❀ 个人介绍:大家好,我是李奕赫!( ̄▽ ̄)~*
🍊 记得点赞、收藏、评论⭐️⭐️⭐️
📣 认真学习!!!🎉🎉
文章目录
- 策略接口
- 三种策略实现
- 全局执行器
最近做的一个AI应用答题平台,其中有应用类型分为两种,一种是测评类例如MBIT测试,一种是打分类例如科普知识得分等。并且其中评分策略也有两种,一种直接计算得出结论,一种用AI生成结论评分。因此这种生成评分结果的方法正好可以使用策略模式来实现,接下来将会用策略模式逐渐解析这个问题
策略接口
需求:针对不同的应用类别和评分策略,编写不同的实现逻辑。
策略模式是一种行为设计模式,它定义了一系列算法,并将每个算法封装到独立的类中,使得它们可以相互替换。在本项目的场景中,输入的参数是一致的(应用和用户的答案列表),并且每种实现逻辑区别较大,比较适合使用策略模式。
/**
*评分策略接口
*/
public interface ScoringStrategy {/*** 执行评分*/UserAnswer doScore(List<String> choices, App app) throws Exception;
}
三种策略实现
之前讲过,应用分两种,测评类和打分类,题目评分分为两种,自定义评分和AI评分。因此我们可以组成三种策略形式,自定义测评,自定义打分,AI测评三种类型,当然后续要是有更多类型,可以依次累加。
/**
*自定义测评策略
*/
public class CustomTestScoringStrategy implements ScoringStrategy {@Resourceprivate QuestionService questionService;@Resourceprivate ScoringResultService scoringResultService;@Overridepublic UserAnswer doScore(List<String> choices, App app) throws Exception {Long appId = app.getId();// 1. 根据 id 查询到题目和题目结果信息Question question = questionService.getOne(Wrappers.lambdaQuery(Question.class).eq(Question::getAppId, appId));List<ScoringResult> scoringResultList = scoringResultService.list(Wrappers.lambdaQuery(ScoringResult.class).eq(ScoringResult::getAppId, appId));// 2. 统计用户每个选择对应的属性个数,如 I = 10 个,E = 5 个// 初始化一个Map,用于存储每个选项的计数Map<String, Integer> optionCount = new HashMap<>();QuestionVO questionVO = QuestionVO.objToVo(question);List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();// 遍历题目列表,遍历答案列表,遍历题目中的选项for (QuestionContentDTO questionContentDTO : questionContent) {for (String answer : choices) {for (QuestionContentDTO.Option option : questionContentDTO.getOptions()) {if (option.getKey().equals(answer)) {String result = option.getResult();// 如果result属性不在optionCount中,初始化为0if (!optionCount.containsKey(result)) {optionCount.put(result, 0);}// 在optionCount中增加计数optionCount.put(result, optionCount.get(result) + 1);}}}}// 3. 遍历每种评分结果,计算哪个结果的得分更高// 初始化最高分数和最高分数对应的评分结果int maxScore = 0;ScoringResult maxScoringResult = scoringResultList.get(0);// 遍历评分结果列表for (ScoringResult scoringResult : scoringResultList) {List<String> resultProp = JSONUtil.toList(scoringResult.getResultProp(), String.class);int score = resultProp.stream().mapToInt(prop -> optionCount.getOrDefault(prop, 0)).sum();if (score > maxScore) {maxScore = score;maxScoringResult = scoringResult;}}// 4. 构造返回值,填充答案对象的属性UserAnswer userAnswer = new UserAnswer();userAnswer.setAppId(appId); //赋值省略return userAnswer;}
}
/**
*自定义打分策略
*/
public class CustomScoreScoringStrategy implements ScoringStrategy {@Resourceprivate QuestionService questionService;@Resourceprivate ScoringResultService scoringResultService;@Overridepublic UserAnswer doScore(List<String> choices, App app) throws Exception {Long appId = app.getId();// 1. 根据 id 查询到题目和题目结果信息(按分数降序排序)Question question = questionService.getOne(Wrappers.lambdaQuery(Question.class).eq(Question::getAppId, appId));List<ScoringResult> scoringResultList = scoringResultService.list(Wrappers.lambdaQuery(ScoringResult.class).eq(ScoringResult::getAppId, appId).orderByDesc(ScoringResult::getResultScoreRange));// 2. 统计用户的总得分int totalScore = 0;QuestionVO questionVO = QuestionVO.objToVo(question);List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();// 遍历题目列表for (int i = 0; i < questionContent.size(); i++) {QuestionContentDTO questionContentDTO = questionContent.get(i);String answer = choices.get(i);// 遍历题目中的选项for (QuestionContentDTO.Option option : questionContentDTO.getOptions()) {// 如果答案和选项的key匹配if (option.getKey().equals(answer)) {int score = Optional.of(option.getScore()).orElse(0);totalScore += score;}}}// 3. 遍历得分结果,找到第一个用户分数大于得分范围的结果,作为最终结果ScoringResult maxScoringResult = scoringResultList.get(0);for (ScoringResult scoringResult : scoringResultList) {if (totalScore >= scoringResult.getResultScoreRange()) {maxScoringResult = scoringResult;break;}}// 4. 构造返回值,填充答案对象的属性UserAnswer userAnswer = new UserAnswer();userAnswer.setAppId(appId); //赋值略return userAnswer;}
}
/**
*AI测评策略
*/
public class AiTestScoringStrategy implements ScoringStrategy {@Resourceprivate QuestionService questionService;@Resourceprivate AiManager aiManager;@Resourceprivate RedissonClient redissonClient;@Overridepublic UserAnswer doScore(List<String> choices, App app) throws Exception {Long appId = app.getId();String jsonStr = JSONUtil.toJsonStr(choices);String cacheKey = buildCacheKey(appId, jsonStr);String answerJson = answerCacheMap.getIfPresent(cacheKey);// 如果有缓存,直接返回if (StrUtil.isNotBlank(answerJson)) {// 构造返回值,填充答案对象的属性UserAnswer userAnswer = JSONUtil.toBean(answerJson, UserAnswer.class);userAnswer.setAppId(appId);userAnswer.setAppType(app.getAppType());userAnswer.setScoringStrategy(app.getScoringStrategy());userAnswer.setChoices(jsonStr);return userAnswer;}// 定义锁RLock lock = redissonClient.getLock(AI_ANSWER_LOCK + cacheKey);try {// 竞争锁boolean res = lock.tryLock(3, 15, TimeUnit.SECONDS);// 没抢到锁,强行返回if (!res) {return null;}// 抢到锁了,执行后续业务逻辑// 1. 根据 id 查询到题目Question question = questionService.getOne(Wrappers.lambdaQuery(Question.class).eq(Question::getAppId, appId));QuestionVO questionVO = QuestionVO.objToVo(question);List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();// 2. 调用 AI 获取结果// 封装 PromptString userMessage = getAiTestScoringUserMessage(app, questionContent, choices);// AI 生成String result = aiManager.doSyncStableRequest(AI_TEST_SCORING_SYSTEM_MESSAGE, userMessage);// 截取需要的 JSON 信息int start = result.indexOf("{");int end = result.lastIndexOf("}");String json = result.substring(start, end + 1);// 缓存结果answerCacheMap.put(cacheKey, json);// 3. 构造返回值,填充答案对象的属性UserAnswer userAnswer = JSONUtil.toBean(json, UserAnswer.class);userAnswer.setAppId(appId);userAnswer.setAppType(app.getAppType());userAnswer.setScoringStrategy(app.getScoringStrategy());userAnswer.setChoices(jsonStr);return userAnswer;} finally {if (lock != null && lock.isLocked()) {if (lock.isHeldByCurrentThread()) {lock.unlock();}}}}
}
以上是三种继承策略接口的doScore实现类
全局执行器
为了简化外部调用,需要根据不同的应用类别和评分策略,选择对应的策略执行,因此需要一个全局执行器。
2 种实现方式:
1)编程式,在内部计算选用何种策略:
@Service
@Deprecated
public class ScoringStrategyContext {@Resourceprivate CustomScoreScoringStrategy customScoreScoringStrategy;@Resourceprivate CustomTestScoringStrategy customTestScoringStrategy;/*** 评分*/public UserAnswer doScore(List<String> choiceList, App app) throws Exception {AppTypeEnum appTypeEnum = AppTypeEnum.getEnumByValue(app.getAppType());AppScoringStrategyEnum appScoringStrategyEnum = AppScoringStrategyEnum.getEnumByValue(app.getScoringStrategy());if (appTypeEnum == null || appScoringStrategyEnum == null) {throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");}// 根据不同的应用类别和评分策略,选择对应的策略执行switch (appTypeEnum) {case SCORE:switch (appScoringStrategyEnum) {case CUSTOM:return customScoreScoringStrategy.doScore(choiceList, app);case AI:break;}break;case TEST:switch (appScoringStrategyEnum) {case CUSTOM:return customTestScoringStrategy.doScore(choiceList, app);case AI:break;}break;}throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");}
}
优点是直观清晰,缺点是不利于扩展和维护。直接使用switch和case进行判断,适用于少量且类型不怎么更改的样式。
2)声明式,在每个策略类中通过接口声明对应的生效条件,适合比较规律的策略选取场景。
接口:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ScoringStrategyConfig {/*** 应用类型* @return*/int appType();/*** 评分策略* @return*/int scoringStrategy();
}
给策略实现类补充注解,将不同的策略实现类添加注解对应用类型和评分方式进行赋值:
@ScoringStrategyConfig(appType = 0, scoringStrategy = 0)
全局执行器
/*** 评分策略执行器*/
@Service
public class ScoringStrategyExecutor {// 策略列表@Resourceprivate List<ScoringStrategy> scoringStrategyList;/*** 评分*/public UserAnswer doScore(List<String> choiceList, App app) throws Exception {Integer appType = app.getAppType();Integer appScoringStrategy = app.getScoringStrategy();if (appType == null || appScoringStrategy == null) {throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");}// 根据注解获取策略for (ScoringStrategy strategy : scoringStrategyList) {if (strategy.getClass().isAnnotationPresent(ScoringStrategyConfig.class)) {ScoringStrategyConfig scoringStrategyConfig = strategy.getClass().getAnnotation(ScoringStrategyConfig.class);if (scoringStrategyConfig.appType() == appType && scoringStrategyConfig.scoringStrategy() == appScoringStrategy) {return strategy.doScore(choiceList, app);}}}throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");}
}
因为用了ScoringStrategyConfig注解,所以这个实现类被加上了component注解,因此可以被spring管理扫描到。然后@Resoure注入的时候,会通过ScoringStrategy类型找到所有实现ScoringStrategy接口的实现类
之后直接调用策略全局执行器即可调用不同doScore方法。
UserAnswer userAnswerWithResult = scoringStrategyExecutor.doScore(choices, app);
这样之后就算新增加应用类型和评分方式,我们代码中执行器和调用方法也不用修改,只需要添加继承策略接口的实现类,给策略实现类补充注解即可