云计算百科
云计算领域专业知识百科平台

利用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;

    @Autowired
    private 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 {

    @Autowired
    private LanguageProperties languageProperties;

    @Override
    protected CompileResult compileCodeByDocker(String containerId, String userCodePath) {
    // 解释型语言,编译步骤为空实现,直接返回成功
    return CompileResult.success();
    }

    @Override
    protected 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 实例。
    */

    @Autowired
    public 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. 结论

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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 利用OJ判题的多语言优雅解耦方法深入体会模板方法模式、策略模式、工厂模式的妙用
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!