项目背景:
首先这篇文章是总结了OJ项目和AI答题平台项目(和一点点的聚合搜索项目)中设计模式的文章
在项目中也用了很多次的设计模式,我感觉起来,这些设计模式的作用就是提高项目的扩展性和降低耦合性
工厂模式:
设计模式(工厂模式,模板方法模式,单例模式)_工厂方法模式与模板方法模式-CSDN博客
关于工厂模式的介绍,贴一篇文章,里面有跑一些demo
OJ项目工厂模式
直接开始对OJ项目开始记录:
首先我们这里有一个沙箱的接口,并且三个实现类
分别对应
实例代码沙箱(纯属给自己测试用的)
远程调用代码沙箱(项目的核心流程)
第三方代码沙箱(可接入第三方的代码沙箱)
这三个沙箱都需要去实现这个沙箱接口。
接着我们循序渐进,我们看第一版的调用沙箱的代码:
这里先粘贴一张自己的工厂模式的文章中的一张图片
对应到OJ项目中
/**
* 第一版代码
* 最朴实无华的自己去创建对应沙箱的实体类
*/
public static void firstCode(String[] args) {
CodeSandBox codeSandBox = new RemoteCodeSandBox();
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code("System.out.println(\"hello world\");")
.language("java")
.inputList(new ArrayList<>())
.build();
ExecuteResponse executeResponse = codeSandBox.executeCode(executeCodeRequest);
System.out.println(executeResponse);
}
看到代码中我们自己想要什么沙箱,就自己去new
这样肯定很麻烦嘛,所以我们进行优化:
根据这个demo所说,我们只需要关系我们需要什么类型,往里传一个type即可
这个工厂就会自动帮我们把这个对象new出来,给我们,我们只需要用每个产品都是实现的接口来接收。
@Value("${codeSandBox.type:example}")
private String type;
/**
* 第二版代码
* 使用工厂模式,根据语言创建对应的沙箱实体类
* 这里其实还用了一个注册器模式(单例模式)在Bean对象创建之后将对应的实体类加入到map中,直接调用
* 第三版代码
* 参数配置化,将这个type配置到yml文件中,使用者直接修改即可
* @Value 注解不能用于 static 字段
*/
public void ThirdCode() {
Map<String, CodeSandBox> codeSandboxMap = new HashMap<>();
codeSandboxMap.put("example",new ExampleCodeSandBox());
codeSandboxMap.put("remote",new RemoteCodeSandBox());
codeSandboxMap.put("thirdParty",new ThirdPartyCodeSandBox());
System.out.println(type);
CodeSandBox codeSandBox = codeSandboxMap.get(type);
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code("System.out.println(\"hello world\");")
.language("java")
.inputList(new ArrayList<>())
.build();
ExecuteResponse executeResponse = codeSandBox.executeCode(executeCodeRequest);
System.out.println(executeResponse);
}
这里有注意点:
将这个type可以配置到yml文件中,这也是让别人更好调用我们这个开源项目的小技巧
代码中还缓存了一个map,这一段可以放到一个类中,在Bean对象都加载之后再缓存
这个在聚合搜索文章有讲聚合平台项目优化(门面模式,适配器模式,注册器模式)_聚合平台适配器-CSDN博客
通过上面的操作,我们就可以不用自己去new对应的沙箱对象了
只需要关心我们的type就行。
代理模式:
这里OJ项目还用到了一个代理模式对调用代码沙箱这个逻辑进行增强
在调用之前和调用之后都打上了日志
这里的这个代理模式很好理解:就是和AOP一起理解即可
具体代码实现:
代理类:
/**
* 代码沙箱代理
* 这里的代理听起来很复杂,实现起来就是写一个类.
* 调用代码沙箱的用户不需要自己去手动打日志,只需要去调用这个代理类即可
* 代理类会自动打日志
* 说到代理类,就经典的案例就是房屋中介,房屋中介就是代理类,租房子的人不需要自己去找房子,只需要去找中介即可
*/
@Slf4j
@AllArgsConstructor
@NoArgsConstructor
public class CodeSandBoxProxy implements CodeSandBox {
private CodeSandBox codeSandBox;
@Override
public ExecuteResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
log.info("代码沙箱执行前");
ExecuteResponse executeResponse = codeSandBox.executeCode(executeCodeRequest);
log.info("代码沙箱执行后");
return executeResponse;
}
}
工厂类:
@Component
public class CodeSandFactory {
private Map<String, CodeSandBox> codeSandboxMap = new HashMap<>();
@PostConstruct
private void init() {
codeSandboxMap.put("example",new ExampleCodeSandBox());
codeSandboxMap.put("remote",new RemoteCodeSandBox());
codeSandboxMap.put("thirdParty",new ThirdPartyCodeSandBox());
}
public CodeSandBox getCodeSandBox(String type) {
return codeSandboxMap.get(type);
}
}
这里就把每个沙箱缓存到一个map中
具体调用代码:
/**
* 第四版代码
* 利用代理模式对调用沙箱功能进行优化
* 调用前和调用后自动打上日志
*/
@Test
void ForthCode() {
CodeSandBox codeSandBox = codeSandFactory.getCodeSandBox(type);
System.out.println(type);
CodeSandBoxProxy codeSandBoxProxy = new CodeSandBoxProxy(codeSandBox);
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code("System.out.println(\"hello world\");")
.language("java")
.inputList(new ArrayList<>())
.build();
ExecuteResponse executeResponse = codeSandBoxProxy.executeCode(executeCodeRequest);
System.out.println(executeResponse);
}
AI答题平台项目工厂模式
这里的代码逻辑就会更复杂一点。可以将这里的一个一个策略类看成一个个对象,你只需要传app进来,我就读取你的app中的两个字段,就可以判断出你给你这个app判题的是那种评分策略(AI评分,自定义评分,得分评分)
这里代码复杂得原因就是涉及到了Java中的注解和反射的使用
首先还是定义一个策略接口:
public interface ScoringStrategy {
public UserAnswer doScoring(List<String> choices, App app);
}
后面的三个策略都需要去实现这个接口
定义注解类:
这段代码定义了一个名为 ScoringStrategyConfig
的 Java 注解(Annotation)。注解是一种元数据,用于为代码元素(如类、方法、变量等)提供额外的信息。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ScoringStrategyConfig {
/**
* 应用类型
* @return
*/
int appType();
/**
* 评分策略
* @return
*/
int scoringStrategy();
}
这个注解上还有两个注解:
@Target(ElementType.TYPE):指定这个注解的作用域:类,接口和枚举
@Retention(RetentionPolicy.RUNTIME) 指定了注解在运行时可用,
在自己的每个策略上加入注解,并指定应用类型和评分策略
工厂类实现:
@Service
public class ScoringStrategyExecutor {
@Resource
private List<ScoringStrategy> strategies;
public UserAnswer doScore(List<String> choices, App app) {
final Integer appType = app.getAppType();
final AppTypeEnum enumByValue = AppTypeEnum.getEnumByValue(appType);
ThrowUtils.throwIf((enumByValue == null||appType==null), ErrorCode.PARAMS_ERROR,"appType参数错误");
for (ScoringStrategy strategy : strategies) {
if(strategy.getClass().isAnnotationPresent(ScoringStrategyConfig.class)){
ScoringStrategyConfig annotation = strategy.getClass().getAnnotation(ScoringStrategyConfig.class);
if(annotation.appType()==app.getAppType()&&annotation.scoringStrategy()==app.getScoringStrategy())
return strategy.doScoring(choices,app);
}
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用配置有误,未找到匹配的策略");
}
}
代码逻辑分析:
@Resource
private List<ScoringStrategy> strategies;
在你的代码中,你使用了 @Resource
注解来注入 List<ScoringStrategy>
。这个注解会自动扫描并注入所有实现了 ScoringStrategy
接口的 Bean。这意味着,只要你的 Spring 容器中存在实现了 ScoringStrategy
接口的 Bean,它们就会被自动注入到 strategies
列表中。
接着就是对传进来的app的内容进行判断
随后遍历这个 strategies
列表
if(strategy.getClass().isAnnotationPresent(ScoringStrategyConfig.class)){
这行代码检查当前策略类是否包含ScoringStrategyConfig
注解。isAnnotationPresent
方法用于判断某个类是否被指定注解修饰
判断你这个类是有这个注解的:
获取注解并检查条件:
ScoringStrategyConfig annotation = strategy.getClass().getAnnotation(ScoringStrategyConfig.class);
if(annotation.appType()==app.getAppType()&&annotation.scoringStrategy()==app.getScoringStrategy())
如果策略类包含ScoringStrategyConfig
注解,代码会获取该注解的实例,并检查注解中的appType
和scoringStrategy
属性是否与当前应用app
的类型和评分策略匹配。
最后就是执行这个具体的评分策略
return strategy.doScoring(choices, app);
小思考小总结
工厂模式就是简化你创建对象的这个过程
你不想自己去new对象,你就搞个"工厂",让这个工厂给你new对象
你只需要传值,你要说清楚你想要什么样的对象
那工厂怎么根据你传的值来new对应的对象呢?
最简单的办法就是在工厂类中写switch或者if-else
OJ项目就是用了一个注册器的设计模式,将所有沙箱对象缓存到map中
AI答题平台呢就是用了注解+放射的方式,在策略对象上+注解,最后遍历列表返回对应的策略对象
工厂模式和这个门面模式之间的一些思考
我在整理笔记的时候,感觉都是从前端传一个type,或者什么值巴拉巴拉的
然后简化过程
后面问了GPT,我的理解其实差不会特别大
工厂模式更注重对象的创建,你传我一个值,我还你一个对象
但是门面模式呢,更注重接口或者系统的调用,在聚合搜索中,我有三个接口,分别调用文章,用户,图片。
我前端传个参数,我后端就对应去调那个接口。
策略模式:
策略模式就是用来解决很多if else分支的问题
但是策略模式通常是要搭配一些其它东西才能比较好的使用
OJ项目策略模式应用:
为什么在OJ项目中要用策略模式:
我们知道,每个语言做相同的题目的耗时和空间都不一样,如果不指定的话,那对Java非常不公平
首先也是不变先来个策略接口:每种策略都需要实现这个接口
/**
* 判题策略接口
*/
public interface JudgeStrategy {
JudgeInfo doJudge(JudgeContext judgeContext);
}
下面只写了两个策略:默认编程语言策略和Java编程语言策略:
这里还有一个JudgeContext上下文这个类,用来(用于定义在策略中传递的参数)
package com.ljh.oj.judge.strategy;
import com.ljh.oj.judge.model.JudgeInfo;
import com.ljh.oj.model.entity.Question;
import com.ljh.oj.model.entity.QuestionSubmit;
import lombok.Builder;
import lombok.Data;
import java.util.List;
/**
* 上下文(用于定义在策略中传递的参数)
*/
@Data
@Builder
public class JudgeContext {
/**
* 这个是调用代码沙箱之后所返回的判题结果的信息
*/
private JudgeInfo judgeInfo;
/**
* 调用代码沙箱之后所返回的输出列表
*/
private List<String> outputList;
/**
* 题目信息
*/
private Question question;
/**
* 用户提交信息
* 主要作用就是获取当前提交的语言给JudgeManager使用
*/
private QuestionSubmit questionSubmit;
}
下面问题就来了:
我们定义好了策略,我们怎么来调用哪一种策略嘞?
首先我们要肯定知道从这个QuestionSubmit中来获取对应用户提交的语言
我们上面总结过:
注解啊,缓存一个map,或者在工厂类中写if-else
我们这里就采用if-else,另外两种方式都用过了
/**
* 判题模块简化封装
* 对doJudge方法进行更上一层封装
*/
@Service
public class JudgeManager {
public JudgeInfo doJudge(JudgeContext judgeContext){
String language = judgeContext.getQuestionSubmit().getLanguage();
JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
if(QuestionSubmitLanguageEnum.JAVA.getValue().equals(language)){
judgeStrategy = new JavaLanguageJudgeStrategy();
}
return judgeStrategy.doJudge(judgeContext);
}
}
这样就比较简单,只要在这个JudgeManager中添加代码即可
AI项目策略模式应用:
已经在上面都已经讲差不多了。