利用OJ判题的多语言优雅解耦方法深入体会模板方法模式、策略模式、工厂模式的妙用
在线评测系统(Online Judge, OJ)的核心是判题引擎,其关键挑战在于如何高效、安全且可扩展地支持多种编程语言。在博主的项目练习过程中,借鉴了相关设计模式实现一种架构设计方案,即通过组合运用模板方法、策略、工厂等设计模式,将判题流程中与语言相关的逻辑进行深度解耦,从而构建一个符合“开闭原则”、易于维护和扩展的现代化判题引擎。
1. 问题域分析:判题引擎的复杂性
一个典型的判题流程包含以下阶段:
- 环境准备:创建隔离的执行环境(如 Docker 容器)。
- 代码编译:将源代码编译成可执行文件(非解释型语言)。
- 代码执行:在受限的环境中运行代码,并监控资源消耗。
- 结果收集:获取程序的输出、错误、执行时间及内存消耗。
- 环境清理:销毁执行环境,回收资源。
该流程的复杂性源于不同编程语言在“编译”和“执行”阶段的显著差异:
- 编译型语言 (C++, Java): 需要特定的编译器和编译指令,生成中间产物(可执行文件或字节码)。
- 解释型语言 (Python, JavaScript): 无需编译,直接通过解释器执行。
- 运行参数: 不同语言的运行时(Runtime)在内存限制、安全策略等方面有不同的配置方式。
若采用过程式编程,通过大量的 if-else
或 switch-case
语句来处理不同语言,将导致代码结构僵化、维护成本高昂,每新增一门语言都可能引发对核心代码的大规模修改,违背了软件设计的 “开闭原则“ (Open-Closed Principle)。
2. 架构设计:设计模式的组合应用
为了应对上述挑战,我们需要采用一系列设计模式来重构判题引擎,将流程中的“不变”与“可变”部分分离。(对于一些生产级别的安全配置本文进行忽略,着重于设计模式的使用)对于实现多语言的容器池参考:[[j借助线程池的思想,构建一个高性能、配置驱动的Docker容器池]])
2.1. 模板方法模式:定义流程骨架
模板方法模式是整个架构的基石。我们定义一个抽象基类 AbstractJudgeTemplate
,它封装了判题流程的固定算法骨架。
@Component
public abstract class AbstractJudgeTemplate { @Autowired protected MultiLanguageDockerSandBoxPool sandBoxPool; // 模板方法,定义了判题的完整流程骨架 public final SandBoxExecuteResult judge(String userCode, List<String> inputList,Long timeLimit) { // 1. 准备环境 String containerId = prepareEnvironment(); // 2. 创建用户代码文件 String userCodePath = createUserCodePath(containerId); File userCodeFile = createUserCodeFile(userCode, userCodePath); try { // 3. 编译代码 CompileResult compileResult = compileCodeByDocker(containerId, userCodePath); // 传递所需参数 if (!compileResult.isCompiled()) { // 如果编译失败,也需要清理文件和容器 return SandBoxExecuteResult.fail(CodeRunStatus.COMPILE_FAILED, compileResult.getExeMessage()); } // 4. 运行代码 return executeCodeByDocker(containerId, inputList,timeLimit); // 传递所需参数 } catch (SecurityException e) {log.error("代码安全检查失败: {}", e.getMessage());return SandBoxExecuteResult.fail(CodeRunStatus.SECURITY_ERROR, "代码包含不安全内容");} catch (ContainerNotAvailableException e) {log.error("容器资源不足: {}", e.getMessage());judgeMetrics.recordContainerError(getLanguageType());return SandBoxExecuteResult.fail(CodeRunStatus.SYSTEM_ERROR, "系统资源不足,请稍后重试");} catch (Exception e) {log.error("判题过程发生异常", e);judgeMetrics.recordSystemError(getLanguageType());return SandBoxExecuteResult.fail(CodeRunStatus.SYSTEM_ERROR, "系统内部错误");} finally { // 5. 清理环境 deleteUserCodeFile(userCodeFile); cleanupEnvironment(containerId); } } private void deleteUserCodeFile(File userCodeFile) { if (userCodeFile != null && userCodeFile.exists()) { FileUtil.del(userCodeFile); } } /** * 创建用户代码文件 */ private File createUserCodeFile(String userCode, String userCodePath) { if (FileUtil.exist(userCodePath)) { FileUtil.del(userCodePath); } return FileUtil.writeString(userCode, userCodePath, Constants.UTF8); } private void cleanupEnvironment(String containerId) { // 只有在 containerId 有效时才归还 ,还可以进行其他校验if (containerId != null) { sandBoxPool.returnContainer(containerId); } }//安全检查的相关方法省略... // --- 抽象方法 (钩子),由子类实现 --- /** * 编译代码,不同语言实现不同 */ protected abstract CompileResult compileCodeByDocker(String containerId, String userCodePath); /** * 运行代码,不同语言的运行命令和参数不同*/ protected abstract SandBoxExecuteResult executeCodeByDocker(String containerId, List<String> inputList,Long timeLimit); /** * 准备环境 * @return 容器id */ protected abstract String prepareEnvironment(); protected abstract String createUserCodePath(String containerId);
}
通过这种方式,AbstractJudgeTemplate
定义了“做什么”(判题流程),而将“怎么做”(具体语言的编译和运行)的责任下放给了子类。
2.2. 策略模式:封装语言特定逻辑
每个具体的语言实现都可以看作是一种独立的“策略”。我们为每种支持的语言创建一个继承自 AbstractJudgeTemplate
的具体类。
Java 策略实现:
@Service("java")
public class JavaJudgeStrategy extends AbstractJudgeTemplate { //与docker操作的对象,也可以进行二次封装进行对上层提高封装后的api@Autowired private DockerClient dockerClient; @Autowiredprivate LanguageProperties languageProperties;@Override protected CompileResult compileCodeByDocker(String containerId, String userCodePath) { // 从配置中获取编译命令String compileCmd = languageProperties.getJava().getCompileCmd();// 使用该命令在容器中执行编译...log.info("Executing compile command: {}", compileCmd);// ... 省略与沙箱交互的底层代码return CompileResult.success();} @Override protected SandBoxExecuteResult executeCodeByDocker(String containerId, List<String> inputList,Long timeLimit) { // 从配置中获取运行命令String executeCmd = languageProperties.getJava().getExecuteCmd();List<String> outputList = new ArrayList<>();for (String input : context.getInputList()) {// 拼接输入参数并执行...log.info("Executing run command: {} with input: {}", executeCmd, input);// ... 省略与沙箱交互的底层代码}//封装结果 return getSanBoxResult(inputList, outList, maxMemory, maxUseTime); } @Override protected String prepareEnvironment() { return sandBoxPool.getContainer(ProgramType.JAVA); } @Override protected String createUserCodePath(String containerId) { String codeDir = sandBoxPool.getHostCodeDir(containerId); return codeDir + File.separator + //从配置中读取也可以JudgeConstants.USER_CODE_JAVA_CLASS_NAME; }//其他方法这里忽略
}
Python 策略实现:
@Service("python3")
public class PythonJudgeStrategy extends AbstractJudgeTemplate {@Autowiredprivate LanguageProperties languageProperties;@Overrideprotected CompileResult compileCodeByDocker(String containerId, String userCodePath) {// 解释型语言,编译步骤为空实现,直接返回成功return CompileResult.success();}@Overrideprotected SandBoxExecuteResult executeCodeByDocker(String containerId, List<String> inputList,Long timeLimit) {// env.executeCommand(RUN_CMD)// ... 返回运行结果}//其他重写方法
}
现在,每种语言的判题逻辑被隔离在独立的策略类中,实现了高度的内聚和解耦。
2.3. 工厂模式:动态选择策略
有了各种策略,我们需要一个机制来根据客户端请求(例如,任务中指定的语言)动态地选择并实例化正确的策略。工厂模式是解决此问题的理想选择。
结合 Spring 框架的依赖注入(DI),可以实现一个高效的策略工厂。
@Component
public class JudgeStrategyFactory {private final Map<String, AbstractJudgeTemplate> strategyMap;/*** 利用 Spring 的构造函数注入,自动将所有 AbstractJudgeTemplate 类型的 Bean 注入。* Key 为 Bean 的名称 (e.g., "java", "python3"),Value 为 Bean 实例。*/@Autowiredpublic JudgeStrategyFactory(Map<String, AbstractJudgeTemplate> strategyMap) {this.strategyMap = strategyMap;}public AbstractJudgeTemplate getStrategy(String language) {AbstractJudgeTemplate strategy = strategyMap.get(language);if (strategy == null) {//可以自定义抛出业务异常throw new ServiceException(ResultCode.FAILED_NOT_SUPPORT_PROGRAM);}return strategy;}
}
客户端调用:
@Service
@Slf4j
public class JudgeServiceImpl implements IJudgeService { @Autowired private JudgeStrategyFactory judgeStrategyFactory; @Autowired private UserSubmitMapper userSubmitMapper; @Override public UserQuestionResultVO doJudgeJavaCode(JudgeSubmitDTO judgeSubmitDTO) { //获取判题策略对象 AbstractJudgeTemplate strategy = judgeStrategyFactory.getStrategy(judgeSubmitDTO.getProgramType().getDesc()); //调用容器池进行判题 SandBoxExecuteResult sandBoxExecuteResult = strategy.judge(judgeSubmitDTO.getUserCode(), judgeSubmitDTO.getInputList(),judgeSubmitDTO.getTimeLimit()); UserQuestionResultVO userQuestionResultVO = new UserQuestionResultVO(); //返回判题结果 //成功 //...相关判断//失败 //...相关判断//存储用户代码数据到数据库 log.info("判题逻辑结束,判题结果为: {} ", userQuestionResultVO.getPass()); return userQuestionResultVO; }
}
3. 架构优势与可扩展性
通过上述设计模式的组合应用,我们构建了一个结构清晰、易于扩展的判题引擎:
- 高内聚,低耦合: 每种语言的实现细节被封装在各自的策略类中,与主流程和其他语言实现完全解耦。
- 符合开闭原则:
- 对修改关闭: 核心判题流程
AbstractJudgeTemplate
和调度器JudgeDispatcherService
无需任何修改。 - 对扩展开放: 若要新增对 Go 语言的支持,只需完成两步:
- 创建一个
GoJudgeStrategy
类,继承AbstractJudgeTemplate
并实现其compile
和run
方法。 - 为该类添加
@Component("go")
注解。
系统即可自动集成新的语言支持,无需改动任何已有代码。
- 创建一个
- 对修改关闭: 核心判题流程
- 职责单一: 每个类(模板、策略、工厂)的职责都非常明确,提升了代码的可读性和可维护性。
4. 结论
在复杂的系统设计中,直接的思考过程实现往往会导致僵化的、难以维护的系统。这时候不妨先对系统中每个类的职责先进行分析清楚,然后借助相关设计模式的思路,将业务逻辑进行解耦合,达到可拓展,可维护的系统架构。