掌握七种常见设计模式:从入门到实践

你是否曾在代码审查时听到 “这里用个工厂模式就好了”,却不太确定对方在说什么?设计模式听起来很高大上,但本质上,它们是前辈们踩坑无数后总结出的 可复用的解决方案。本文带你从零掌握六种最常用的设计模式。


一、什么是设计模式?

1994 年,Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四位作者(人称 GoF,Gang of Four)出版了《设计模式:可复用面向对象软件的基础》一书,将 23 种经典设计模式归纳为三大类:

类别 核心关注点 典型模式
创建型 如何创建对象 单例、工厂、建造者
结构型 如何组合对象 适配器、装饰器、代理
行为型 对象间如何协作 观察者、策略、命令

在开始学习具体模式之前,建议先了解 SOLID 原则 —— 它是理解设计模式为何这样设计的思维基础。


二、单例模式(Singleton)

2.1 一句话理解

确保一个类只有一个实例,并提供全局访问点。

2.2 典型场景

  • 日志记录器 —— 全局共享同一个日志实例
  • 数据库连接池 —— 避免重复创建连接
  • 应用配置管理 —— 配置只需加载一次

2.3 Java 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// synchronized + volatile 双重检查锁定实现线程安全的单例
public class Singleton {

private volatile static Singleton uniqueInstance;

private Singleton() {
}

public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

2.4 注意事项

  • 优点:节省资源,保证全局一致性
  • 缺点:违反单一职责原则(管理自身创建 + 业务逻辑),增加单元测试难度
  • 现代开发中,依赖注入(DI)往往是更好的全局共享方案

2.5 优缺点

优点

  • 节省资源
  • 全局控制

缺点

  • 扩展性差
  • 职责过重
  • 测试困难

三、工厂模式(Factory)

3.1 一句话理解

将对象创建逻辑封装起来,客户端不需要知道具体类的名字。

3.2 典型场景

  • 支付系统 —— 根据渠道创建不同的支付处理器(微信、支付宝、银行卡)
  • 通知服务 —— 根据类型创建不同的通知方式(短信、邮件、推送)
  • 数据库驱动 —— 根据配置动态切换 MySQL、PostgreSQL、SQLite

3.3 Java 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 抽象产品:电子设备
interface Device {
void operate(); // 操作设备
}

// 具体产品:手机
class Phone implements Device {
@Override
public void operate() {
System.out.println("手机操作:开机 -> 显示主界面 -> 关机\n");
}
}

// 具体产品:电脑
class Computer implements Device {
@Override
public void operate() {
System.out.println("电脑操作:开机 -> 加载系统 -> 关机\n");
}
}

// 简单工厂:设备工厂
class DeviceFactory {
// 根据类型创建设备
public static Device createDevice(String type) {
if (type.equalsIgnoreCase("PHONE")) {
return new Phone();
} else if (type.equalsIgnoreCase("COMPUTER")) {
return new Computer();
}
return null;
}
}

public class SimpleFactoryDemo {
public static void main(String[] args) {
// 通过工厂创建设备
Device phone = DeviceFactory.createDevice("PHONE");
Device computer = DeviceFactory.createDevice("COMPUTER");

// 使用设备
phone.operate();
computer.operate();
}
}

3.4 三种变体

  • 简单工厂:一个工厂类根据参数创建不同对象(如上例)
  • 工厂方法:父类定义创建接口,子类决定创建哪种对象
  • 抽象工厂:创建一组相关对象,确保它们风格一致。它提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们具体的类。可以理解为它是“工厂的工厂”

3.5 优缺点

简单工厂模式

  • 优点:结构简单,将创建逻辑集中管理,实现了职责分离。
  • 缺点:违反开闭原则,新增产品需要修改工厂类。

工厂方法模式

  • 优点:符合开闭原则,扩展性好,创建逻辑被分散到各个具体工厂中,符合单一职责原则。
  • 缺点:每增加一个产品,就需要增加一个具体工厂类,会导致类数量成倍增加,增加了系统的复杂性。

抽象工厂模式

  • 优点:非常适合用于创建一系列相互匹配的产品。切换整个产品族非常方便,只需更换具体的工厂即可。
  • 缺点:扩展新的产品等级结构困难。例如,如果产品族需要增加一个“鼠标”,那么 AbstractFactory 接口就需要增加一个 createMouse() 方法,所有已经实现的具体工厂类也都要跟着修改,这违反了开闭原则。
特性 简单工厂模式 工厂方法模式 抽象工厂模式
复杂度
关注点 创建单一类型的产品 创建单一类型的产品 创建一族相关联的产品
开闭原则 违反 遵守 对扩展产品族遵守,对扩展产品类违反
核心 一个集中式工厂 将创建逻辑延迟到子类 创建产品家族

四、观察者模式(Observer)

4.1 一句话理解

定义一对多的依赖关系,当一个对象状态变化时,所有依赖者自动收到通知。

4.2 典型场景

