news 2026/9/22 21:02:12

常用的设计模式新手避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
常用的设计模式新手避坑

5个常用设计模式新手避坑指南:面试不挂实战能跑

面试官问:“单例模式怎么保证线程安全?”你张嘴就来“加锁”,结果被追问“双重检查锁DCL为什么需要volatile?”直接卡壳,面经上写的套路在真实场景里根本行不通。

做开发这几年,见过太多新手把设计模式当成背题工具,代码里硬套模板,结果项目里全是“伪模式”。今天咱们不聊虚的,直接上手写一个日志记录系统,用5个最常用设计模式解决真实痛点。看完这篇,你不仅能应付面试,还能在简历里写出“重构过日志模块”这种硬通货。

项目目标:为什么不用工厂硬写if-else

假设我们要做一个日志系统,支持控制台输出、文件输出、数据库输出三种方式。新手第一反应通常是这样的:

public class Logger {public void log(String message) {String type = config.get("log.type");if ("console".equals(type)) {System.out.println(message);} else if ("file".equals(type)) {// 文件写入逻辑} else if ("db".equals(type)) {// 数据库写入逻辑}}
}

这段代码的问题显而易见:每加一种日志类型,就要改Logger类,违反开闭原则。更致命的是,配置解析、资源释放、错误处理全混在一起,维护起来简直是噩梦。

我们的目标是:用5个设计模式重构这个模块,让新增日志类型时零改动核心类,同时保证线程安全、资源可控。最终代码会参考Logback、SLF4J等成熟框架的设计思路,但用极简代码实现核心思想。

目录结构:先搭骨架再填肉

别一上来就写代码,先把目录结构定下来。设计模式的本质是结构,结构对了,代码自然清晰:

src/main/java/com/example/logger/
├── core/
│   ├── ILogger.java          # 抽象日志接口(策略模式)
│   ├── LoggerContext.java    # 上下文管理(单例+观察者)
│   └── LogEvent.java         # 日志事件对象(享元模式)
├── strategy/
│   ├── ConsoleLogger.java    # 控制台策略
│   ├── FileLogger.java       # 文件策略
│   └── DbLogger.java         # 数据库策略
├── factory/
│   └── LoggerFactory.java    # 工厂方法
└── util/└── ResourcePool.java     # 资源池(池化思想)

这个结构有几个关键点:

  • strategy包放所有具体策略,核心类不直接依赖它们
  • factory包负责创建,隔离“怎么创建”和“怎么用”
  • core包只放接口和上下文,这是系统的“骨架”

很多新手犯的错误是把所有类塞在一个包里,结果类之间互相依赖,改一处崩一片。记住:包结构就是你的依赖关系图

核心代码实现:5个模式逐个拆解

1. 策略模式:让日志类型可插拔

先定义接口,这是所有策略模式的起点:

public interface ILogger {void log(LogEvent event);void close(); // 资源释放
}

注意:接口里必须包含close()方法。新手经常忽略资源释放,导致文件句柄泄漏、数据库连接耗尽。参考Apache Commons Logging的开发者文档,所有IO相关组件都必须实现Closeable接口。

控制台策略实现:

public class ConsoleLogger implements ILogger {@Overridepublic void log(LogEvent event) {// 格式化输出,这里简化处理System.out.printf("[%s] %s: %s%n", event.getLevel(), event.getTime(), event.getMessage());}@Overridepublic void close() {// 控制台无需释放资源,但方法必须存在}
}

文件策略要注意资源管理:

public class FileLogger implements ILogger {private final PrintWriter writer;private final String filePath;public FileLogger(String filePath) {this.filePath = filePath;try {this.writer = new PrintWriter(new FileWriter(filePath, true), true);} catch (IOException e) {throw new RuntimeException("Failed to open log file: " + filePath, e);}}@Overridepublic void log(LogEvent event) {// 加锁保证多线程写入安全synchronized (writer) {writer.printf("[%s] %s: %s%n", event.getLevel(), event.getTime(), event.getMessage());}}@Overridepublic void close() {writer.close(); // 关键:必须关闭}
}

避坑点:很多新手在log()方法里创建PrintWriter,每次日志都打开文件,性能直接崩掉。资源初始化应该在构造函数或工厂里完成,而不是每次调用时。

2. 工厂方法:解耦创建逻辑

新手最爱犯的错:在业务代码里直接new FileLogger()。这样如果将来要改成从配置文件读取路径,或者根据环境动态选择,就得改所有调用处。

工厂方法模式解决的就是这个问题:

public class LoggerFactory {private static final Map<String, Function<String, ILogger>> STRATEGY_MAP = new HashMap<>();static {// 注册所有策略STRATEGY_MAP.put("console", path -> new ConsoleLogger());STRATEGY_MAP.put("file", path -> new FileLogger(path));STRATEGY_MAP.put("db", path -> new DbLogger(path));}public static ILogger create(String type, String path) {Function<String, ILogger> factory = STRATEGY_MAP.get(type);if (factory == null) {throw new IllegalArgumentException("Unknown logger type: " + type);}return factory.apply(path);}
}

关键细节:用静态Map注册策略,而不是if-else判断。这样新增日志类型时,只需要:

