写代码绕不开new。但当系统复杂到一定程度,你会发现到处散落的new是个大麻烦:
- 改一个类的构造方式,满世界找
new的地方改 - 想根据条件创建不同对象,写出一堆
if-else - 对象创建过程太复杂,几十行初始化代码到处复制
创建型模式就是来治这些病的。一共 5 种,下面逐个拆解。
创建型模式关心的核心问题只有一个:对象怎么来的?把"怎么创建对象"这件事封装起来,让使用者不需要知道细节,拿来就用。
一、单例模式(Singleton)
一句话
一个类在整个系统里只有一个实例,谁来要都给同一个。
生活中的例子
- 公司打印机:整个办公室共用一台打印机,不是每人发一台。你提交打印任务,和同事提交打印任务,都是发到同一台打印机。
- Windows 任务管理器:不管你按多少次 Ctrl+Shift+Esc,打开的都是同一个窗口,不会弹出来两个。
为什么需要它
有些东西天生只该有一份:
- 配置管理器:配置只需要读一次,全局共享
- 数据库连接池:连接是昂贵资源,不能每次
new一个 - 日志器:所有模块往同一个地方写日志
- 应用的全局缓存:数据只有一份,避免不同缓存实例数据不一致
如果不做限制,每个角落都new一份,轻则浪费内存,重则数据不一致。比如两个配置管理器实例,一个读了新配置,另一个还是老的——系统行为就乱套了。
示意图
代码实现
java实现(双重检查锁)
Python 实现
Python 线程安全版本:
注意事项
| 问题 | 说明 | 解法 |
|---|---|---|
| 多线程安全 | 两个线程同时判断 instance 为空,创建出两个 | 加锁 / 双重检查 / 枚举 |
| 反射攻击 | Java 反射可以强行调用私有构造器 | 枚举实现可防;或在构造器里加判断 |
| 序列化问题 | 反序列化会绕过构造器创建新对象 | 加readResolve()方法 |
| 测试困难 | 全局状态难以 mock | 配合依赖注入框架使用 |
二、工厂方法模式(Factory Method)
一句话
定义一个创建对象的接口,让子类决定实例化哪个类。
生活中的例子
- 披萨连锁店:总部定义了"做披萨"的标准流程(揉面、放料、烤制),但北京店做的是宫保鸡丁披萨,纽约店做的是芝士披萨。"做什么口味"这个决定权交给各分店。
- 物流公司:客户只说"帮我发货",但走陆运还是空运还是海运,由物流公司根据情况决定用哪种运输工具。
- 手机工厂:你下单买一部手机,富士康帮你组装。同样的产线,装不同的零件就出不同型号。
为什么需要它
假设你在做一个文档导出功能:
java
// 这样写,每加一种格式就要改这段代码 if (type.equals("pdf")) { return new PdfExporter(); } else if (type.equals("excel")) { return new ExcelExporter(); } else if (type.equals("word")) { return new WordExporter(); } // 后来又加了 csv、html、markdown... 这段代码改到崩溃每次新增格式都要回来改这段代码,改一次就有一次出 bug 的风险。这违反了开闭原则(对扩展开放,对修改关闭)。
结构图
工厂方法里有两条独立的"继承/实现链",这两条链是平行存在的,一条管"谁来造",一条管"造出来的东西长什么样"。
工厂:ExportFactory是抽象类,子类继承它
产品:Exporter是接口,实现类实现它
为什么这样设计?ExportFactory是抽象类而不是接口,因为它里面有公共代码可以复用:
java
// 这段逻辑所有工厂都一样,写在抽象类里复用 public void export(Data data, OutputStream out) { Exporter exporter = createExporter(); // 调子类实现的方法 byte[] result = exporter.render(data); out.write(result)子类只需要实现createExporter()这一个方法就够了,其余逻辑继承父类。
Exporter是接口,因为产品只需要约定"你必须有render()方法",没有任何公共代码可以共享,所以用接口更合适。
代码实现
java实现
python实现
新增格式?写一个新的 Factory + Exporter 就行,原有代码一行不动。
三、抽象工厂模式(Abstract Factory)
一句话
提供一个接口,创建一系列相关的对象,而不指定具体类。
生活中的例子
- 宜家家具套装:你选了"北欧风格",那椅子、桌子、沙发、柜子全是北欧风的。你选了"中式风格",全套都是中式。不会出现北欧椅子配中式桌子的混搭。
- 手机生态:买了苹果手机,充电器是 Lightning/USB-C、耳机是 AirPods、手表是 Apple Watch——一整套生态。买安卓则是另一套。
- 游戏皮肤:选了"暗黑主题",角色、武器、坐骑、特效全部是暗黑风格的配套。
和工厂方法的区别
- 工厂方法:一个工厂造一种产品
- 抽象工厂:一个工厂造一族产品(多种产品配套)
| 工厂方法 | 抽象工厂 | |
|---|---|---|
| 一个工厂造几种产品 | 一种 | 一族(多种) |
| 解决什么问题 | 让子类决定造哪种产品 | 保证一整套产品风格一致 |
| 例子 | PdfFactory 只造 PdfExporter | MacFactory 造 Mac 全套 UI |
| 抽象的是什么 | 一个创建方法 | 一组创建方法 |
| 创建方法 | createExporter() — 只有一个 | createButton() + createInput() + createDialog() — 多个 |
"抽象工厂"的"抽象"更准确的理解是:把创建一整族相关产品这件事抽象成了一个接口,强调的是"族",而不只是单个产品。
结构图
代码实现
java实现
python实现
四、建造者模式(Builder)
一句话
分步骤构建一个复杂对象,同样的构建过程可以创建不同的表示。
生活中的例子
- 点汉堡套餐:你可以自选面包类型、肉饼种类、是否加芝士、是否加生菜、要什么酱料、配什么饮料。一步步选,最后组装成一个完整的套餐。
- 装修房子:地板选什么材质、墙面刷什么颜色、厨房用什么台面、卫生间装什么花洒……一项项确定,最后交付一个完整的房子。
- 写简历:先填基本信息,再填教育经历,再填工作经验,再填技能……不是一次性传 20 个参数,而是分步骤填充。
为什么需要它
当一个对象有十几个参数,构造器会变成噩梦:
java
// 看到这种代码你能分清哪个参数是什么吗? new House(4, 2, true, false, true, "木质", null, 3, true, "现代");
这段代码有三个真实的痛点:
看不懂:7 个参数,没有名字,只有顺序,读代码要来回对照类定义
传错了不报错:true 和 false 顺序写反了,编译器不会提示,运行时悄悄出 bug
可选参数很麻烦:有些房子没泳池,你得要么传 false,要么写一堆重载构造器
参数一多,顺序一乱就出 bug。有些参数是可选的,有些有默认值,用构造器重载的话要写几十个重载方法。
Builder 怎么解决问题的:
java