设计模式:GoF 在判题平台的实战
这一课你将能做到
- 说出 23 个 GoF 模式的三大分类(创建型、结构型、行为型),并举出每类在判题平台的代表实现
- 用工厂、策略、观察者、模板方法、责任链五个高频模式改造判题平台核心流程
- 判断"这里该不该上模式",避免过度设计
为什么要学
设计模式是前人踩坑后总结的"可复用解决方案"。判题平台里,提交校验、多语言判题、结果通知、限流熔断,每一步都对应一个经典模式。学会模式不是背类图,而是看到问题时能立刻反应:"这是策略模式该上的地方。"
先建立图像
创建型 结构型 行为型
───────── ───────── ─────────
工厂 适配器 策略
建造者 装饰器 观察者
原型 代理 模板方法
单例 门面 责任链
命令
跟着做
步骤 1:工厂模式——创建不同语言的判题器
public interface JudgeEngine {
JudgeResult judge(Submission submission);
}
@Component
public class JudgeEngineFactory {
private final Map<String, JudgeEngine> engines;
public JudgeEngineFactory(List<JudgeEngine> engineList) {
this.engines = engineList.stream()
.collect(Collectors.toMap(JudgeEngine::language, Function.identity()));
}
public JudgeEngine create(String language) {
JudgeEngine engine = engines.get(language.toLowerCase());
if (engine == null) throw new IllegalArgumentException("不支持的语言: " + language);
return engine;
}
}
工厂隐藏了"到底 new 哪个实现"的细节,调用方只说"我要 Java 判题器"。
步骤 2:策略模式——多种限流算法
public interface RateLimitStrategy {
boolean tryAcquire(String key);
}
@Component
public class TokenBucketStrategy implements RateLimitStrategy {
public boolean tryAcquire(String key) { /* 令牌桶实现 */ }
}
@Component
public class SlidingWindowStrategy implements RateLimitStrategy {
public boolean tryAcquire(String key) { /* 滑动窗口实现 */ }
}
@Service
public class SubmitRateLimiter {
private final RateLimitStrategy strategy;
public SubmitRateLimiter(@Qualifier("tokenBucket") RateLimitStrategy strategy) {
this.strategy = strategy;
}
public boolean allow(long userId) {
return strategy.tryAcquire("submit:" + userId);
}
}
限流算法可插拔,改配置就换实现,业务代码不动。
步骤 3:观察者模式——判题结果通知
public interface JudgeResultListener {
void onJudgeFinished(Submission submission);
}
@Component
public class WebSocketNotifyListener implements JudgeResultListener {
public void onJudgeFinished(Submission s) { /* 推送 WebSocket */ }
}
@Component
public class PracticeRecordListener implements JudgeResultListener {
public void onJudgeFinished(Submission s) { /* 更新 cf_practice_record */ }
}
@Service
public class JudgeEventPublisher {
private final List<JudgeResultListener> listeners;
public JudgeEventPublisher(List<JudgeResultListener> listeners) {
this.listeners = listeners;
}
public void publish(Submission submission) {
listeners.forEach(l -> l.onJudgeFinished(submission));
}
}
判题完成是"事件",监听者各自处理,publisher 不知道也不关心有多少监听者。
步骤 4:模板方法——判题流程骨架
public abstract class AbstractJudgeEngine implements JudgeEngine {
@Override
public final JudgeResult judge(Submission submission) {
preCheck(submission);
CompileResult compile = doCompile(submission);
if (!compile.success()) return JudgeResult.compileError(compile.error());
RunResult run = doRun(compile, submission);
return postProcess(run);
}
protected abstract void preCheck(Submission s);
protected abstract CompileResult doCompile(Submission s);
protected abstract RunResult doRun(CompileResult compile, Submission s);
protected abstract JudgeResult postProcess(RunResult run);
}
@Component
public class JavaJudgeEngine extends AbstractJudgeEngine {
protected void preCheck(Submission s) { /* Java 特有检查 */ }
protected CompileResult doCompile(Submission s) { /* javac */ }
protected RunResult doRun(CompileResult c, Submission s) { /* java 运行 */ }
protected JudgeResult postProcess(RunResult run) { /* 对比输出 */ }
}
模板方法把"判题流程"固定,把"具体实现"留给子类。Java 和 Python 判题器共用骨架。
步骤 5:责任链——提交前置校验
public abstract class SubmissionValidator {
private SubmissionValidator next;
public SubmissionValidator linkWith(SubmissionValidator next) {
this.next = next; return next;
}
public ValidationResult validate(Submission s) {
ValidationResult r = doValidate(s);
if (!r.ok() || next == null) return r;
return next.validate(s);
}
protected abstract ValidationResult doValidate(Submission s);
}
@Component
public class AuthValidator extends SubmissionValidator {
protected ValidationResult doValidate(Submission s) {
return s.getUserId() == null ? ValidationResult.fail("未登录") : ValidationResult.ok();
}
}
@Component
public class CodeValidator extends SubmissionValidator {
protected ValidationResult doValidate(Submission s) {
return s.getCode().isBlank() ? ValidationResult.fail("代码为空") : ValidationResult.ok();
}
}
校验器像链条一样串起来,每个节点独立,可插拔、可复用。
讲清楚要点
- 模式是"问题-解决方案"对:不是背类图,是看到问题能想到模式。
- 创建型管对象怎么生:工厂、建造者、单例、原型。
- 结构型管对象怎么组:适配器、装饰器、代理、门面。
- 行为型管对象怎么协作:策略、观察者、模板方法、责任链、命令。
- Spring 让模式零成本:
@Component + List<Interface> 注入,策略和观察者几乎不用手动维护注册表。
- 模式不是银弹:能
if-else 解决的别上策略;能直接调用的别上观察者。复杂度要匹配问题规模。
你可能会懵的地方
- 策略和工厂怎么一起用?
工厂负责"创建",策略负责"选择"。判题平台里,工厂创建具体语言的
JudgeEngine,策略模式让 JudgeEngine 本身可替换。两者经常搭档。
- 观察者模式和消息队列什么关系?
观察者是进程内同步调用,消息队列是跨进程异步。判题结果先用观察者同步更新本地状态,再发 MQ 异步通知其他服务,两者互补。
- 模板方法和策略模式怎么选?
模板方法是"流程固定、步骤可变"(继承);策略是"算法整体替换"(接口)。判题流程固定用模板方法,限流算法整体替换用策略。
- 责任链和拦截器/过滤器什么区别?
责任链是"一个节点处理不了就传给下一个";拦截器是"前后都处理,可中断"。Spring 的
HandlerInterceptor 是拦截器,Servlet Filter 是过滤器,都算责任链的变体。
- 23 个模式都要会吗?
不需要。面试和实战高频的是:工厂、策略、观察者、模板方法、责任链、单例、代理、适配器。其他的了解概念即可。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 所有逻辑塞一个 Service | 违反 SRP,难测难改 | 拆成策略、观察者、责任链 |
new JavaJudgeEngine() 硬编码 | 无法替换实现 | 工厂 + Spring 注入 |
| 判题完成直接调 5 个 service | 高耦合 | 观察者发布事件,监听者各自处理 |
| 每个语言复制粘贴判题流程 | 重复代码 | 模板方法固定骨架,子类填差异 |
| 校验规则写死在 Service 里 | 改规则要改核心代码 | 责任链,每个校验独立 |
卡点急救
- 不知道用哪个模式:画个流程图,看变化点在哪——创建变化用工厂,算法变化用策略,流程变化用模板,通知变化用观察者。
- Spring 里模式写多了类爆炸:用
@Configuration + @Bean 集中注册,或用 @ComponentScan 自动扫描;接口 + List 注入让 Spring 帮你组装。
- 模式叠加导致过度设计:问"这个扩展点未来 6 个月会变吗?"不会就保持简单;会就先用最简单的模式(策略/观察者),别一次上全套。
面试常问
- 工厂模式和策略模式区别?
答:工厂管"怎么创建对象"(生产),策略管"怎么替换算法"(使用)。判题平台里,工厂创建
JudgeEngine,策略让 RateLimitStrategy 可插拔。工厂通常配合策略一起用。
- 观察者模式在 Spring 里怎么实现?
答:Spring 的
ApplicationEvent + @EventListener 就是观察者。判题完成发 SubmissionJudgedEvent,监听者用 @EventListener 处理。也可以用 List<Listener> 手动注入,更轻量。
- 模板方法模式在 JDK 或 Spring 里的例子?
答:
AbstractApplicationContext 的 refresh() 是模板方法,onRefresh() 留给子类;JdbcTemplate 的 execute() 是模板方法,doInStatement() 留给回调。判题平台的 AbstractJudgeEngine 同理。
自测三问
- 判题平台要支持"提交前进行用户限流",应该用什么模式?为什么不用 if-else?
- 判题完成后要同时更新练习记录、发 WebSocket 通知、写审计日志,应该用什么模式?
- 新增"Go 语言判题"时,工厂、策略、模板方法各自怎么配合?
小结一句话
设计模式是"变化点"的封装术——把会变的创建、算法、流程、通知抽出来,让不变的骨架稳定。
延伸