MogFace人脸检测模型WebUI项目重构:优化Java八股文中的设计模式应用
MogFace人脸检测模型WebUI项目重构:优化Java八股文中的设计模式应用
最近在重构一个集成了MogFace人脸检测模型的后台WebUI项目,发现很多同学在面试时能把设计模式背得滚瓜烂熟,但一到实际项目里,要么生搬硬套,要么干脆不用。其实,那些所谓的“Java八股文”设计模式,用对了地方,真的能让代码质量提升一个档次。
就拿这个项目来说,核心功能很简单:用户上传图片,后端调用MogFace模型进行人脸检测,返回检测结果(比如人脸位置、置信度)。但需求一复杂起来,比如要支持多种检测策略、结果需要异步通知多个模块、操作前后要加日志和监控,代码就开始变得混乱。这时候,合理运用几个常见的设计模式,就能让代码结构清晰、易于维护。
今天,我就以这个项目为例,聊聊怎么在实际编码中,把“八股文”里的设计模式用活,而不是背死。
1. 项目背景与痛点:为什么需要设计模式?
最开始,我们的代码写在一个大Controller里,大概长这样:
@RestController @RequestMapping("/face") public class FaceDetectionController { @Autowired private MogFaceService mogFaceService; @Autowired private NotificationService notificationService; @Autowired private MetricsService metricsService; @PostMapping("/detect") public ApiResponse detect(@RequestParam("image") MultipartFile file, @RequestParam(value = "mode", defaultValue = "standard") String mode) { // 1. 记录开始时间 long startTime = System.currentTimeMillis(); log.info("开始处理人脸检测请求,模式: {}", mode); // 2. 根据模式选择不同逻辑(一堆if-else) DetectionResult result; try { if ("standard".equals(mode)) { result = mogFaceService.detectStandard(file); } else if ("fast".equals(mode)) { result = mogFaceService.detectFast(file); } else if ("accurate".equals(mode)) { result = mogFaceService.detectAccurate(file); } else { return ApiResponse.error("不支持的检测模式"); } // 3. 发送通知(邮件、站内信等) notificationService.sendDetectionSuccessNotification(result); // 4. 记录指标 metricsService.recordDetectionTime(System.currentTimeMillis() - startTime); metricsService.incrementDetectionCount(); log.info("人脸检测成功,耗时: {}ms", System.currentTimeMillis() - startTime); return ApiResponse.success(result); } catch (Exception e) { log.error("人脸检测失败", e); notificationService.sendDetectionFailureNotification(e.getMessage()); return ApiResponse.error("检测失败"); } } }这段代码跑起来没问题,但维护起来就很头疼了。痛点非常明显:
- 违反开闭原则:每增加一种新的检测模式(比如加个“夜间模式”),就得回来修改这个Controller里的if-else,容易出错。
- 职责混杂:一个方法里,既负责业务逻辑(调用检测),又负责横切关注点(日志、监控、通知),耦合度太高。
- 难以扩展:如果后续要求检测完成后,除了发通知,还要更新用户画像、写入消息队列,这个方法的代码会越来越臃肿。
- 可测试性差:因为依赖了多个具体服务,单元测试很难写,需要Mock一堆对象。
这其实就是典型的需要用设计模式来解耦和优化的场景。下面我们看看怎么用几个经典模式来重构。
2. 工厂模式:优雅地创建检测策略
首先解决那个长长的if-else。我们有好几种检测策略(标准、快速、精准),未来还可能增加。这正好是工厂模式的用武之地。
工厂模式的核心思想是:将对象的创建逻辑封装起来,客户端不关心具体创建哪个对象,只通过一个统一的接口来获取。
2.1 定义策略接口和具体实现
我们先抽象出一个检测策略的接口:
public interface DetectionStrategy { /** * 执行人脸检测 * @param imageFile 图片文件 * @return 检测结果 */ DetectionResult execute(MultipartFile imageFile) throws DetectionException; }然后,为每种模式实现这个接口:
@Service public class StandardDetectionStrategy implements DetectionStrategy { @Autowired private MogFaceService mogFaceService; @Override public DetectionResult execute(MultipartFile imageFile) { // 调用标准检测逻辑 return mogFaceService.detectStandard(imageFile); } } @Service public class FastDetectionStrategy implements DetectionStrategy { @Autowired private MogFaceService mogFaceService; @Override public DetectionResult execute(MultipartFile imageFile) { // 调用快速检测逻辑,可能牺牲一些精度 return mogFaceService.detectFast(imageFile); } } // ... 其他策略类似2.2 实现策略工厂
接下来,实现一个工厂来管理这些策略:
@Component public class DetectionStrategyFactory { private final Map<String, DetectionStrategy> strategyMap = new ConcurrentHashMap<>(); @Autowired public DetectionStrategyFactory(List<DetectionStrategy> strategies) { // Spring会自动注入所有DetectionStrategy的实现类 for (DetectionStrategy strategy : strategies) { // 这里需要一个方式将策略名称和实现类关联起来 // 例如,可以通过注解或约定好的Bean名称 String strategyName = resolveStrategyName(strategy); strategyMap.put(strategyName, strategy); } } private String resolveStrategyName(DetectionStrategy strategy) { // 简单实现:根据类名转换,如 StandardDetectionStrategy -> "standard" String className = strategy.getClass().getSimpleName(); return className.replace("DetectionStrategy", "").toLowerCase(); } public DetectionStrategy getStrategy(String mode) { DetectionStrategy strategy = strategyMap.get(mode); if (strategy == null) { throw new IllegalArgumentException("未找到对应的检测策略: " + mode); } return strategy; } }2.3 重构后的Controller
现在,Controller变得清爽多了:
@RestController @RequestMapping("/face") public class RefactoredFaceDetectionController { @Autowired private DetectionStrategyFactory strategyFactory; @PostMapping("/detect") public ApiResponse detect(@RequestParam("image") MultipartFile file, @RequestParam(value = "mode", defaultValue = "standard") String mode) { try { // 1. 通过工厂获取策略,无需if-else DetectionStrategy strategy = strategyFactory.getStrategy(mode); // 2. 执行策略 DetectionResult result = strategy.execute(file); return ApiResponse.success(result); } catch (IllegalArgumentException e) { return ApiResponse.error("不支持的检测模式: " + mode); } catch (DetectionException e) { return ApiResponse.error("检测失败: " + e.getMessage()); } } }好处立竿见影:增加新策略时,只需要新建一个实现DetectionStrategy接口的类并注册为Spring Bean,工厂会自动发现它。Controller的代码一行都不用改,完全符合开闭原则。
3. 观察者模式:解耦结果处理逻辑
第一个问题解决了,但原来的Controller里还有发送通知、记录指标这些“事后处理”逻辑。它们和核心的检测业务关系不大,但必须执行。如果以后还要加新的处理逻辑,难道又要回来改Controller吗?
这时候可以用观察者模式(或者叫发布-订阅模式)。它的核心是:当一个对象(主题)的状态发生改变时,所有依赖于它的对象(观察者)都会得到通知并自动更新。
在我们的场景里,“人脸检测完成”就是一个事件,通知服务、指标服务等都是这个事件的观察者。
3.1 定义检测事件和监听器
首先,定义一个检测完成事件:
public class FaceDetectionEvent { private final DetectionResult result; private final long costTimeMillis; private final boolean success; private final String mode; // 构造器、getter方法省略... }然后,定义事件监听器接口:
public interface DetectionEventListener { /** * 处理检测完成事件 */ void onDetectionCompleted(FaceDetectionEvent event); }让各个处理服务实现这个监听器:
@Component public class NotificationEventListener implements DetectionEventListener { @Override public void onDetectionCompleted(FaceDetectionEvent event) { if (event.isSuccess()) { // 发送成功通知 sendSuccessNotification(event.getResult()); } else { // 发送失败通知 sendFailureNotification(event.getResult()); } } // ... 具体发送方法 } @Component public class MetricsEventListener implements DetectionEventListener { @Override public void onDetectionCompleted(FaceDetectionEvent event) { // 记录耗时 recordDetectionTime(event.getCostTimeMillis()); // 计数 if (event.isSuccess()) { incrementSuccessCount(); } else { incrementFailureCount(); } } // ... 具体记录方法 }3.2 实现事件发布者
我们需要一个事件发布者来管理监听器并发布事件:
@Component public class DetectionEventPublisher { private final List<DetectionEventListener> listeners = new CopyOnWriteArrayList<>(); @Autowired(required = false) // required=false避免没有监听器时启动报错 public void setListeners(List<DetectionEventListener> listeners) { this.listeners.addAll(listeners); } public void publishEvent(FaceDetectionEvent event) { // 异步执行,避免阻塞主流程 listeners.forEach(listener -> { try { listener.onDetectionCompleted(event); } catch (Exception e) { // 单个监听器处理失败不应影响其他监听器 log.error("事件监听器处理失败: {}", listener.getClass().getSimpleName(), e); } }); } }3.3 再次重构Controller
现在,Controller只需要关注核心检测逻辑,事件处理完全解耦:
@RestController @RequestMapping("/face") public class RefactoredFaceDetectionControllerV2 { @Autowired private DetectionStrategyFactory strategyFactory; @Autowired private DetectionEventPublisher eventPublisher; @PostMapping("/detect") public ApiResponse detect(@RequestParam("image") MultipartFile file, @RequestParam(value = "mode", defaultValue = "standard") String mode) { long startTime = System.currentTimeMillis(); boolean success = false; DetectionResult result = null; try { DetectionStrategy strategy = strategyFactory.getStrategy(mode); result = strategy.execute(file); success = true; return ApiResponse.success(result); } catch (IllegalArgumentException e) { return ApiResponse.error("不支持的检测模式: " + mode); } catch (DetectionException e) { return ApiResponse.error("检测失败: " + e.getMessage()); } finally { // 无论成功失败,都发布事件 long costTime = System.currentTimeMillis() - startTime; FaceDetectionEvent event = new FaceDetectionEvent(result, costTime, success, mode); eventPublisher.publishEvent(event); } } }这样一来,以后要新增一个处理逻辑(比如检测成功后更新缓存),只需要新增一个监听器实现类即可。核心业务代码完全不受影响,扩展性大大增强。
4. 代理模式:无侵入地添加日志与监控
我们还有最后一个问题:那些横切关注点,比如方法执行时间的监控、入参出参的日志,虽然已经从业务逻辑里抽离出来了,但还是要手动在代码里写long startTime = System.currentTimeMillis()和log.info(...)。有没有更优雅的方式?
当然有,这就是代理模式的典型应用场景。我们可以通过动态代理(或者Spring AOP)在方法执行前后自动添加这些通用逻辑。
4.1 使用Spring AOP实现方法代理
Spring AOP本质上就是基于动态代理实现的。我们定义一个切面,专门用来处理检测服务的监控和日志:
@Aspect @Component @Slf4j public class DetectionMonitorAspect { /** * 切入点:拦截所有DetectionStrategy接口的execute方法 */ @Pointcut("execution(* com.yourproject.strategy.DetectionStrategy.execute(..))") public void detectionPointcut() {} /** * 环绕通知:记录方法执行时间 */ @Around("detectionPointcut()") public Object monitorDetectionTime(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); Object[] args = joinPoint.getArgs(); MultipartFile file = (MultipartFile) args[0]; log.info("开始执行人脸检测,方法: {},文件大小: {} bytes", methodName, file.getSize()); long startTime = System.currentTimeMillis(); try { // 执行原方法 Object result = joinPoint.proceed(); long costTime = System.currentTimeMillis() - startTime; log.info("人脸检测执行成功,方法: {},耗时: {}ms", methodName, costTime); // 这里也可以将耗时推送到监控系统(如Prometheus) recordToMetricsSystem(methodName, costTime, true); return result; } catch (Exception e) { long costTime = System.currentTimeMillis() - startTime; log.error("人脸检测执行失败,方法: {},耗时: {}ms,错误: {}", methodName, costTime, e.getMessage()); recordToMetricsSystem(methodName, costTime, false); throw e; } } private void recordToMetricsSystem(String methodName, long costTime, boolean success) { // 实际项目中,这里可以推送数据到监控系统 // metricsService.record(methodName, costTime, success); } }4.2 最终版的Controller
应用了工厂模式、观察者模式和代理模式之后,我们的Controller达到了最简洁的状态:
@RestController @RequestMapping("/face") @Slf4j public class FinalFaceDetectionController { @Autowired private DetectionStrategyFactory strategyFactory; @Autowired private DetectionEventPublisher eventPublisher; @PostMapping("/detect") public ApiResponse detect(@RequestParam("image") MultipartFile file, @RequestParam(value = "mode", defaultValue = "standard") String mode) { DetectionResult result = null; boolean success = false; long startTime = System.currentTimeMillis(); // 这个时间记录可以被AOP替代,这里仅作演示 try { // 1. 工厂模式:获取策略 DetectionStrategy strategy = strategyFactory.getStrategy(mode); // 2. 代理模式(AOP):自动添加了日志和监控 result = strategy.execute(file); success = true; return ApiResponse.success(result); } catch (IllegalArgumentException e) { log.warn("不支持的检测模式: {}", mode); return ApiResponse.error("不支持的检测模式: " + mode); } catch (Exception e) { log.error("人脸检测过程异常", e); return ApiResponse.error("系统繁忙,请稍后重试"); } finally { // 3. 观察者模式:发布事件,异步处理后续逻辑 long costTime = System.currentTimeMillis() - startTime; FaceDetectionEvent event = new FaceDetectionEvent(result, costTime, success, mode); // 异步发布,不阻塞请求响应 CompletableFuture.runAsync(() -> eventPublisher.publishEvent(event)); } } }现在的代码,核心业务逻辑只有三行(获取策略、执行策略、返回结果),清晰明了。所有的扩展点(新策略、新监听器、新监控项)都可以通过新增类来实现,完全符合设计原则。
5. 总结与思考
回过头看这次重构,我们用了三个最常被问到的“八股文”设计模式,解决了实际开发中的三个核心痛点:
- 工厂模式解决了对象创建的逻辑复杂和违反开闭原则的问题,让策略扩展变得轻而易举。
- 观察者模式解耦了核心业务与后续处理逻辑,让系统在面对新增需求时展现出良好的弹性。
- 代理模式(通过AOP实现)将横切关注点(日志、监控)从业务代码中剥离,让代码更专注于业务本身,也更整洁。
当然,设计模式不是银弹,不能为了用而用。在这个项目里,之所以这些模式用起来顺手,是因为它们恰好匹配了我们遇到的“变化点”:检测策略会变、结果处理方式会变、监控需求会变。识别出这些可能变化的地方,然后用恰当的模式封装变化,这才是设计模式正确的打开方式。
下次当你准备面试背“八股文”时,不妨多想想这些模式在你的项目里能用在哪儿。用实战经验去理解理论,印象会更深刻,回答起来也更有底气。毕竟,面试官想听的,不是你背得多熟,而是你真的会用。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
