什么是设计模式:从GoF到云原生时代的演进

换个角度看,设计模式这东西,就是咱们程序员在写代码时总结出来的一套“套路”。别觉得“套路”是个贬义词,这里的套路指的是经过时间考验的、能优雅解决特定问题的模板。就像你做饭有菜谱,咱们写代码也有“菜谱”。

很多刚入门的同学一听到设计模式,脑子里蹦出来的就是那本厚得像砖头一样的《设计设计:可复用面向对象软件的基础》(也就是传说中的 GoF 著作)。没错,这是设计模式的“圣经”,1994年出版的那一版至今仍是核心基础。虽然这本书写了快30年了,但里面的思想一点都不过时。为什么?因为设计模式关注的是抽象层面的解耦、复用和扩展,而不是具体的语法糖。

不过,这不代表设计模式是一成不变的。作为一个写了5年代码的开发者,我见证了它从经典的面向对象(OOP)到如今云原生和函数式编程的演进。

早期的GoF 23种模式,主要解决的是单体应用或者早期企业级应用的问题。那时候大家追求的是“高内聚、低耦合”。比如,你想换一种数据库,能不能不改业务代码?用工厂模式。你想让多个模块监听一个事件的发生,能不能互不干扰?用观察者模式。

但到了2024-2026年这个云原生时代,情况变了。光会写传统的单例或者工厂已经不够看了。现在的趋势是:

在社区里,关于设计模式的讨论也很有意思。比如大家经常吵“单例模式是不是反模式”?因为它容易造成全局状态污染。还有“工厂模式和依赖注入(DI)到底选哪个”?在Spring全家桶横行的今天,这个问题确实值得深思。

别被这些概念吓到,咱们接下来的章节,就挑最实用的三个“剑客”——单例、工厂、观察者,结合最新的技术趋势,给你掰开了揉碎了讲。

📌 要点提醒

不要为了用设计模式而用设计模式。如果你只有一个类,别硬上单例;如果你只创建一种对象,别硬上工厂。设计模式解决的是变化带来的问题,没有变化,就不需要模式。

---

单例模式实现与线程安全:枚举与双重检查锁详解

单例模式,其实,“计划生育”政策在代码里的体现:在这个类的一生中,只能有一个实例存在

为什么要这么干?咱们拿数据库连接池或者日志管理器举个例子。如果你每次写日志都new一个Logger对象,或者每次查数据库都建一个新连接,那系统资源分分钟被你榨干。这时候,全局唯一的一个实例就显得尤为重要了。

但是,实现一个完美的单例,坑是真多。尤其是多线程环境下,如果不小心,可能会“生”出好几个实例来。

咱们先聊聊最经典,也是面试必问的双重检查锁(Double-Checked Locking, DCL)。这玩意儿在Java里是经典面试题,很多新手写着写着就漏了volatile关键字,结果在极端情况下还是翻车。

看代码,这是一个标准的、线程安全的双重检查锁实现:

