第一篇 创建型模式:把 new 藏起来
创建型模式解决的都是同一个问题的不同侧面:new 这个动作太硬了。
当你写 new MySQLConnection(...) 的时候,你把三件事焊死在了一起:要造什么、怎么造、造完给谁。以后想换成 PostgreSQL,想加个连接池,想在测试里换成内存假实现------每一件都要改这行代码所在的地方,而那个地方可能有一百个。
创建型模式的共同手法是:把「造对象」这个动作从业务代码里抽出来,交给一个专门的角色。 业务代码从此只说「给我一个能连数据库的东西」,不说「给我一个 MySQLConnection」。
第 1 章 单例模式 Singleton:全世界只能有一个
1.1 场景:配置读了三遍
你的服务启动时读了一次配置,中间某个工具类里又读了一次,某个 SDK 初始化时又读了一次。三次读之间,运维改了一次配置文件------于是你的服务在半小时里,一半逻辑用旧配置,一半用新配置,线上出现了一堆「怎么都复现不了」的怪现象。
或者更常见的:连接池。你写了 new ConnectionPool(),然后在三个地方各 new 了一次。三个池,各占 20 个连接,数据库最大连接数 50。恭喜,第五分钟开始报错。
一句话本质: 保证一个类只有一个实例,并提供一个全局访问点。
它真正在解决的是: 某个「有状态且必须唯一」的资源,不能被复制。
1.2 为什么不能只用 static
很多人的第一反应是「全用 static 不就行了」。能解决一半问题,但 static 有三个硬伤:
- 不能实现接口。 Java 的 static 方法不参与多态,你没法在测试时用一个假实现替换它。
- 初始化时机不可控。 类一加载就初始化,可能拖慢启动,也可能在依赖还没准备好时就跑起来。
- 生命周期不受控。 没法延迟创建、没法销毁重建。
单例是「有对象、但只有一个」,static 是「没有对象」。这个区别在需要多态、需要延迟初始化、需要资源释放时非常关键。
1.3 角色表
|-------------|------------------------------------------------|
| 角色 | 职责 |
| Singleton 类 | 私有构造器(堵死外部 new)+ 私有静态实例(存唯一实例)+ 公有静态获取方法(唯一入口) |
三件事缺一不可。少一个私有构造器,别人照样能 new;少一个静态字段,每次都新建;少一个获取方法,你拿不到。
1.4 Java 实现:从「能跑」到「正确」的三级跳
第一版,饿汉式: 类加载时就创建,天生线程安全,简单。
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public final class ConfigCenter { private static final ConfigCenter INSTANCE = new ConfigCenter(); private final Map<String, String> data = new HashMap<>(); private ConfigCenter() { // 私有构造:外部无法 new data.put("timeout", "3000"); } public static ConfigCenter getInstance() { return INSTANCE; } public String get(String key) { return data.get(key); }} |
缺点:不管用不用,类一加载就创建。如果初始化很重(比如要读远程配置),会拖慢启动。
第二版,懒汉 + 双重检查锁: 第一次调用时才创建。
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public final class ConfigCenter { private static volatile ConfigCenter instance; // volatile 不能省 private final Map<String, String> data = new HashMap<>(); private ConfigCenter() {} public static ConfigCenter getInstance() { if (instance == null) { // 第一次检查:避免每次都加锁 synchronized (ConfigCenter.class) { if (instance == null) { // 第二次检查:防止重复创建 instance = new ConfigCenter(); } } } return instance; }} |
坑: volatile 少了就是 bug。instance = new ConfigCenter() 不是原子操作,它分三步:分配内存、初始化对象、把引用指向内存。JVM 可能重排成 1-3-2,此时另一个线程在第一次检查时看到 instance != null,直接返回一个「半成品」对象------字段全是默认值。这个 bug 极难复现,因为它取决于 CPU 和 JIT 的即时状态。
第三版,静态内部类: 兼顾懒加载与线程安全,而且不用加锁。这是我在生产代码里最推荐的写法。
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public final class ConfigCenter { private ConfigCenter() {} private static class Holder { // 外部类加载时不会初始化它 private static final ConfigCenter INSTANCE = new ConfigCenter(); } public static ConfigCenter getInstance() { return Holder.INSTANCE; // 首次调用触发 Holder 初始化,由 JVM 保证线程安全 }} |
还有第四版,枚举: 《Effective Java》作者 Joshua Bloch 推荐的做法,天生防反射攻击、防反序列化破坏。
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public enum ConfigCenter { INSTANCE; private final Map<String, String> data = new HashMap<>(); public String get(String key) { return data.get(key); } public void put(String key, String value) { data.put(key, value); }}// 使用:ConfigCenter.INSTANCE.get("timeout") |
1.5 Python 实现:三种写法,一个真相
写法一, new 控制实例创建:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| class ConfigCenter: _instance = None def new(cls): if cls._instance is None: cls._instance = super().new(cls) cls._instance._data = {"timeout": "3000", "retry": "3"} return cls._instance def get(self, key: str): return self._data.get(key)a, b = ConfigCenter(), ConfigCenter()print(a is b) # True |
写法二,用装饰器(更通用):
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| def singleton(cls): instances = {} def wrapper(*args, **kwargs): if cls not in instances: instancescls = cls(*args, **kwargs) return instancescls return wrapper@singletonclass ConnectionPool: def init(self, size: int = 10): self.size = size self.conns = \[\]p1, p2 = ConnectionPool(20), ConnectionPool(5)print(p1 is p2, p1.size) # True 20 ------ 第二次的参数被忽略了! |
坑: 上面这个装饰器会「静默忽略」第二次传入的参数。调用方以为拿到的池子大小是 5,实际是 20。单例的所有实现都有这个陷阱:后续调用的参数无处安放 。所以单例的初始化参数,最好来自配置而非调用方。
写法三,模块级单例------这是 Python 里最该用的写法:
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| # config.py_data = {"timeout": "3000", "retry": "3"}def get(key: str): return _data.get(key)def put(key: str, value: str): _datakey = value# 其它任何文件# from config import get# get("timeout") |
Python 的真相: 模块在 Python 里只被导入一次,模块级的变量天然就是单例。你不需要为了单例去写一个类 。只有当这个资源需要继承、需要多态、需要延迟初始化时,才值得上类。这是 Python 与 Java 最大的观念差异之一:Java 用类来组织一切,Python 用模块和函数就够了。
1.6 双语言对照
|--------|-------------------------------------|-------------------------|
| 维度 | Java | Python |
| 推荐写法 | 静态内部类 / 枚举 | 模块级变量 + 函数 |
| 线程安全 | 需要显式处理(volatile、synchronized、类初始化锁) | 模块导入有 GIL 与导入锁保护,通常无需处理 |
| 防止被破坏 | 反射可破私有构造(枚举除外)、反序列化可造新实例 | 几乎防不住,也没人在乎 |
| 本质差异 | 单例是「受控的全局状态」 | 单例是「模块的默认行为」 |
1.7 记忆锚点
口诀: 私构造、静字段、公方法,三件套。
联想: 一个国家只有一个首都。你可以坐飞机去(getInstance),但你不能自己再造一个首都(私有构造)。
一眼识别: 类里出现 private static 的自类型字段 + private 构造器 → 单例。
1.8 什么时候用,什么时候别用
该用: 配置中心、连接池、日志器、缓存、ID 生成器、Spring 里的 Bean(默认单例)。判据是「这个资源重复创建会出问题 」------而不是「这个资源创建起来有点慢」。
别用: 只要这个对象没有「必须唯一」的语义,就别单例。单例最被诟病的三点:
- 它是个伪装成对象的全局变量。 任何地方都能改它,任何地方的 bug 都可能是它造成的。
- 它让测试变难。 测试之间共享状态,跑 A 测试污染了 B 测试。这是单元测试里最经典的翻车方式。
- 它隐藏了依赖。 一个类用了单例,你在构造函数签名上看不出来。重构时你根本不知道谁依赖它。
现代做法: 把单例「交给容器管」。Spring 的 @Component、Python 的依赖注入框架,本质都是「容器保证单例,但你的类本身不知道自己被单例了」。这样既保证了唯一,又保留了可测试性------因为容器可以在测试时给你塞一个假的。这就是**「单例是一种生命周期策略,而不是一种类设计」** 的思想。
1.9 常见坑清单
|------------------|---------------|-----------------------|
| 坑 | 后果 | 规避 |
| 双重检查锁忘了 volatile | 拿到半初始化对象,随机崩溃 | 用静态内部类或枚举 |
| 单例里存了可变状态 | 多线程互相踩数据 | 状态尽量不可变,或加锁 |
| 在单例构造器里做重活 | 启动慢、可能死锁 | 懒加载或改为显式 init() |
| 单例被反射/序列化绕过 | 出现两个实例 | 枚举实现;或 readResolve() |
| 分布式环境下当全局唯一 | 多台机器各有各的「唯一」 | 跨进程唯一要靠 Redis/DB/etcd |
1.10 三十秒自测
- 单例和全 static 方法的类,本质区别是什么? 2. 为什么双重检查锁需要 volatile? 3. Python 里为什么说「模块就是单例」? 4. 你的单例在集群部署下还唯一吗?
第 2 章 工厂方法模式 Factory Method:造什么,子类说了算
2.1 场景:物流系统要支持新渠道
你写了一个物流调度类:
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public class Logistics { public void planDelivery(String goods, String type) { if ("truck".equals(type)) { Truck t = new Truck(); t.deliver(goods); } else if ("ship".equals(type)) { Ship s = new Ship(); s.deliver(goods); } else if ("air".equals(type)) { Airplane a = new Airplane(); a.deliver(goods); } }} |
现在要加「无人机配送」。你要改这个类的 if-else。同时你发现,卡车和轮船的调度流程其实有细微差别(卡车要看路况,轮船要看港口),于是 planDelivery 里又多了几个 if。
问题不在 if,问题在于:「创建什么」和「怎么使用」被写在了一起。 每次新增产品,都要回来改这段使用逻辑。
一句话本质: 定义一个创建对象的接口,但让子类决定实例化哪个类。工厂方法让类的实例化延迟到子类。
它的结构特征: 父类里有一句 createXxx() 的抽象调用,子类各自实现它。
2.2 关键一步:把「变的部分」提出来
这个模式的精髓可以用一句话说清:
父类写流程,子类写产品。 流程里需要产品的那一行,调用一个抽象方法 createTransport();具体造卡车还是轮船,由子类填。
这就是所谓的「工厂方法」------一个方法,专门负责造对象。 它不是工厂类,是一个方法。
2.3 Java 实现
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| // 产品接口interface Transport { void deliver(String goods);}class Truck implements Transport { public void deliver(String goods) { System.out.println("卡车陆运:" + goods); }}class Ship implements Transport { public void deliver(String goods) { System.out.println("轮船海运:" + goods); }}// 创建者:流程在这里,产品由子类提供abstract class Logistics { protected abstract Transport createTransport(); // ← 工厂方法,核心就这一行 public void planDelivery(String goods) { // 流程骨架,稳定不变 System.out.println("1. 接单 " + goods); Transport t = createTransport(); // 不写 new,交给子类 System.out.println("2. 装货"); t.deliver(goods); System.out.println("3. 签收"); }}class RoadLogistics extends Logistics { protected Transport createTransport() { return new Truck(); }}class SeaLogistics extends Logistics { protected Transport createTransport() { return new Ship(); }}// 使用public class Main { public static void main(String\[\] args) { Logistics l = new SeaLogistics(); l.planDelivery("100 台冰箱"); }} |
注意这里发生了什么: planDelivery 里没有一个 new Truck(),也没有一个 if。加无人机?新建 AirLogistics 类和 Drone 类,不改任何旧文件 。这就是开闭原则。
2.4 Python 实现
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from abc import ABC, abstractmethodclass Transport(ABC): @abstractmethod def deliver(self, goods: str) -> None: ...class Truck(Transport): def deliver(self, goods: str) -> None: print(f"卡车陆运:{goods}")class Ship(Transport): def deliver(self, goods: str) -> None: print(f"轮船海运:{goods}")class Logistics(ABC): @abstractmethod def create_transport(self) -> Transport: ... # 工厂方法 def plan_delivery(self, goods: str) -> None: # 流程骨架 print(f"1. 接单 {goods}") t = self.create_transport() print("2. 装货") t.deliver(goods) print("3. 签收")class RoadLogistics(Logistics): def create_transport(self) -> Transport: return Truck()class SeaLogistics(Logistics): def create_transport(self) -> Transport: return Ship()SeaLogistics().plan_delivery("100 台冰箱") |
Pythonic 变体:用注册表 + 函数,比继承更轻。 当产品种类多、但流程差异不大时,注册表写法更受欢迎:
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from typing import Callable, Dict_registry: Dictstr, Callable\[\[, Transport]] = {}def register(kind: str): def deco(factory: Callable\[, Transport]): _registrykind = factory return factory return deco@register("truck")def _make_truck() -> Transport: return Truck()@register("ship")def _make_ship() -> Transport: return Ship()def create_transport(kind: str) -> Transport: if kind not in _registry: raise ValueError(f"未知运输方式: {kind}") return _registrykind()create_transport("ship").deliver("100 台冰箱") |
这个写法的好处:新增产品只要写一个带 @register 的函数,连继承都不用 。坏处:注册表是隐式的,读代码时不容易看出有哪些产品(要靠全局搜索 @register)。
2.5 双语言对照
|---------|------------------------------------------|----------------------------------|
| 维度 | Java | Python |
| 典型实现 | 抽象类 + 抽象方法 + 子类 | ABC + 抽象方法;或注册表 + 函数 |
| 为什么必须抽象 | 没有抽象方法就没法强制子类实现 | 同;但 Python 靠「约定 + ABC 校验」,运行期才报错 |
| 更轻的替代 | 静态工厂方法(static Transport of(String type)) | 字典映射 / 函数 / functools.partial |
| 常见误用 | 为了一个产品就搞两层继承 | 用 if-else 冒充工厂 |
2.6 记忆锚点
口诀: 父类定流程,子类定产品;一个抽象方法,撑起一条产品线。
联想: 总部的《开分店手册》里写着「第一步选址,第二步装修,第三步按当地口味定菜单 ,第四步开业」。手册是父类,菜单是工厂方法,各地分店是子类。
一眼识别: 抽象类里有一个返回产品类型的 abstract 方法,且被自己的其它方法调用。
2.7 什么时候用,什么时候别用
该用: ① 你事先不知道要创建哪个类的实例;② 你希望子类来决定创建什么;③ 有一批产品共享同一套使用流程,只是产品本身不同。
别用: 只有一种产品、且短期内不会增加时,直接 new 就好。为「可能的未来」预先抽象,是最常见的过度设计。
坑: 工厂方法会让类的数量翻倍(每个产品一个创建者)。如果你有 20 个产品、且它们的使用流程完全一样,那你不该用工厂方法,而应该用一个简单工厂 (一个类 + 一个 switch/字典)------它不属于 GoF 23 个模式,但实用得多。区别是:简单工厂违反开闭原则(加产品要改 switch),工厂方法遵守,代价是类爆炸。这就是「后果」要素要你权衡的东西。
2.8 三十秒自测
- 工厂方法和「简单工厂」的核心区别是什么? 2. 为什么说工厂方法体现了开闭原则? 3. 如果产品的创建逻辑有复杂参数,工厂方法还合适吗?(提示:该上抽象工厂或建造者了)
第 3 章 抽象工厂模式 Abstract Factory:成套地造
3.1 场景:主题皮肤不能混搭
你的后台系统要支持「浅色主题」和「暗黑主题」。每个主题下都有按钮、输入框、下拉框、弹窗。设计师反复强调:同一套界面里,不能出现浅色按钮配暗色输入框。
用工厂方法,你会得到 LightButtonFactory、DarkButtonFactory、LightInputFactory......然后在业务代码里小心翼翼地 new 出配套的一批。只要有一个地方手滑,就混搭了。
问题变成了:怎么保证「一整套产品」要么全浅色,要么全暗色?
一句话本质: 提供一个接口,用于创建一系列相关或相互依赖的对象 ,而无需指定它们的具体类。
与工厂方法的差别: 工厂方法造一个 产品;抽象工厂造一族 产品,并保证它们配套。
3.2 结构:一个接口,N 个产品方法
|-----------------|-------------|----------------------------------------|
| 角色 | 职责 | 本例 |
| AbstractFactory | 声明创建一族产品的方法 | UIFactory:createButton()、createInput() |
| ConcreteFactory | 实现一族中的具体产品 | LightFactory、DarkFactory |
| AbstractProduct | 每类产品的接口 | Button、Input |
| ConcreteProduct | 具体实现 | LightButton、DarkInput... |
3.3 Java 实现
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| // 产品族 Ainterface Button { void render(); }class LightButton implements Button { public void render() { System.out.println("浅色按钮"); } }class DarkButton implements Button { public void render() { System.out.println("暗色按钮"); } }// 产品族 Binterface Input { void render(); }class LightInput implements Input { public void render() { System.out.println("浅色输入框"); } }class DarkInput implements Input { public void render() { System.out.println("暗色输入框"); } }// 抽象工厂:一个方法对应一族中的一个产品interface UIFactory { Button createButton(); Input createInput();}// 具体工厂:保证配套class LightFactory implements UIFactory { public Button createButton() { return new LightButton(); } public Input createInput() { return new LightInput(); }}class DarkFactory implements UIFactory { public Button createButton() { return new DarkButton(); } public Input createInput() { return new DarkInput(); }}// 业务代码:只依赖抽象工厂,永远不可能混搭class LoginPage { private final Button button; private final Input input; public LoginPage(UIFactory factory) { this.button = factory.createButton(); this.input = factory.createInput(); } public void show() { input.render(); button.render(); }}// 使用new LoginPage(new DarkFactory()).show(); |
关键点: LoginPage 只认识 UIFactory。它没有能力混搭,因为「造按钮」和「造输入框」必须走同一个工厂对象。混搭在类型系统层面 就被堵死了。
3.4 Python 实现
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from abc import ABC, abstractmethodclass Button(ABC): @abstractmethod def render(self) -> None: ...class Input(ABC): @abstractmethod def render(self) -> None: ...class LightButton(Button): def render(self): print("浅色按钮")class DarkButton(Button): def render(self): print("暗色按钮")class LightInput(Input): def render(self): print("浅色输入框")class DarkInput(Input): def render(self): print("暗色输入框")class UIFactory(ABC): @abstractmethod def create_button(self) -> Button: ... @abstractmethod def create_input(self) -> Input: ...class LightFactory(UIFactory): def create_button(self): return LightButton() def create_input(self): return LightInput()class DarkFactory(UIFactory): def create_button(self): return DarkButton() def create_input(self): return DarkInput()class LoginPage: def init(self, factory: UIFactory): self.button = factory.create_button() self.input = factory.create_input() def show(self): self.input.render() self.button.render()LoginPage(DarkFactory()).show() |
Pythonic 变体:用「数据 + 工厂函数」代替两个类。 因为 Python 的类可以当值传递,一族产品可以塞进一个字典:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| THEMES = { "light": {"button": LightButton, "input": LightInput}, "dark": {"button": DarkButton, "input": DarkInput},}def make_ui(theme: str): pack = THEMEStheme return pack"button"(), pack"input"()btn, inp = make_ui("dark") |
这在产品少、变体少的时候更直观。代价是失去了类型约束------你可以在 THEMES 里塞一个不配套的组合,编译器不会拦你。
3.5 双语言对照与记忆
|--------|--------------------|-----------------|
| 维度 | Java | Python |
| 约束强度 | 编译期强约束,混搭直接编不过 | 运行期才可能暴露,靠自觉与测试 |
| 扩展新族 | 新增一个 Factory 类,容易 | 新增一个字典项或类,容易 |
| 扩展新产品 | 要改接口和所有具体工厂 ,难 | 同样要改所有族,难 |
口诀: 一族产品一个厂,配套不混搭。
联想: 买西装要买成套的(外套 + 裤子 + 领带)。抽象工厂就是那家「成套出售」的店,你不能只要外套不要裤子。
一眼识别: 一个接口里有多个 createXxx() 方法,且实现类成对出现。
3.6 什么时候用,什么时候别用
该用: 产品有「族」的概念且必须配套(跨平台 UI、多数据库方言、多支付渠道的整套 API、游戏的多套美术资源)。
别用: 产品之间没有配套约束时,用工厂方法或简单工厂就够。抽象工厂的致命弱点是**「加新产品很难」** ------你加一个 createCheckbox(),所有具体工厂都要改。所以它适合「产品族稳定、族数量会增长」的场景,不适合「产品种类频繁增加」的场景。
坑: 把抽象工厂当成「万能工厂」,一个接口塞 15 个 create 方法。这时候它已经不是抽象工厂了,是一个「上帝工厂」。正确做法是按职责拆成多个工厂接口(接口隔离原则)。
第 4 章 建造者模式 Builder:分步组装复杂对象
4.1 场景:十三个参数的构造函数
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public HttpRequest(String url, String method, Map<String,String> headers, String body, int timeout, int retry, boolean followRedirect, String proxy, boolean gzip, String userAgent, ...) { } |
调用方写起来是这样:
|----------------------------------------------------------------------------------------------------------|
| new HttpRequest("https://api.x.com", "POST", headers, body, 3000, 3, true, null, false, "Mozilla", ...); |
三个月后你自己都看不懂这行代码:true 是什么?null 是哪个参数?想只传 url 和 timeout?做不到,中间的全得填。
一句话本质: 把一个复杂对象的构建过程 与它的表示 分离,使得同样的构建过程可以创建不同的表示。
说人话: 对象太复杂,一次 new 不完,那就一步一步来,每步给个名字,最后 build() 交货。
4.2 两种 Builder,别搞混
|--------------------------|--------------------------------|--------------------------------------------|-------------------|
| 类型 | 特征 | 典型例子 | 是否需要 Director |
| 链式 Builder (实用派) | 每个 setter 返回 this,最后 build | OkHttp 的 Request.Builder、Lombok 的 @Builder | 不需要 |
| GoF 经典 Builder (学术派) | 有 Director 编排步骤,Builder 提供步骤实现 | 文档转换器、套餐组装 | 需要 |
工作中 95% 的场景用链式 Builder。但你要知道经典 Builder 的存在------当「构建步骤的顺序」本身就是业务逻辑时(比如「必须先登录再下单」),Director 才有意义。
4.3 Java 实现:链式 Builder
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public final class HttpRequest { private final String url; // 必填 private final String method; private final Map<String, String> headers; private final String body; private final int timeoutMs; private HttpRequest(Builder b) { // 私有构造,只能由 Builder 造 this.url = b.url; this.method = b.method; this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(b.headers)); this.body = b.body; this.timeoutMs = b.timeoutMs; } public static Builder builder(String url) { return new Builder(url); } public static class Builder { private final String url; private String method = "GET"; // 默认值集中在这里 private final Map<String, String> headers = new LinkedHashMap<>(); private String body; private int timeoutMs = 3000; public Builder(String url) { if (url == null || url.isEmpty()) throw new IllegalArgumentException("url 必填"); this.url = url; } public Builder method(String m) { this.method = m; return this; } public Builder header(String k, String v){ headers.put(k, v); return this; } public Builder body(String b) { this.body = b; return this; } public Builder timeout(int ms) { this.timeoutMs = ms; return this; } public HttpRequest build() { return new HttpRequest(this); // 校验也可以放在这里 } }}// 使用:读起来像一句话HttpRequest req = HttpRequest.builder("https://api.x.com/order") .method("POST") .header("Content-Type", "application/json") .body("{\"id\":1}") .timeout(5000) .build(); |
为什么值得这么写: ① 参数有名字,可读性从「猜」变成「读」;② 只传你关心的,其余走默认值;③ 对象创建完就不可变(final 字段),天然线程安全;④ 校验集中在一处。
4.4 Python 实现:两种风格
风格一,链式(贴近 Java 的写法):
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| class HttpRequest: def init(self, b: "HttpRequest.Builder"): self.url = b.url self.method = b.method self.headers = dict(b.headers) self.body = b.body self.timeout_ms = b.timeout_ms @staticmethod def builder(url: str) -> "HttpRequest.Builder": return HttpRequest.Builder(url) class Builder: def init(self, url: str): if not url: raise ValueError("url 必填") self.url = url self.method = "GET" self.headers = {} self.body = None self.timeout_ms = 3000 def method(self, m: str) -> "HttpRequest.Builder": self.method = m return self def header(self, k: str, v: str) -> "HttpRequest.Builder": self.headersk = v return self def body(self, b: str) -> "HttpRequest.Builder": self.body = b return self def timeout(self, ms: int) -> "HttpRequest.Builder": self.timeout_ms = ms return self def build(self) -> "HttpRequest": return HttpRequest(self)req = (HttpRequest.builder("https://api.x.com/order") .method("POST") .header("Content-Type", "application/json") .body('{"id":1}') .timeout(5000) .build()) |
风格二,关键字参数 + dataclass(Python 的「原生 Builder」):
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from dataclasses import dataclass, fieldfrom typing import Optional@dataclass(frozen=True)class HttpRequest: url: str method: str = "GET" headers: dict = field(default_factory=dict) body: Optionalstr = None timeout_ms: int = 3000req = HttpRequest( url="https://api.x.com/order", method="POST", body='{"id":1}', timeout_ms=5000,) |
Python 的真相: 关键字参数 + 默认值,本身就是 Builder 模式的「语言级实现」。frozen=True 给了你不可变性,field(default_factory=...) 解决了可变默认值陷阱。当你发现自己在 Python 里写链式 Builder 时,先问一句:是不是关键字参数就够了? 链式 Builder 只在两种情况下还有价值:① 需要在 build 时做复杂校验或转换;② 需要「分步构建、中间状态被传递」的场景(比如生成器式的文档构造)。
4.5 双语言对照与记忆
|--------------|---------------|-------------------------|
| 维度 | Java | Python |
| 最常用写法 | 静态内部类 + 链式方法 | 关键字参数 + dataclass |
| 为什么 Java 需要它 | 没有默认参数、没有命名参数 | 语言自带,很多时候不需要模式 |
| 不可变怎么保证 | final + 防御性拷贝 | @dataclass(frozen=True) |
口诀: 必填进构造,可选走链式,最后 build 交货。
联想: 点奶茶。杯型必选(构造参数),糖度、冰量、加料一个个报(链式方法),最后「就这些」(build)。你不会一口气报十三个参数,店员也记不住。
一眼识别: 有一个 static class Builder,方法全返回 this,末尾一个 build()。
4.6 什么时候用,什么时候别用
该用: 构造参数超过 4 个;有大量可选参数;对象构建有顺序或校验要求;需要不可变对象。
别用: 参数就两三个,直接构造器最清楚。Builder 会多出一个类、多一层间接,读代码时要跳两次。
坑: ① Builder 里忘了做参数校验,非法对象照样被造出来;② build() 里没有做防御性拷贝,外部改了集合,你的「不可变对象」跟着变;③ 用 Builder 造只有两个字段的小对象,纯属自嗨。
第 5 章 原型模式 Prototype:复制比新建便宜
5.1 场景:一千个配置几乎一样的对象
你要造 1000 个「报表任务」。它们的 90% 字段一样(数据源、模板、收件人、时区),只有名称和 cron 表达式不同。每个都从头 new,意味着 1000 次数据库查询、1000 次模板解析。
更麻烦的是:这些对象的初始化过程有副作用(要注册监听器、要占资源),重复 1000 次代价很高。
一句话本质: 用原型实例指定要创建的对象种类,并通过拷贝这个原型 来创建新对象。
说人话:造不如抄。有一个现成的,复制一份改改就行------前提是拷贝确实比重新构造便宜。
5.2 核心难点:深拷贝还是浅拷贝
原型模式 90% 的坑都在这里。
|---------|------------------------|--------------------------------|
| 类型 | 行为 | 风险 |
| 浅拷贝 | 只复制对象本身,引用类型字段仍指向同一块内存 | 改副本的 list,原型的 list 也变了 |
| 深拷贝 | 递归复制所有引用对象 | 性能开销大;循环引用会死循环;某些对象(连接、锁)根本不能拷 |
5.3 Java 实现
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| import java.util.*;public class ReportTask implements Cloneable { private String name; private String cron; private List<String> recipients; // 引用类型,必须深拷 private Map<String, String> params; // 引用类型,必须深拷 public ReportTask(String name, String cron, List<String> recipients, Map<String, String> params) { this.name = name; this.cron = cron; this.recipients = new ArrayList<>(recipients); this.params = new HashMap<>(params); } @Override public ReportTask clone() { try { ReportTask copy = (ReportTask) super.clone(); // 浅拷贝:基本类型已 OK copy.recipients = new ArrayList<>(this.recipients); // 手动深拷引用字段 copy.params = new HashMap<>(this.params); return copy; } catch (CloneNotSupportedException e) { throw new AssertionError(e); // 实现了 Cloneable 就不会走到这里 } } public void setName(String name) { this.name = name; } public void setCron(String cron) { this.cron = cron; } public List<String> getRecipients() { return recipients; }}// 使用:一份原型,批量复制ReportTask proto = new ReportTask("日报", "0 8 * * *", List.of("boss@x.com"), Map.of("format", "pdf"));List<ReportTask> tasks = new ArrayList<>();for (int i = 0; i < 1000; i++) { ReportTask t = proto.clone(); // 不再查库、不再解析模板 t.setName("日报-" + i); t.setCron("0 " + i % 24 + " * * *"); tasks.add(t);} |
坑: super.clone() 只做浅拷贝,而且它「不调用构造器」。如果类里有 final 字段或依赖构造器初始化的不变量,Cloneable 会让你很难受。所以《Effective Java》直接建议:别用 Cloneable,用拷贝构造器 :
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public ReportTask(ReportTask other) { // 拷贝构造器,清晰、无副作用 this.name = other.name; this.cron = other.cron; this.recipients = new ArrayList<>(other.recipients); this.params = new HashMap<>(other.params);} |
5.4 Python 实现:语言直接给了你
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| import copyfrom dataclasses import dataclass, fieldfrom typing import List, Dict@dataclassclass ReportTask: name: str cron: str recipients: Liststr = field(default_factory=list) params: Dictstr, str = field(default_factory=dict)proto = ReportTask("日报", "0 8 * * *", "boss@x.com", {"format": "pdf"})# 浅拷贝:引用字段共享shallow = copy.copy(proto)shallow.recipients.append("me@x.com")print(proto.recipients) # 'boss@x.com', 'me@x.com' ← 原型被污染了!# 深拷贝:完全独立proto2 = ReportTask("日报", "0 8 * * *", "boss@x.com", {"format": "pdf"})deep = copy.deepcopy(proto2)deep.recipients.append("me@x.com")print(proto2.recipients) # 'boss@x.com' ← 干净 |
自定义拷贝行为: 当对象里有「不能拷」的东西(连接、文件句柄、锁),重写魔术方法:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| class ReportTask: def init(self, name, cron, conn): self.name = name self.cron = cron self.conn = conn # 数据库连接,不能复制 def copy(self): # 浅拷贝:连接直接共享引用 new = self.class.new(self.class) new.dict.update(self.dict) return new def deepcopy(self, memo): new = self.class.new(self.class) memoid(self) = new # 处理循环引用 for k, v in self.dict.items(): if k == "conn": new.conn = v # 连接不深拷,直接共享 else: new.dictk = copy.deepcopy(v, memo) return new |
Python 的真相: copy.deepcopy 是「原型模式的通用实现」,它用 memo 字典处理循环引用,用 deepcopy 让你定制。在 Python 里,你几乎不需要自己实现原型模式 ------除非拷贝成本太高,需要「写时复制(Copy-on-Write)」之类的优化。
5.5 双语言对照与记忆
|--------|----------------------------|--------------------------------|
| 维度 | Java | Python |
| 语言支持 | 需实现 Cloneable + 重写 clone() | copy.copy / copy.deepcopy 开箱即用 |
| 推荐做法 | 拷贝构造器 / 静态工厂 copyOf() | 直接 deepcopy,必要时定制 deepcopy |
| 主要风险 | 浅拷贝漏字段、final 字段无法赋值 | 深拷贝性能、不可拷贝对象、循环引用 |
口诀:造不如抄------但先算清拷贝成本;抄要抄干净(深拷) 。
联想: 复印机。原件(原型)放上去,按一下出一份(clone)。但如果你原件上贴了张便利贴(引用字段),复印件上也会有------除非你把便利贴也复印一遍(深拷贝)。
一眼识别: implements Cloneable 或 copy.deepcopy 出现在「批量创建相似对象」的场景里。
5.6 什么时候用,什么时候别用
该用: 对象初始化成本高(查库、解析、网络);需要大量相似对象;想避免「创建 + 一堆 setter」这种两步走;需要在运行时动态增加/删除产品种类(原型注册表)。
别用: 对象简单、构造便宜;对象持有不可复制的资源(连接、线程、锁)且没有妥善处理;类层次深、深拷贝会牵连一大片。
坑: ① 深拷贝一个含 10 万条数据的 List,比重新构造还慢;② 拷贝后忘了重置 ID、时间戳,导致主键冲突;③ 循环引用导致 deepcopy 栈溢出(Python 的 memo 能处理,手写的深拷贝不能)。
5.7 创建型小结
|--------|---------|---------|------------|
| 模式 | 解决 | 一句话 | 代价 |
| 单例 | 唯一性 | 全局只有一个 | 全局状态、难测试 |
| 工厂方法 | 创建与使用耦合 | 子类决定造什么 | 类数量翻倍 |
| 抽象工厂 | 产品族配套 | 成套地造 | 加新产品很痛 |
| 建造者 | 参数爆炸 | 分步组装 | 多一个类、多一层跳转 |
| 原型 | 创建成本高 | 复制现成的 | 深拷贝复杂且慢 |
记住这张表的第三列。面试被问「说说工厂方法和抽象工厂的区别」,你只要答「一个造单品,一个造成套」,比背三段定义管用。