利用OJ判题的多语言优雅解耦方法深入体会模板方法模式、策略模式、工厂模式的妙用

在线评测系统(Online Judge, OJ)的核心是判题引擎,其关键挑战在于如何高效、安全且可扩展地支持多种编程语言。在博主的项目练习过程中,借鉴了相关设计模式实现一种架构设计方案,即通过组合运用模板方法、策略、工厂等设计模式,将判题流程中与语言相关的逻辑进行深度解耦,从而构建一个符合“开闭原则”、易于维护和扩展的现代化判题引擎。

1. 问题域分析:判题引擎的复杂性

一个典型的判题流程包含以下阶段:

  1. 环境准备:创建隔离的执行环境(如 Docker 容器)。
  2. 代码编译:将源代码编译成可执行文件(非解释型语言)。
  3. 代码执行:在受限的环境中运行代码,并监控资源消耗。
  4. 结果收集:获取程序的输出、错误、执行时间及内存消耗。
  5. 环境清理:销毁执行环境,回收资源。

该流程的复杂性源于不同编程语言在“编译”和“执行”阶段的显著差异:

  • 编译型语言 (C++, Java): 需要特定的编译器和编译指令,生成中间产物(可执行文件或字节码)。
  • 解释型语言 (Python, JavaScript): 无需编译,直接通过解释器执行。
  • 运行参数: 不同语言的运行时(Runtime)在内存限制、安全策略等方面有不同的配置方式。

若采用过程式编程,通过大量的 if-elseswitch-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 语言的支持,只需完成两步:
      1. 创建一个 GoJudgeStrategy 类,继承 AbstractJudgeTemplate 并实现其 compilerun 方法。
      2. 为该类添加 @Component("go") 注解。
        系统即可自动集成新的语言支持,无需改动任何已有代码。
  • 职责单一: 每个类(模板、策略、工厂)的职责都非常明确,提升了代码的可读性和可维护性。

4. 结论

在复杂的系统设计中,直接的思考过程实现往往会导致僵化的、难以维护的系统。这时候不妨先对系统中每个类的职责先进行分析清楚,然后借助相关设计模式的思路,将业务逻辑进行解耦合,达到可拓展,可维护的系统架构。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/92696.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/92696.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[FOC电机控制]霍尔传感器于角度问题