public class DatabaseConnectionPool { // volatile 关键字绝对不能少! // 它能防止指令重排序,保证在实例化过程中,其他线程能看到最新的状态 private static volatile DatabaseConnectionPool instance; private Connection connection; // 私有构造方法,防止外部 new private DatabaseConnectionPool() { // 模拟初始化连接 System.out.println("初始化数据库连接池..."); // 实际项目中这里会加载配置,建立物理连接 } public static DatabaseConnectionPool getInstance() { // 第一次检查:如果实例已经存在,直接返回,避免不必要的同步开销 if (instance == null) { synchronized (DatabaseConnectionPool.class) { // 第二次检查:进入同步块后,再检查一次,防止多个线程同时通过第一次检查 if (instance == null) { instance = new DatabaseConnectionPool(); } } } return instance; } public void executeQuery(String sql) { System.out.println("执行SQL: " + sql); } // 测试一下 public static void main(String[] args) { Runnable task = () -> { DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); System.out.println(pool.hashCode()); // 看看hashcode是不是一样的 }; // 模拟多线程环境 for (int i = 0; i < 5; i++) { new Thread(task).start(); } } }

稍微解释下这个代码。外面那层if是为了性能,如果已经创建了,就别进锁了。里面那层if是为了安全,防止两个线程同时卡在锁门口,一个进去了出来后,另一个进去又new一个。最骚的是volatile关键字,它保证了指令的有序性,不然JVM可能会指令重排,导致返回了一个还没初始化完成的对象,直接让你的程序崩掉。

不过,作为老鸟,我得告诉你一个更牛逼的实现方式,也是Java 8+ 推荐的方式:枚举单例

《Effective Java》这本书里明确说了,单元素的枚举类型是实现单例的最佳方式。为什么?因为它不仅能避免线程同步问题,还能自动支持序列化机制,防止反序列化重新创建新的对象。

public enum LoggerSingleton { INSTANCE; private String logFile; // 枚举的构造器默认是私有的,而且线程安全 LoggerSingleton() { logFile = "app.log"; System.out.println("日志管理器初始化,写入文件: " + logFile); } public void log(String message) { // 实际项目中这里会写文件或者输出到控制台 System.out.println("[INFO] " + message); } public static void main(String[] args) { // 用法极其简单 LoggerSingleton.INSTANCE.log("系统启动成功"); LoggerSingleton.INSTANCE.log("收到用户请求"); // 验证唯一性 System.out.println(LoggerSingleton.INSTANCE == LoggerSingleton.INSTANCE); // true } }

你看,用枚举写单例,代码极其简洁,想出错都难。除非你在用老掉牙的Java版本,否则我强烈建议直接用枚举。

⚡ 效率提示

除非你有特殊的历史包袱或者必须要延迟加载(而且延迟加载对性能影响不大),否则无脑选择枚举实现单例。它是防黑客、防反射、防序列化攻击的金钟罩。

---

工厂模式实战:解耦对象创建与依赖注入对比

工厂模式,核心思想就一句话:把创建对象的权利从业务代码里拿出去,交给一个专门的“工厂”去干

为什么非要这么麻烦?直接new不行吗?其实,直接new就是把“用什么”和“怎么造”死死绑在一起了。比如你写了一个UserService,里面直接new MySQLDao()。哪天老板说要换Oracle了,你是不是得去改UserService的代码?这就违反了“对修改关闭,对扩展开放”的开闭原则。

工厂模式就是为了解耦。咱们看个实际的例子。假设咱们在做跨平台的UI开发,要生成不同系统的按钮。

// 1. 产品接口 interface Button { void render(); void onClick(); } // 2. 具体产品 class WindowsButton implements Button { public void render() { System.out.println("渲染一个Windows风格的按钮"); } public void onClick() { System.out.println("Windows按钮点击效果"); } } class MacOSButton implements Button { public void render() { System.out.println("渲染一个MacOS风格的按钮"); } public void onClick() { System.out.println("MacOS按钮点击效果"); } } // 3. 工厂类 class GUIFactory { public static Button createButton(String osType) { if (osType == null || osType.isEmpty()) { return null; } if (osType.equalsIgnoreCase("Windows")) { return new WindowsButton(); } else if (osType.equalsIgnoreCase("MacOS")) { return new MacOSButton(); } throw new IllegalArgumentException("不支持的操作系统: " + osType); } } // 客户端代码 public class Application { public static void main(String[] args) { String osName = System.getProperty("os.name"); // 获取系统名称 Button button = GUIFactory.createButton(osName); if (button != null) { button.render(); button.onClick(); } } }

在这个例子里,客户端代码根本不关心按钮是怎么new出来的,它只要告诉工厂“我要个按钮”,工厂就给它一个适配当前系统的按钮。这就是简单工厂

但是,现在社区里有个热门话题:工厂模式 vs 依赖注入(DI)。很多新手会懵逼,Spring里不都是用@Autowired吗?还用得着工厂吗?

核心要点:工厂模式是“我自己主动去拿”,而依赖注入是“别人喂给我”。

在Spring这种框架里,你通常不需要写上面的GUIFactory了,因为Spring容器本身就是个大工厂。你只需要定义Bean,容器帮你注入。但是,这并不意味着工厂模式没用了。在以下场景,工厂依然不可替代:

比如,在2024-2026年的云原生趋势下,工厂模式被用来动态创建云资源。比如根据负载情况,动态决定是创建一个标准的Pod还是Serverless实例。

// 伪代码逻辑,展示云资源工厂的概念 class CloudResourceFactory { public static Resource createResource(String type, int load) { if ("serverless".equals(type) && load < 100) { return new LambdaFunction(); // 低负载用Serverless } else { return new DockerContainer(); // 高负载用容器 } } }

⚡ 效率提示

如果你在一个重度使用Spring的项目里,简单的对象创建直接用依赖注入(DI)。但不要盲目排斥工厂模式,当你发现对象的创建逻辑变得复杂,或者需要根据运行时条件动态决定创建哪种对象时,果断上工厂模式,它们往往是配合使用的,而不是二选一。

---

观察者模式与响应式编程:Vue3 Proxy与RxJava应用

观察者模式,咱们平时用得最多,可能你用了都不知道。它的定义是:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

打个比方,就是“订阅-发布”机制。你订阅了某个UP主(Subject),UP主发视频(Event)了,平台就会自动通知你(Observer)。

在前端领域,观察者模式简直是灵魂。咱们拿Vue3举例。Vue2用的是Object.defineProperty,Vue3升級后用了ES6的Proxy。为啥换?因为Proxy能劫持整个对象,不用像defineProperty那样去遍历每一个属性,性能更好,代码也更简洁。

虽然我们不能直接看到Vue的源码(那是经过高度封装的),但我们可以用Proxy模拟一个简单的响应式系统,这就是观察者模式的精髓:

// 模拟 Vue3 的响应式原理 const reactive = (target) => { // 存储依赖(观察者)的地方 const dep = new Set(); return new Proxy(target, { get(obj, key) { // 依赖收集:这里模拟一下,实际Vue中是在effect里做的 // console.log(`读取了 ${key} 属性`); return obj[key]; }, set(obj, key, value) { obj[key] = value; // 触发更新:通知所有观察者 console.log(`属性 ${key} 被修改为 ${value},通知所有观察者...`); dep.forEach(fn => fn()); // 模拟执行更新函数 return true; } }); }; // 测试代码 const data = reactive({ price: 10, quantity: 2 }); // 模拟一个观察者(Watcher) const watcher = () => { console.log(`计算总价: ${data.price * data.quantity}`); }; // 模拟依赖收集 const dep = new Set(); dep.add(watcher); // 触发变化 data.price = 20; // 会触发 set,通知 watcher 重新计算

在后端Java世界,观察者模式也没闲着。特别是响应式编程兴起后。像RxJava或者Project Reactor(Spring WebFlux的底裤),它们把观察者模式玩出了花。

传统的观察者模式是“推”或者“拉”,而RxJava引入了数据流的概念。你可以把数据想象成一条河,观察者(Subscriber)在这头,数据源(Observable)在那头。中间你可以加各种过滤器(filter)、转换器(map)。

举个RxJava的简单例子(需要引入io.reactivex.rxjava3:rxjava:3.1.5依赖):

import io.reactivex.rxjava3.core.Observable; import java.util.concurrent.TimeUnit; public class ReactiveDemo { public static void main(String[] args) throws InterruptedException { // 创建一个被观察者(数据流) Observable<Long> observable = Observable.interval(1, TimeUnit.SECONDS) .take(5); // 只取前5个 // 订阅(观察者) observable.subscribe( data -> System.out.println("接收到数据: " + data), // onNext error -> System.err.println("出错了: " + error), // onError () -> System.out.println("数据流结束了") // onComplete ); // 防止主线程退出 Thread.sleep(6000); } }

这段代码里,Observable.interval就像一个定时器,每隔一秒发射一个数字。这就是一个典型的异步数据流。观察者不需要去轮询,数据来了自动就处理了。这在处理高并发、异步IO的场景下,比传统的回调地狱(Callback Hell)要优雅得多。

现在的趋势是,观察者模式正在和AI辅助生成结合。GitHub Copilot现在已经能很准确地帮你生成观察者接口和事件处理的骨架代码了。而且,在函数式编程里,观察者模式甚至被简化成了高阶函数回调,不再需要定义那么多的接口类。

📌 要点提醒

如果你在做前端开发,一定要深入理解Vue3的Proxy或者React的发布订阅机制,这是框架的基石。如果你在做后端,遇到需要处理大量异步事件或者实时数据推送(比如股票行情、IM消息)的场景,别犹豫,上观察者模式,或者直接用RxJava/Reactor这种响应式库,能让你少写几千行回调代码。

5. 组合场景实战:电商系统中模式的协同工作

其实,设计模式这东西,单独拿出来看大家都懂,但一放到真实项目里很多人就懵了,不知道怎么把它们串起来。其实在稍微复杂点的系统里,这几个模式经常是“组团出道”的。咱们就拿电商系统举个例子,这是最经典的实战场景,涵盖了下单、配置、通知等核心流程。

在这个电商系统里,咱们要搞定三个事儿:第一,系统的全局配置(比如支付密钥、API地址)得有个统一管理的地方,不能到处new;第二,订单类型可能有很多种,比如普通订单、秒杀订单,创建逻辑不一样,得解耦;第三,订单状态一旦变了(比如从“待支付”变成“已支付”),得自动通知物流、积分、支付等多个下游系统,不能硬编码。

这里就是组合场景的威力了:单例模式负责管理全局配置,工厂模式负责创建不同类型的订单,而观察者模式负责在订单状态改变时通知各个模块。

5.1 单例模式:全局配置中心

咱们先搞配置。电商系统的配置通常是只读的,而且全局唯一。这里,不要用那种复杂的双重检查锁了,在Java里,最佳实践是直接用枚举实现单例(参考GoF经典理论结合Java 8+特性),这是《Effective Java》里推荐的做法,天生防反射、防序列化破坏。

// 全局配置管理器(单例) public enum AppConfig { INSTANCE; private String paymentGatewayUrl; private String logisticsApiKey; // 枚举构造器默认就是私有的,且只执行一次 AppConfig() { System.out.println("加载系统配置..."); this.paymentGatewayUrl = "https://pay.example.com/v3"; this.logisticsApiKey = "sk-1234567890abcdef"; } public String getPaymentGatewayUrl() { return paymentGatewayUrl; } public String getLogisticsApiKey() { return logisticsApiKey; } }

5.2 工厂模式:订单创建工厂

接下来是订单。咱们定义一个简单的订单接口,然后搞个工厂来生产它们。这样以后加个“团购订单”,只需要改工厂,不用改业务调用代码。

// 订单接口 interface Order { void process(); } // 普通订单 class NormalOrder implements Order { @Override public void process() { System.out.println("处理普通订单逻辑,计算运费..."); } } // 秒杀订单 class SeckillOrder implements Order { @Override public void process() { System.out.println("处理秒杀订单逻辑,校验库存..."); } } // 订单工厂 class OrderFactory { public static Order createOrder(String type) { // 这里可以用单例的配置来决定一些创建参数 String apiUrl = AppConfig.INSTANCE.getPaymentGatewayUrl(); System.out.println("使用支付网关: " + apiUrl); if ("normal".equalsIgnoreCase(type)) { return new NormalOrder(); } else if ("seckill".equalsIgnoreCase(type)) { return new SeckillOrder(); } throw new IllegalArgumentException("未知订单类型"); } }

5.3 观察者模式:订单状态广播

订单创建完了,支付成功,这时候得通知一堆人。咱们用观察者模式,把订单作为“主题(Subject)”。

import java.util.ArrayList; import java.util.List; // 观察者接口 interface OrderObserver { void onOrderPaid(Order order); } // 物流模块观察者 class LogisticsObserver implements OrderObserver { @Override public void onOrderPaid(Order order) { System.out.println("物流模块:收到订单支付成功通知,准备发货单!"); } } // 积分模块观察者 class PointsObserver implements OrderObserver { @Override public void onOrderPaid(Order order) { System.out.println("积分模块:收到通知,准备给用户加积分!"); } } // 订单主题(被观察者) class OrderSubject { private List<OrderObserver> observers = new ArrayList<>(); private Order order; public OrderSubject(Order order) { this.order = order; } public void addObserver(OrderObserver observer) { observers.add(observer); } // 模拟支付成功 public void paySuccess() { System.out.println("订单支付成功!"); notifyObservers(); } private void notifyObservers() { for (OrderObserver observer : observers) { observer.onOrderPaid(this.order); } } }

5.4 协同工作演示

最后看看怎么把它们跑起来:

public class ECommerceDemo { public static void main(String[] args) { // 1. 工厂创建订单 Order order = OrderFactory.createOrder("seckill"); order.process(); // 2. 设置观察者 OrderSubject subject = new OrderSubject(order); subject.addObserver(new LogisticsObserver()); subject.addObserver(new PointsObserver()); // 3. 触发状态变更(这里单例配置其实在工厂里已经用过了) subject.paySuccess(); } }

⚡ 效率提示:在真实开发中,观察者模式的通知逻辑如果涉及IO操作(比如调第三方接口),千万别在paySuccess主线程里同步执行,否则一个物流接口超时,整个支付回调就卡住了。一定要搞个线程池做异步通知,或者直接用消息队列(MQ)来解耦,那其实是观察者模式在分布式场景下的“高配版”。

---

6. 2024趋势与:AI生成代码与反模式争议

做开发这行,这两年最大的感受就是变化太快。特别是2024年,AI辅助编程已经不是“尝鲜”了,而是真的在改变我们的编码习惯。结合咱们聊的单例、工厂、观察者,咱们得聊聊现在的技术趋势

6.1 单例的“反模式”争议

先说个社区里吵得最凶的话题:单例模式是不是反模式?很多大佬(特别是搞DDD领域驱动设计的)特别反感单例。为啥?因为单例本质上就是全局变量。它虽然控制了实例数量,但也带来了隐性的依赖,而且最要命的是,它极难测试。

比如你写单元测试,单例里有个状态,上一个测试改了,下一个测试没重置,直接就挂了。所以在2024年,如果你用Spring Boot这类框架,其实很少手写单例了。Spring的Bean默认就是单例作用域(Singleton Scope),但它是由容器管理的,比你自己写的new要可控得多。

🔧 实战技巧:如果你不是在写底层框架或者工具类(比如日志、配置),尽量别自己手写单例。如果是为了解决“只有一个实例”的问题,优先考虑依赖注入(DI)容器来管理生命周期。

6.2 AI生成代码:GitHub Copilot与工厂模式

现在写代码,谁不用个GitHub Copilot或者Cursor简直亏大了。AI在生成工厂模式代码这块简直是神级辅助。比如你定义了几个产品类,你只要敲个注释“// Create a factory to generate these products”,AI立马给你把工厂类骨架搭好了。

但是!AI生成的代码往往比较“教科书”。比如让它写个单例,它可能给你来个双重检查锁(DCL),看着挺牛,其实在Java 5之前是有Bug的(指令重排序),虽然现在JMM修复了,但代码复杂度高。或者它给你生成观察者模式,可能还是用最原始的继承实现,而不是现在的函数式接口。

6.3 云原生与轻量化趋势

现在的趋势是云原生轻量化

看看这个Python闭包实现单例的例子,比Java那一大坨类定义清爽多了,这也是2024年函数式编程流行带来的变化:

def singleton(cls): instances = {} def get_instance(*args, **kwargs): if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance @singleton class DatabaseConnection: def __init__(self): print("建立数据库连接... (只会执行一次)") self.status = "connected" # 测试 conn1 = DatabaseConnection() conn2 = DatabaseConnection() print(conn1 is conn2) # True

💡 经验总结:用AI生成设计模式代码时,一定要人工Review。特别是工厂模式,AI可能会过度设计,搞出一堆抽象工厂,结果你其实只需要个简单工厂。记住一句话:代码是写给人的,不是写给编译器或AI看的。 简单直接往往比“模式大全”更好。

---

7. 常见面试问题解析:手写单例与推拉模型区别

到了面试环节,这几个模式绝对是必考题。特别是单例的线程安全实现,以及观察者模式中“推模型”和“拉模型”的区别。咱们直接上干货,把这些坑一个个填平。

7.1 手写单例:线程安全与枚举

面试官最喜欢让你手写单例。如果你还在写那种synchronized方法,或者双重检查锁,可能只能拿个及格分。值得留意的是,,在Java里,最优雅、最安全、最防破解的写法是枚举单例

为什么?因为传统的双重检查锁(DCL)虽然理论可行,但代码繁琐,且如果忘了加volatile,在高并发下可能因为指令重排序出问题。而反射攻击可以轻松破解私有构造器。枚举呢?JVM保证枚举实例的创建是线程安全的,而且枚举类没有构造器可以被反射调用。

不过,为了展示你对底层原理的理解,我还是给你写一个经典的双重检查锁(DCL)版本,这是面试常客:

class SingletonDCL { // 值得留意的是,必须加 volatile,防止指令重排序 private static volatile SingletonDCL instance; private SingletonDCL() { // 防止反射破坏单例(虽然枚举不需要这个) if (instance != null) { throw new RuntimeException("单例已存在,禁止反射创建!"); } } public static SingletonDCL getInstance() { // 第一次检查,避免不必要的同步 if (instance == null) { synchronized (SingletonDCL.class) { // 第二次检查,确保只有一个实例 if (instance == null) { instance = new SingletonDCL(); } } } return instance; } }

如果你在面试时直接写枚举,并解释为什么枚举更好(参考GoF 1994经典理论结合现代Java特性),面试官绝对眼前一亮。

7.2 观察者模式:推模型 vs 拉模型

这是观察者模式里最容易被忽略的细节。很多新手只知道通知,不知道怎么传数据。

咱们用代码对比一下。假设订单状态变了,咱们要通知价格信息。

import java.util.ArrayList; import java.util.List; // 推模型示例 class PushSubject { private List<Observer> observers = new ArrayList<>(); private String state; private double price; public void setState(String state, double price) { this.state = state; this.price = price; notifyPush(); } private void notifyPush() { // 把数据直接推过去 for (Observer o : observers) { o.update(state, price); } } interface Observer { // 推模型:参数里直接带数据 void update(String state, double price); } } // 拉模型示例 class PullSubject { private List<PullObserver> observers = new ArrayList<>(); private String state; private double price; public void setState(String state, double price) { this.state = state; this.price = price; notifyPull(); } private void notifyPull() { // 只通知,不传数据 for (PullObserver o : observers) { o.update(this); // 把自身引用传过去 } } // 提供getter让观察者自己拉数据 public String getState() { return state; } public double getPrice() { return price; } interface PullObserver { // 拉模型:拿到主题对象,自己取数据 void update(PullSubject subject); } }

📌 要点提醒:在实际项目里,我更推荐推模型。现在的系统讲究“事件驱动”,咱们通常会定义一个Event对象(比如OrderPaidEvent),里面封装了所有相关的DTO数据,一次性推给观察者。这样观察者不需要依赖具体的Subject类,只需要依赖Event,解耦更彻底。特别是结合Spring的ApplicationEventPublisher,那简直是推模型的完美实践。