「设计模式与范式」系列 Day08
写在前面
模块一花了七篇讲设计原则和思想,从这篇开始正式进入 23 种设计模式的具体内容,第一站是创建型模式。创建对象这件事,语言层面一个 new 关键字就能搞定,可现实中围绕它却衍生出单例、工厂、建造者、原型四种模式。它们到底是在解决同一个问题的四个方面,还是四个互不相干的问题?这篇先把全家福拍一张,后面四篇再逐个展开细讲。
一、是什么:四种模式各自解决的创建难题
| 模式 | 解决的问题 |
|---|---|
| 单例 | 创建一个全局唯一的对象,保证系统里任何地方拿到的都是同一份 |
| 工厂 | 创建一批不同但相关的对象,由传入的参数决定具体创建哪一种 |
| 建造者 | 创建参数很多、其中一些可选的复杂对象,按需定制化组装 |
| 原型 | 创建成本很高的对象,通过复制已有实例来创建新实例,省下重新构建的开销 |
四者共同的落脚点都是创建型模式这一个大类:
四个模式面对的具体痛点不一样------全局唯一性、多类型选择、多参数组装、创建成本------但都是把"怎么创建对象"这件事从"怎么使用对象"里单独拎出来解决,这也是 Day05 提到过的:创建型模式的本质是解耦对象的创建和使用。
二、为什么:直接 new 到底差在哪
单个 new 语句本身没有任何问题,问题出在业务场景给"创建"这个动作加了额外的约束,而 new 关键字本身满足不了这些约束:
- 单例要解决的不是"怎么造",而是"只能有一份" :直接
new谁都能造一个新实例,没有任何机制能拦住"再 new 一次",需要单例模式在类内部把住这个口子。 - 工厂要解决的是"该造哪一种" :如果调用方自己写
if (type.equals("a")) new A(); else new B();去判断该 new 哪个具体实现类,这段判断逻辑会散落在代码库的各个角落,调用方也被迫直接依赖具体实现类,换一个实现就要改所有调用点。 - 建造者要解决的是"参数太多、有些还可选" :属性一多,构造函数要么退化成一长串参数(顺序全靠背,容易传错),要么退化成一堆重载构造函数排列组合;直接先
new出对象再一个个set,对象在设置完之前又是不完整的、多线程下还不安全。 - 原型要解决的是"造一份的代价太大" :有些对象初始化要查数据库、要做复杂计算,每次都重新
new一遍代价很高,不如基于已有的实例做一次复制。
四条痛点看似各不相同,其实都是同一个根源:当创建过程本身携带了额外的约束或者成本,直接暴露一个裸的构造函数,就不足以把这些约束表达清楚,于是才需要专门的模式把"创建"这一步封装起来。
三、怎么用:四种模式的最小样子
这一节只给每种模式最简的样子,建立第一印象,具体的实现方式、常见坑留到各自专属的一篇里展开。
单例:把构造方法私有化,类自己持有并返回唯一实例:
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
}
工厂 :调用方只传参数,不关心具体创建的是哪个类,JDK 里 Calendar.getInstance() 就是这个套路,内部按时区、地区参数返回不同的 Calendar 子类实例:
java
public class ShapeFactory {
public static Shape create(String type) {
if ("circle".equals(type)) return new Circle();
if ("square".equals(type)) return new Square();
throw new IllegalArgumentException("unknown type: " + type);
}
}
建造者 :把一堆可选参数拆成链式调用,Lombok 的 @Builder 生成的就是这种代码:
java
User user = User.builder()
.name("Alice")
.age(20)
.build();
原型 :基于已有对象复制,而不是重新构建,JDK 的 ArrayList 就实现了 Cloneable:
java
public class ConfigTemplate implements Cloneable {
@Override
public ConfigTemplate clone() throws CloneNotSupportedException {
return (ConfigTemplate) super.clone();
}
}
四、面试追问
Q1:单例、工厂、建造者、原型这四种创建型模式,本质上是在解决同一个问题吗?
是同一大类问题的四个不同侧面:都是围绕"怎么创建对象"展开,把对象的创建过程从使用过程中解耦出来。具体痛点不同------单例应对全局唯一性约束,工厂应对多类型选择,建造者应对参数多且可选的复杂构造,原型应对创建成本过高------但共同的落脚点都是让创建这一步不再是一个裸的 new。
Q2:既然可以直接 new,为什么还要用单例模式和工厂模式?
new 本身没法表达额外约束。单例场景下,业务要求全局只能存在一个实例,new 关键字管不住"再造一个";工厂场景下,如果让调用方自己判断该创建哪个具体实现类,判断逻辑会散落在代码各处,调用方也会被迫直接依赖具体实现类,换实现要改所有调用点。这两种模式都是把这部分本该收敛的逻辑收进一个统一的入口。
Q3:建造者模式解决的是构造函数的什么问题?
解决参数多、其中一部分还是可选的这类复杂对象的构造问题。如果全塞进构造函数,要么退化成一长串参数(顺序全靠记,容易传错),要么退化成一堆重载构造函数的排列组合;如果先 new 出对象再逐个 set,对象在设置完整之前处于不完整状态,多线程下也不安全。建造者用链式调用把这些可选参数按需组装,构造过程本身也可以做校验。
Q4:原型模式相比直接 new 再赋值,节省的是什么成本?什么时候更适合用它?
节省的是重新构建对象要付出的初始化成本------比如要查数据库、做复杂计算才能得到的对象状态。原型模式基于已有实例做一次复制,跳过这些昂贵的初始化步骤。适用场景是"创建成本高但结构相对固定"的对象,比如某种预先加载好的配置模板、需要昂贵计算才能得到的中间结果对象。
Q5:创建型模式和后面要讲的结构型、行为型模式,核心区别是什么?
三大类模式都是在做解耦,只是解耦的对象不一样:创建型模式解耦的是对象的创建过程和使用过程;结构型模式解耦的是不同的功能模块,让它们能灵活组合;行为型模式解耦的是对象之间不同的行为/职责划分。创建型模式只管"对象怎么被造出来",一旦对象造好了,接下来该怎么组织、怎么协作,就交给结构型和行为型模式了。
下一篇预告
Day09 单例模式:饿汉、懒汉、双重检查锁、枚举实现,哪种才是真的安全。