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

发布于:2025-08-07 ⋅ 阅读:(12) ⋅ 点赞:(0)

在线评测系统(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;  

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

4. 结论

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


网站公告

今日签到

点亮在社区的每一天
去签到