设计模式实战:创建型五剑客——单例、工厂、建造者、原型、抽象工厂
设计模式实战:创建型五剑客——单例、工厂、建造者、原型、抽象工厂
设计模式实战:创建型五剑客——单例、工厂、建造者、原型、抽象工厂是软件工程中解决特定问题的经典方案,它提供了可复用的设计思路和代码结构。
本文深入分析了设计模式实战:创建型五剑客——单例、工厂、建造者、原型、抽象工厂的适用场景、优缺点和实现方式,是提升代码质量的重要参考。
单例模式确保一个类只有一个实例,并提供一个全局访问点。这在需要共享资源的场景中非常常见,比如:数据库连接池、线程池、Spring 的 Bean 默认就是单例。
1.2 六种实现方式演进
饿汉式(静态常量)
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}优点:写法简单,线程安全(JVM 保证类加载时实例化)。
缺点:类加载时就创建实例,如果没用就浪费内存。
懒汉式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}缺点:多线程下会创建多个实例,不推荐使用。
懒汉式(同步方法)
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}缺点:每次调用都同步,性能差。
双重检查锁(DCL)⭐ 推荐
public class Singleton {
// volatile 防止指令重排
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,避免不必要的同步
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,确保只创建一个实例
instance = new Singleton();
}
}
}
return instance;
}
}为什么需要 volatile?
new Singleton() 不是原子操作,分为三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
如果 2 和 3 被重排序,另一个线程可能拿到未初始化的对象。volatile 禁止指令重排,保证安全。
静态内部类 ⭐ 推荐
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}优点:利用类加载机制保证线程安全,且实现了懒加载(只有调用 getInstance() 时才加载 Holder 类)。
枚举 ⭐⭐ 最推荐
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("Hello from Singleton");
}
}优点:天然线程安全,防反射攻击,防反序列化破坏。Joshua Bloch 在《Effective Java》中强烈推荐。
1.3 单例的破坏与防护
反射攻击
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = constructor.newInstance();
Singleton instance2 = Singleton.getInstance();
// instance1 != instance2,单例被破坏防护:在私有构造器中加判断:
private Singleton() {
if (Holder.INSTANCE != null) {
throw new RuntimeException("单例已存在,禁止反射创建");
}
}枚举单例天然防反射:JVM 层面禁止通过反射创建枚举对象。
反序列化破坏
// 实现 Serializable 的单例,反序列化会创建新对象
ObjectInputStream ois = new ObjectInputStream(...);
Singleton instance = (Singleton) ois.readObject();
// 新对象 != 原实例防护:实现 readResolve() 方法:
protected Object readResolve() {
return getInstance();
}1.4 Spring 中的单例
Spring 的 Bean 默认作用域就是 singleton,但它的实现方式和上面不同:
// Spring 的单例是通过 ConcurrentHashMap 实现的
public class DefaultSingletonBeanRegistry {
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
protected Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
synchronized (this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = singletonFactory.getObject();
this.singletonObjects.put(beanName, singletonObject);
}
}
}
return singletonObject;
}
}注意:Spring 的单例是容器级单例,不是 JVM 级单例。一个 Spring 容器中一个 Bean 一个实例,但可以有多个容器。
1.5 避坑指南
| 坑 | 说明 | 解决方案 |
|---|---|---|
| 忘加 volatile | DCL 下指令重排导致拿到未初始化对象 | 加 volatile 关键字 |
| 单例有状态 | 多线程下状态被共享修改 | 尽量用无状态单例,或用 ThreadLocal |
| 反射破坏 | 通过反射创建新实例 | 用枚举或在构造器中防御 |
| 序列化破坏 | 反序列化创建新实例 | 实现 readResolve() |
二、工厂方法模式(Factory Method)
2.1 什么是工厂方法
定义一个创建对象的接口,但由子类决定实例化哪个类。工厂方法让类的实例化延迟到子类。
2.2 实际场景:日志记录器
假设我们需要支持多种日志存储方式(文件、数据库、消息队列):
// 产品接口
public interface Logger {
void log(String message);
}
// 具体产品:文件日志
public class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("写入文件日志: " + message);
}
}
// 具体产品:数据库日志
public class DatabaseLogger implements Logger {
@Override
public void log(String message) {
System.out.println("写入数据库日志: " + message);
}
}
// 工厂接口
public interface LoggerFactory {
Logger createLogger();
}
// 具体工厂
public class FileLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new FileLogger();
}
}
public class DatabaseLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new DatabaseLogger();
}
}使用:
LoggerFactory factory = new FileLoggerFactory();
Logger logger = factory.createLogger();
logger.log("系统启动");2.3 简单工厂(静态工厂)
严格来说简单工厂不是标准模式,但实际开发中用得最多:
public class LoggerFactory {
public static Logger createLogger(String type) {
return switch (type) {
case "file" -> new FileLogger();
case "db" -> new DatabaseLogger();
case "mq" -> new MqLogger();
default -> throw new IllegalArgumentException("未知日志类型: " + type);
};
}
}Spring 中的 FactoryBean 就是工厂模式的典型应用:
public interface FactoryBean<T> {
T getObject() throws Exception; // 生产对象
Class<?> getObjectType();
boolean isSingleton();
}2.4 避坑指南
- 不要过度设计:如果只有一两种产品,直接 new 就行,别强行上工厂
- 工厂方法 vs 简单工厂:产品类型不常变用简单工厂,常扩展用工厂方法
- Spring 中优先用 @Bean 方法:本质上就是工厂方法,交给容器管理
三、建造者模式(Builder)
3.1 什么是建造者模式
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。适合参数多、可选参数多的对象。
3.2 实际场景:构建 SQL 查询语句
public class SqlBuilder {
private String table;
private List<String> columns = new ArrayList<>();
private List<String> conditions = new ArrayList<>();
private String orderBy;
private Integer limit;
public SqlBuilder table(String table) {
this.table = table;
return this;
}
public SqlBuilder column(String column) {
this.columns.add(column);
return this;
}
public SqlBuilder where(String condition) {
this.conditions.add(condition);
return this;
}
public SqlBuilder orderBy(String orderBy) {
this.orderBy = orderBy;
return this;
}
public SqlBuilder limit(int limit) {
this.limit = limit;
return this;
}
public String build() {
StringBuilder sql = new StringBuilder("SELECT ");
sql.append(columns.isEmpty() ? "*" : String.join(", ", columns));
sql.append(" FROM ").append(table);
if (!conditions.isEmpty()) {
sql.append(" WHERE ").append(String.join(" AND ", conditions));
}
if (orderBy != null) {
sql.append(" ORDER BY ").append(orderBy);
}
if (limit != null) {
sql.append(" LIMIT ").append(limit);
}
return sql.toString();
}
}使用:
String sql = new SqlBuilder()
.table("user")
.column("id")
.column("name")
.column("email")
.where("age > 18")
.where("status = 'active'")
.orderBy("create_time DESC")
.limit(10)
.build();
// SELECT id, name, email FROM user WHERE age > 18 AND status = 'active' ORDER BY create_time DESC LIMIT 103.3 经典 Builder 模式(链式调用 + 内部 Builder 类)
public class User {
private final String name; // 必填
private final int age; // 必填
private final String email; // 可选
private final String phone; // 可选
private final String address; // 可选
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
this.phone = builder.phone;
this.address = builder.address;
}
public static class Builder {
private String name;
private int age;
private String email;
private String phone;
private String address;
public Builder(String name, int age) {
this.name = name;
this.age = age;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
// 参数校验
if (name == null || name.isEmpty()) {
throw new IllegalStateException("name 不能为空");
}
return new User(this);
}
}
}使用:
User user = new User.Builder("张三", 25)
.email("zhangsan@example.com")
.phone("13800138000")
.build();3.4 Lombok 的 @Builder 注解
实际开发中,直接用 Lombok 省力:
@Builder
@Data
public class User {
private String name;
private int age;
private String email;
private String phone;
}3.5 避坑指南
| 坑 | 说明 |
|---|---|
| 忘记校验 | build() 方法中不做校验,导致构建出无效对象 |
| 可变对象 | Builder 本身是可变的,多线程下需注意 |
| 过度使用 | 只有 2-3 个字段的对象不需要 Builder |
四、原型模式(Prototype)
4.1 什么是原型模式
用已有实例来创建新实例,而不是通过 new。Java 中通过实现 Cloneable 接口和重写 clone() 方法来实现。
4.2 浅拷贝 vs 深拷贝
浅拷贝
public class Address {
String city;
String street;
public Address(String city, String street) {
this.city = city;
this.street = street;
}
}
public class Person implements Cloneable {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Person clone() throws CloneNotSupportedException {
return (Person) super.clone(); // 浅拷贝
}
}Address address = new Address("北京", "朝阳路");
Person p1 = new Person("张三", address);
Person p2 = p1.clone();
p2.name = "李四";
p2.address.city = "上海";
System.out.println(p1.address.city); // 输出:上海!被改了!浅拷贝的问题:引用类型字段只复制引用,修改克隆对象的引用字段会影响原始对象。
深拷贝方式一:手动深拷贝
@Override
protected Person clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = new Address(this.address.city, this.address.street);
return cloned;
}深拷贝方式二:序列化/反序列化 ⭐ 推荐
@SuppressWarnings("unchecked")
public static <T> T deepCopy(T obj) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
}深拷贝方式三:JSON 序列化
// 使用 Jackson
ObjectMapper mapper = new ObjectMapper();
Person copy = mapper.readValue(mapper.writeValueAsString(original), Person.class);
// 使用 Gson
Gson gson = new Gson();
Person copy = gson.fromJson(gson.toJson(original), Person.class);4.3 避坑指南
- 优先用序列化做深拷贝,比手动 clone 更不容易出错
- 不要用 Cloneable:Joshua Bloch 说这个接口设计有缺陷,推荐用拷贝构造器或工厂方法
- 注意性能:序列化深拷贝比直接 new 慢
五、抽象工厂模式(Abstract Factory)
5.1 什么是抽象工厂
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。简单说:工厂的工厂。
5.2 实际场景:跨数据库基础设施
假设系统需要支持 MySQL 和 PostgreSQL,每种数据库都有 Connection、Statement、ResultSet:
// 抽象产品
public interface DbConnection {
void connect();
}
public interface DbStatement {
void execute(String sql);
}
// MySQL 实现
public class MySqlConnection implements DbConnection {
@Override
public void connect() {
System.out.println("MySQL 连接");
}
}
public class MySqlStatement implements DbStatement {
@Override
public void execute(String sql) {
System.out.println("MySQL 执行: " + sql);
}
}
// PostgreSQL 实现
public class PgConnection implements DbConnection {
@Override
public void connect() {
System.out.println("PostgreSQL 连接");
}
}
public class PgStatement implements DbStatement {
@Override
public void execute(String sql) {
System.out.println("PostgreSQL 执行: " + sql);
}
}
// 抽象工厂
public interface DatabaseFactory {
DbConnection createConnection();
DbStatement createStatement();
}
// MySQL 工厂
public class MySqlFactory implements DatabaseFactory {
@Override
public DbConnection createConnection() {
return new MySqlConnection();
}
@Override
public DbStatement createStatement() {
return new MySqlStatement();
}
}
// PostgreSQL 工厂
public class PgFactory implements DatabaseFactory {
@Override
public DbConnection createConnection() {
return new PgConnection();
}
@Override
public DbStatement createStatement() {
return new PgStatement();
}
}使用:
public class Application {
private final DatabaseFactory factory;
public Application(DatabaseFactory factory) {
this.factory = factory;
}
public void run() {
DbConnection conn = factory.createConnection();
conn.connect();
DbStatement stmt = factory.createStatement();
stmt.execute("SELECT * FROM user");
}
}
// 根据配置选择工厂
DatabaseFactory factory = "mysql".equals(config.getDbType())
? new MySqlFactory()
: new PgFactory();
new Application(factory).run();5.3 抽象工厂 vs 工厂方法
| 维度 | 工厂方法 | 抽象工厂 |
|---|---|---|
| 关注点 | 创建单一产品 | 创建产品族 |
| 扩展方式 | 新增工厂子类 | 新增工厂实现 |
| 典型场景 | 日志记录器 | 跨数据库/跨 UI 主题 |
5.4 避坑指南
- 产品族固定时用抽象工厂,产品经常新增时用工厂方法
- 开闭原则的妥协:新增产品族容易(加新工厂),但新增产品类型难(要改所有工厂接口)
六、五种模式对比总结
| 模式 | 核心问题 | 一句话总结 | Spring 中的应用 |
|---|---|---|---|
| 单例 | 全局唯一实例 | 一个类只造一个对象 | Bean 默认单例 |
| 工厂方法 | 创建单一产品 | 子类决定造什么 | FactoryBean |
| 建造者 | 复杂对象构建 | 分步构建,链式调用 | StringBuilder、BeanDefinitionBuilder |
| 原型 | 复制已有对象 | 克隆而非 new | prototype 作用域 |
| 抽象工厂 | 创建产品族 | 工厂的工厂 | 不同 DataSource 工厂 |
七、面试要点
高频面试题
Q1:单例模式有几种实现方式?哪种最好?
答:常见六种——饿汉式、懒汉式(线程不安全/同步方法)、DCL、静态内部类、枚举。推荐枚举(防反射、防序列化、线程安全)或静态内部类(懒加载、线程安全)。DCL 需要加 volatile。
Q2:DCL 单例为什么要加 volatile?
答:new 操作不是原子的,分为分配内存、初始化、赋值引用三步。如果没有 volatile,JVM 可能指令重排为先赋值再初始化,其他线程可能拿到未初始化的对象。
Q3:Spring 的单例和 GoF 的单例有什么区别?
答:GoF 单例是 JVM 级别唯一(类加载器级别),Spring 单例是容器级别唯一(每个 ApplicationContext 中一份)。Spring 通过 ConcurrentHashMap + DCL 实现。
Q4:工厂方法和抽象工厂的区别?
答:工厂方法创建单一产品,通过子类扩展产品类型;抽象工厂创建产品族,通过新工厂实现扩展产品族。工厂方法是抽象工厂的退化形式。
Q5:建造者模式和工厂模式的区别?
答:工厂模式关注"创建什么",建造者模式关注"怎么创建"。工厂直接产出完整对象,建造者分步构建复杂对象。参数少用工厂,参数多用建造者。
Q6:原型模式的深拷贝怎么实现?
答:三种方式——手动 clone(逐层拷贝引用字段)、序列化/反序列化(推荐)、JSON 序列化(简单但性能略差)。
面试加分项
- 能说出设计模式在 Spring 中的应用
- 能分析每种模式的优缺点和适用场景
- 能手写 DCL 单例并解释 volatile 的作用
- 知道单例被反射/序列化破坏的原理和防护方式
总结
创建型模式是设计模式的基础,核心就是"怎么创建对象":
- 单例:全局唯一,注意线程安全和防破坏
- 工厂方法:单一产品,子类决定
- 抽象工厂:产品族,工厂的工厂
- 建造者:复杂对象分步构建,链式调用
- 原型:克隆已有对象,注意深浅拷贝
实际开发中,不一定非要严格按 UML 类图来写。重要的是理解思想——将对象创建与使用分离,让代码更灵活。Spring 框架本身就是创建型模式的教科书,多读 Spring 源码对理解这些模式大有裨益。