  1. 实现ILogger接口
  2. 在STRATEGY_MAP里加一行注册

核心类零改动,这就是开闭原则的落地。

3. 单例模式:上下文只有一份

LoggerContext负责管理所有日志实例,必须保证全局唯一。但单例模式新手常写成这样:

public class LoggerContext {private static LoggerContext instance;public static LoggerContext getInstance() {if (instance == null) {instance = new LoggerContext(); // 竞态条件!}return instance;}
}

这段代码在多线程环境下会创建多个实例。正确写法用双重检查锁:

public class LoggerContext {private static volatile LoggerContext instance;private final List<ILogger> loggers = new CopyOnWriteArrayList<>();private LoggerContext() {}public static LoggerContext getInstance() {if (instance == null) { // 第一次检查synchronized (LoggerContext.class) {if (instance == null) { // 第二次检查instance = new LoggerContext();}}}return instance;}public void registerLogger(ILogger logger) {loggers.add(logger);}public void log(LogEvent event) {for (ILogger logger : loggers) {logger.log(event);}}public void shutdown() {for (ILogger logger : loggers) {logger.close();}loggers.clear();}
}

为什么需要volatile:这是面试高频考点。不加volatile,instance可能未完全初始化就被其他线程看到,导致空指针或半初始化对象。JMM(Java内存模型)保证volatile变量写入后的可见性,防止指令重排序。

避坑点:单例模式不是万能的。如果LoggerContext依赖外部配置(比如数据库连接池),构造函数里初始化会导致循环依赖。此时应该用延迟初始化或依赖注入。

4. 观察者模式:解耦事件分发

目前LoggerContext直接调用所有logger,如果未来要加“日志监控”“告警推送”等功能,就得改LoggerContext。观察者模式解决的就是这种“一对多”通知场景。

简化版实现:

public class LoggerContext {private final List<ILogger> loggers = new CopyOnWriteArrayList<>();private final List<Consumer<LogEvent>> listeners = new CopyOnWriteArrayList<>();public void addEventListener(Consumer<LogEvent> listener) {listeners.add(listener);}public void log(LogEvent event) {// 分发日志到所有loggerfor (ILogger logger : loggers) {logger.log(event);}// 通知所有监听器for (Consumer<LogEvent> listener : listeners) {try {listener.accept(event);} catch (Exception e) {// 关键:监听器异常不能影响主流程System.err.println("Listener error: " + e.getMessage());}}}
}

实战场景:加一个告警监听器,当出现ERROR级别日志时发送邮件:

LoggerContext.getInstance().addEventListener(event -> {if ("ERROR".equals(event.getLevel())) {// 发送告警邮件alertService.send(event.getMessage());}
});

这样告警逻辑完全独立于日志核心,新增监控功能不用改LoggerContext。

5. 享元模式:复用日志事件对象

LogEvent对象在高频日志场景下会创建大量实例,GC压力大。享元模式通过对象池复用不可变对象。

简化版LogEvent:

public class LogEvent {private final String level;private final String message;private final long timestamp;public LogEvent(String level, String message) {this.level = level;this.message = message;this.timestamp = System.currentTimeMillis();}// getter方法public String getLevel() { return level; }public String getMessage() { return message; }public long getTime() { return timestamp; }
}

注意:享元模式要求对象不可变。如果LogEvent字段可变,复用会导致数据污染。这里timestamp是创建时固定的,所以可以安全复用。

实际项目中,Logback用到了更复杂的对象池,但核心思想一致:高频创建的小对象,尽量复用

运行与测试:验证模式真的有用

写个测试类验证一下:

public class LoggerTest {public static void main(String[] args) throws Exception {// 1. 获取单例上下文LoggerContext context = LoggerContext.getInstance();// 2. 注册多种日志策略context.registerLogger(LoggerFactory.create("console", null));context.registerLogger(LoggerFactory.create("file", "app.log"));// 3. 添加告警监听器context.addEventListener(event -> {if ("ERROR".equals(event.getLevel())) {System.out.println("ALERT: " + event.getMessage());}});// 4. 记录日志context.log(new LogEvent("INFO", "System started"));context.log(new LogEvent("ERROR", "Database connection failed"));// 5. 关闭资源Thread.sleep(100); // 等待文件写入context.shutdown();}
}

运行结果:

[INFO] 2024-01-15 10:30:00: System started
[ERROR] 2024-01-15 10:30:00: Database connection failed
ALERT: Database connection failed

文件app.log里也记录了这两条日志。关键点:新增“邮件告警”功能时,我们只加了一个监听器,没改任何核心类。这就是设计模式的价值。

测试避坑

  • 单例模式测试时,每个测试用例后必须shutdown()并重置instance(反射),否则测试间互相污染
  • 文件日志测试要用临时目录,别污染项目目录
  • 多线程测试要用CountDownLatchCompletableFuture,别用Thread.sleep()硬等

优化扩展:从能用到好用

基础功能跑通后,还有几个进阶点:

1. 异步日志 同步日志会阻塞业务线程。参考Logback的AsyncAppender,用Disruptor或阻塞队列实现异步:

// 伪代码
private final BlockingQueue<LogEvent> queue = new ArrayBlockingQueue<>(10000);
private final ExecutorService executor = Executors.newSingleThreadExecutor();public void log(LogEvent event) {queue.offer(event); // 非阻塞,满则丢弃
}// 后台线程消费队列
executor.submit(() -> {while (true) {LogEvent event = queue.take();for (ILogger logger : loggers) {logger.log(event);}}
});

2. 动态配置热更新 用观察者模式监听配置文件变化,运行时切换日志级别:

// 监听application.properties变化
FileWatcher.watch("application.properties", changes -> {String level = changes.get("log.level");context.setLogLevel(level);
});

3. 优雅降级 日志系统不能因为IO错误拖垮主业务。所有logger的log()方法必须捕获异常:

@Override
public void log(LogEvent event) {try {// 写入逻辑} catch (Exception e) {// 降级:只打stderr,不抛异常System.err.println("Log failed: " + e.getMessage());}
}

4. 性能指标 加埋点统计日志耗时、队列积压量,用Micrometer暴露到Prometheus:

Timer.builder("logger.write").tag("logger", type).register(meterRegistry).record(() -> logger.log(event));

小结:模式是手段不是目的

这套日志系统用到了5个设计模式,但每个模式都解决了具体问题:

  • 策略模式:日志类型可插拔
  • 工厂方法:解耦创建逻辑
  • 单例模式:上下文唯一性
  • 观察者模式:事件通知解耦
  • 享元模式:对象复用降GC

新手最常见的误区:为了用模式而用模式。比如明明用简单工厂就够,非要搞出抽象工厂+建造者,代码复杂度爆炸。判断标准很简单:如果不用这个模式,代码会怎样? 如果答案是“多改几处if-else”“资源泄漏”“线程不安全”,那这个模式就有价值;如果只是“看起来更高级”,那就别用。

设计模式的终极目标不是炫技,而是让代码在需求变化时少改、改得安全。下次写代码前,先问自己:这里有没有重复逻辑?有没有资源泄漏风险?有没有线程安全问题?答案指向哪个模式,就用哪个。

你在项目里踩过哪个设计模式的坑?比如单例在Spring里怎么用的?策略模式怎么配合配置中心?评论区聊聊,咱们互相避坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/22 21:02:12

2026最新大厂面试反侦查考点:别再背八股,这样答才拿高薪

2026最新大厂面试反侦查考点:别再背八股,这样答才拿高薪 看了一堆教程还是不会写项目,甚至面试时遇到“反侦查”这种偏门词都懵圈?别慌,2026最新的面试风向变了,大厂不再只考八股文,更看重你对底层逻辑和边界场景的理解。很多兄弟觉得“反侦查”是个谍战片词汇,但在编程面试语境下,它特指…

作者头像 李华
网站建设 2026/9/22 21:02:09

转岗程序员别慌:一文搞懂 leaning 底层原理与实战

转岗程序员别慌:一文搞懂 leaning 底层原理与实战 刚背完 Python 字典的增删改查,却连一个待办事项应用都搭不起来?别急,这不只是你的错觉。很多转行做开发的伙伴,卡在“语法”和“工程”的断层上。今天这篇,带你 一文搞懂 leaning…

作者头像 李华
网站建设 2026/9/22 21:02:00

同一个网段排查耗时3小时?5个性能优化实战技巧

同一个网段排查耗时3小时?5个性能优化实战技巧 凌晨两点,IDE 右下角弹出一条刺眼的红色警告。你盯着屏幕上那一长串 java.net.UnknownHostException 和 Connection timed out ,心里只有一句话:这报错一堆看不懂,StackTrace…

作者头像 李华
网站建设 2026/9/22 21:01:52

间岛问题最佳实践: 面试原理卡壳? 3步搞懂核心逻辑

间岛问题最佳实践: 面试原理卡壳? 3步搞懂核心逻辑 面试被问到“间岛问题”的核心原理,脑子一片空白?别慌,这种尴尬我见过太多次。很多开发者只记得背结论,却说不清背后的推导逻辑,导致在技术深挖环节直接挂掉。今天不整虚的,咱们直接上干货,用一套可落地的最佳实践,把这个问题拆解得明明白白,让你下次面试能…

作者头像 李华
网站建设 2026/9/22 21:01:43

2026最新风云下载源码拆解:新手避坑指南

2026最新风云下载源码拆解:新手避坑指南 看了一堆教程还是不会写项目?别慌,这不是你的错。很多开发者卡在从“会写代码”到“能落地”的鸿沟,往往是因为缺乏对底层逻辑的拆解能力。2026最新的风云下载项目源码,正好是一个绝佳的解剖对象。它虽非顶级开源框架,但其内部对并发控制、断点续传和文件流处理的实现…

作者头像 李华
网站建设 2026/9/22 21:01:31

神武宝石计算器实战:3个代码技巧搞定配装最佳实践

神武宝石计算器实战:3个代码技巧搞定配装最佳实践 官方文档堆砌着成千上万的属性词条,配装时翻来覆去算半天,根本抓不住重点。这不仅是神武玩家的噩梦,更是后端开发中典型的数据聚合与算法优化痛点。很多开发者在接到类似需求时,容易陷入“硬算”的误区,导致性能瓶颈。其实,掌握 神武宝石计算器…

作者头像 李华