  • 消息推送 —— 用户订阅话题后,有新内容自动通知
  • 数据绑定 —— 数据变化时 UI 自动刷新(Vue、React 的响应式系统)
  • 事件总线 —— 模块间松耦合通信

4.3 Java 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 观察者接口
interface Subscriber {
void onMessage(String topic, String content);
}

// 被观察者(主题)
class MessageBroker {
private Map<String, List<Subscriber>> subscribers = new HashMap<>();

public void subscribe(String topic, Subscriber subscriber) {
subscribers.computeIfAbsent(topic, k -> new ArrayList<>()).add(subscriber);
}

public void unsubscribe(String topic, Subscriber subscriber) {
List<Subscriber> list = subscribers.get(topic);
if (list != null) {
list.remove(subscriber);
}
}

public void publish(String topic, String content) {
List<Subscriber> list = subscribers.get(topic);
if (list != null) {
for (Subscriber s : list) {
s.onMessage(topic, content);
}
}
}
}

// 使用
MessageBroker broker = new MessageBroker();

broker.subscribe("tech", (topic, content) ->
System.out.println("收到 [" + topic + "]: " + content));

broker.publish("tech", "设计模式真有趣!");
// 输出: 收到 [tech]: 设计模式真有趣!

4.4 注意事项

  • 务必在不需要时取消订阅,否则可能导致内存泄漏
  • 大量观察者时注意性能,可以考虑异步通知

4.5 优缺点

优点

  • 高度解耦
  • 符合开闭原则
  • 支持广播通信

缺点

  • 潜在的性能问题
  • 可能导致意外的级联或循环
  • 调试困难

五、策略模式(Strategy)

5.1 一句话理解

定义一组可互换的算法,让算法可以独立于客户端变化。

5.2 典型场景

  • 排序 —— 运行时切换不同排序算法
  • 折扣计算 —— 满减、打折、立减等多种策略
  • 数据压缩 —— gzip、brotli、lz4 等算法互换
  • 表单验证 —— 不同字段使用不同的验证规则

5.3 Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from abc import ABC, abstractmethod

# 策略接口
class DiscountStrategy(ABC):
@abstractmethod
def apply(self, price: float) -> float:
pass

# 具体策略
class NoDiscount(DiscountStrategy):
def apply(self, price: float) -> float:
return price

class PercentageDiscount(DiscountStrategy):
def __init__(self, percent: float):
self.percent = percent

def apply(self, price: float) -> float:
return price * (1 - self.percent / 100)

class FullReductionDiscount(DiscountStrategy):
def __init__(self, threshold: float, reduction: float):
self.threshold = threshold
self.reduction = reduction

def apply(self, price: float) -> float:
if price >= self.threshold:
return price - self.reduction
return price

# 上下文
class Order:
def __init__(self, total: float):
self.total = total

def checkout(self, strategy: DiscountStrategy) -> float:
return strategy.apply(self.total)

# 使用
order = Order(300)
print(order.checkout(NoDiscount())) # 300.0
print(order.checkout(PercentageDiscount(20))) # 240.0
print(order.checkout(FullReductionDiscount(200, 50))) # 250.0

策略模式最大的好处是消灭了长长的 if-else —— 新增一个策略只需要新建一个类,符合开闭原则。


六、装饰器模式(Decorator)

6.1 一句话理解

在不修改原对象的情况下,动态地给对象添加额外行为。

6.2 典型场景

  • I/O 流 —— BufferedReader 装饰 FileReader
  • 中间件链 —— HTTP 请求依次经过日志、鉴权、压缩中间件
  • UI 组件 —— 给文本框加边框、滚动条、阴影等
  • 日志增强 —— 给函数加计时、重试、缓存

6.3 Python 实现(函数装饰器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import time
import functools

def retry(max_attempts: int = 3, delay: float = 1.0):
"""装饰器:给函数添加自动重试能力"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
import logging
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
logging.warning(
f"[{func.__name__}] 第 {attempt} 次失败,{delay}s 后重试: {e}"
)
time.sleep(delay)
return None
return wrapper
return decorator

def log_execution(func):
"""装饰器:记录函数执行时间"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[{func.__name__}] 执行耗时: {elapsed:.3f}s")
return result
return wrapper

# 使用 —— 装饰器可以叠加
@retry(max_attempts=3, delay=0.5)
@log_execution
def fetch_data(url: str):
# 模拟不稳定的网络请求
import random
if random.random() < 0.5:
raise ConnectionError("网络异常")
return f"来自 {url} 的数据"

# 调用
print(fetch_data("https://api.example.com/data"))

面向对象的装饰器和 Python 函数装饰器共享同一个核心理念:通过组合而非继承来扩展功能


七、适配器模式(Adapter)

7.1 一句话理解

将一个接口转换成客户端期望的另一个接口,让不兼容的类可以一起工作。

7.2 典型场景