如果电机有1对极(p1&#xff0c;那么每旋转一圈的机械角度&#xff0c;电气角度会转动一圈&#xff08;360&#xff09;。如果电机有2对极(p2&#xff0c;那么每旋转一圈的机械角度&#xff0c;电气角度会转动两圈&#xff08;720&#xff09;。

阿里云 Flink

阿里云 Flink 是阿里云基于Apache Flink打造的企业级实时计算平台&#xff0c;旨在为用户提供高效、稳定、易用的流处理与批处理能力&#xff0c;帮助企业快速构建实时数据处理链路&#xff0c;支撑实时业务决策。核心特性流批一体计算继承 Apache Flink “流批一体” 的核心优…

企业级高性能web服务器

1 web服务基础 1.1 正常情况的单次web服务访问流程&#xff1a; 正常情况下&#xff0c;单次 Web 服务访问流程从用户在客户端发起请求开始&#xff0c;到最终在客户端展示内容结束&#xff0c;涉及客户端、网络传输、服务器端等多个环节&#xff0c;以下是详细过程&#xff…

免费PDF编辑软件 pdf24-creator 及其安装包

最近发现了一款还算是不错的PDF编辑和阅读软件 pdf24-creator&#xff0c;官方下载网站为&#xff1a;https://tools.pdf24.org/zh/creator&#xff0c;但是官方下载如果没有魔法的话&#xff0c;下载速度很慢&#xff0c;比百度网盘下载还满&#xff0c;因此我把它分享到网盘。…

openvela之ADB

ADB&#xff08;Android Debug Bridge&#xff09;是一款功能丰富的命令行工具&#xff0c;旨在实现开发工作站与设备&#xff08;如模拟器、实体设备&#xff09;之间的通信。通过 ADB&#xff0c;开发者可以便捷地在设备上执行命令、传输文件、调试应用等。本文将详细介绍 AD…

如何控制需求交付节奏

有效控制需求的交付节奏&#xff0c;其核心在于将产品开发过程从一个不可预测的、时快时慢的混乱状态&#xff0c;转变为一套产出稳定、流程顺畅、步调可持续的系统化交付机制。要成功构建这套机制&#xff0c;实现有节奏的价值交付&#xff0c;必须综合运用五大关键策略&#…

汇编中常用寄存器介绍

X86-32位寄存器 4个数据寄存器&#xff1a;EAX、EBX、ECX和EDX; 2个变址和指针寄存器&#xff1a;ESI和EDI; 2个指针寄存器&#xff1a;ESP和EBP; 1个指令指针寄存器&#xff1a;EIP; 6个段寄存器&#xff1a;ES、CS、SS、DS、FS和GS; 1个标志寄存器&#xff1a;EFlags。 在X8…

SOMGAN:用自组织映射改善GAN的模式探索能力

论文信息 论文题目:Improving mode exploring capability ofgenerative adversarial nets by self-organizing map(利用自组织映射提高生成对抗网络的模式探索能力) 期刊:Neurocomputing 摘要:生成对抗网络(GANs)的出现将生成模型的研究推向了一个新的高潮。支持这一进步…

《汇编语言:基于X86处理器》第12章 复习题和练习

本篇记录了《汇编语言&#xff1a;基于X86处理器》第12章 复习题和练习的笔记。12.6复习题和练习12.6.1 简答题1.假设有二进制浮点数1101.01101&#xff0c;如何将其表示为十进制分数之和?答&#xff1a;1101.01101(1x)(1x)(0x)(1x)(0x)(1x)(1x)(1x)(1x) 13.406252.为什么十进…

ApacheCon Asia 2025 中国开源年度报告:Apache Doris 国内第一

上周刚落下帷幕的 ApacheCon Asia 2025 中&#xff0c;一个数据让所有人都为之震撼&#xff1a;全球 Apache 基金会项目 OpenRank 排行榜中&#xff0c;Apache Doris 位居第二&#xff0c;在中国 Apache 项目中更是稳居第一。 这个排名意味着什么&#xff1f;在 Apache 基金会管…

Pytest中实现自动生成测试用例脚本代码

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快在Python的测试框架中&#xff0c;我们通常会针对某个系统进行测试用例的维护&#xff0c;在对庞大系统进行用例维护时&#xff0c;往往会发现很多测试用例是差不多…

一周学会Matplotlib3 Python 数据可视化-标注 (Annotations)

锋哥原创的Matplotlib3 Python数据可视化视频教程&#xff1a; 2026版 Matplotlib3 Python 数据可视化 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 课程介绍 本课程讲解利用python进行数据可视化 科研绘图-Matplotlib&#xff0c;学习Matplotlib图形参数基本设置&…

安全合规1--实验:ARP欺骗、mac洪水攻击、ICMP攻击、TCP SYN Flood攻击

一、实验环境 (思科的云实验平台)攻击机&#xff1a;Kali Linux&#xff08;IP&#xff1a;192.168.234.128&#xff0c;MAC&#xff1a;00:00:29:35:64:EC&#xff09;目标1&#xff1a;网关&#xff08;IP&#xff1a;192.168.234.2&#xff0c;MAC&#xff1a;00:50:56:ED:D…

Linux下GCC的C++实现Hive到Snowflake数据迁移

程序结构 ├── main.cpp ├── config.json ├── hive_export/ ├── parquet_data/ ├── sql_scripts/ └── logs/核心代码实现 (main.cpp) #include <iostream> #include <fstream> #include <vector> #include <thread> #include <mut…

drippingblues靶机教程

一、信息搜集首先将其在VirtualBOX中安装&#xff0c;并将kali与靶机都设置为桥接模式紧接着我们扫描IP&#xff0c;来发现靶机地址&#xff0c;经过搜集&#xff0c;发现IP是192.168.1.9&#xff0c;我们去访问一下紧接着我们扫一下开放了哪些端口。发现开放了21、22以及80端口…

39.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--调整发布脚本

这篇文章&#xff0c;我们要调整发布脚本。之所以要调整发布脚本&#xff0c;是因为现在我们的项目有三个环境&#xff1a;本地&#xff08;Local&#xff09;、开发&#xff08;Development&#xff09;、生产&#xff08;Production&#xff09;。Tip&#xff1a;我们的项目虽…

商品、股指及ETF期权五档盘口Tick级与分钟级历史行情数据多维解析

在金融数据分析领域&#xff0c;本地CSV文件是存储高频与低频数据的常用载体。本文以期权市场数据为例&#xff0c;探讨如何基于CSV格式处理分钟级行情、高频Tick数据、日频数据、逐笔委托记录、五档订单簿及历史行情数据&#xff0c;并提供专业的技术实现方案。以下将从数据预…

云端软件工程智能代理:任务委托与自动化实践全解

云端软件工程智能代理&#xff1a;任务委托与自动化实践全解 背景与未来趋势 随着软件工程复杂度不断提升&#xff0c;开发者对自动化工具的依赖也日益增强。我们正进入一个“人机协作”的新时代&#xff0c;开发者可以专注于核心创新&#xff0c;将重复性、繁琐的任务委托给智…

making stb style lib(1): do color print in console

col.h: see origin repo // origin repo: https://github.com/resyfer/libcol #ifndef _COL_HOL_H_ #define _COL_HOL_H_#include <stdlib.h> #include <stdio.h> #include <stdbool.h> #include <string.h> #include <math.h> // 新增&#xf…

llm本地部署+web访问+交互

要实现基于llm的web访问和交互&#xff0c;需支持对llm的访问和对网络搜索的调用。 这里使用ollama llm兼容openai sdk访问&#xff1b;使用proxyless-llm-websearch模拟网络搜索。 1 ollama本地部署 假设ollama已经部署&#xff0c;具体过程参考 在mac m1基于ollama运行dee…