  • 第三方库集成 —— 统一不同短信服务商的 API
  • 遗留系统改造 —— 新系统调用老的接口
  • 数据格式转换 —— XML 转 JSON、CSV 转 DataFrame

7.3 Java 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 原有的第三方日志库(不能修改)
class ThirdPartyLogger {
public void logMessage(String msg) {
System.out.println("[ThirdParty] " + msg);
}
}

// 我们系统期望的日志接口
interface Logger {
void info(String message);
void error(String message);
}

// 适配器
class LoggerAdapter implements Logger {
private ThirdPartyLogger adaptee;

public LoggerAdapter(ThirdPartyLogger adaptee) {
this.adaptee = adaptee;
}

@Override
public void info(String message) {
adaptee.logMessage("[INFO] " + message);
}

@Override
public void error(String message) {
adaptee.logMessage("[ERROR] " + message);
}
}

// 使用
ThirdPartyLogger oldLogger = new ThirdPartyLogger();
Logger logger = new LoggerAdapter(oldLogger);
logger.info("用户登录成功"); // [ThirdParty] [INFO] 用户登录成功
logger.error("数据库连接失败"); // [ThirdParty] [ERROR] 数据库连接失败

适配器的精髓在于不修改原有代码的前提下让新旧系统协作 —— 这在微服务拆分和渐进式重构中特别有用。

7.4 优缺点

优点

  • 增强了类的复用性:可以复用已存在的、功能强大的 Adaptee 类,而无需修改其源码。
  • 提高了灵活性和扩展性:可以非常方便地替换或增加新的适配器,来适配不同的 Adaptee,符合开闭原则。
  • 解耦:将客户端(Client)与具体的实现类(Adaptee)解耦,客户端只需要和目标接口(Target)打交道。

缺点

  • 增加了系统的复杂性:每适配一个类都需要增加一个适配器类,如果过度使用,会导致系统中的类数量增多,代码可读性有所下降。
  • (针对类适配器)限制较多:由于语言的单继承限制,类适配器一次最多只能适配一个 Adaptee 类,并且要求 Target 必须是接口或抽象类。

八、代理模式(Proxy)

8.1 一句话理解

为另一个对象提供一个替身或占位符,以控制对这个对象的访问。

8.2 典型场景

  • 虚拟代理(Virtual Proxy)—— 大对象的延迟加载(如图片、数据库连接)
  • 远程代理(Remote Proxy)—— 代表远程对象进行通信(如 RMI、RPC)
  • 保护代理/安全代理(Protection Proxy)—— 控制访问权限(如安全代理、智能指引)
  • 智能引用(Smart Reference)—— 在访问对象时执行额外操作(如计数、日志)

8.3 Java 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 定义发送短信的接口
public interface SmsService {
String send(String message);
}

// 实现发送短信的接口
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}

// 定义一个 JDK 动态代理类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* @author shuang.kou
* @createTime 2020年05月11日 11:23:00
*/
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;

public DebugInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return result;
}
}

// 获取代理对象的工厂类
public class JdkProxyFactory {
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载器
target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler
);
}
}

// 实际使用
SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("java");

九、如何选择合适的设计模式?

在 90 秒内快速定位正确的模式,可以问自己四个问题:

  1. 什么在变化? —— 对象创建方式在变 → 创建型模式;行为在变 → 行为型模式
  2. 需要互相替换吗? —— 算法互换 → 策略模式;功能增强 → 装饰器模式
  3. 对象间如何通信? —— 一对多通知 → 观察者模式;复杂调用链 → 责任链模式
  4. 接口不兼容怎么办? —— 加个适配器

十、总结

模式 一句话 关键词
单例 全局唯一实例 共享、节省资源
工厂 封装对象创建 解耦、可扩展
观察者 状态变化自动通知 发布-订阅、松耦合
策略 算法族可互换 消除 if-else
装饰器 动态添加行为 组合优于继承
适配器 接口转换 兼容、渐进式改造
代理 控制对象访问 替身、权限控制

设计模式不是银弹,不要为了用而用。Martin Fowler 说过:“软件中的模式,与其说是规范,不如说是建议。” 当你发现某个问题天然吻合一个模式的形状时,那就是使用它的最佳时机。