主题
界面
主题
《SOLID 原则》讲了设计规则;从本节起讲具体的模式。创建型模式只回答一个问题:
"对象怎么造"—— 直接 new?交给工厂?用建造者一步步搭?★ 判断标准始终是同一个 ✓:
创建逻辑的"变化点"在哪里?把变化点隔离出去,就是创建型模式的价值 ✓✓
先看单例的一个真实事故 ✗:
某项目用"双检锁"写了一个配置单例,上线后偶发"配置为空"。
排查发现:**没加 volatile** → 别的线程可能拿到"构造未完成"的对象 ✗✗
(这就是 Java 内存模型里的"指令重排"—— 对象引用先被赋值,构造还没执行完)★ 本节把单例放进两个攻击场景里测一遍 ✓✓:
| 攻击 | 能挡住吗 |
|---|---|
| 反射调用私有构造器 | 普通写法全部失守 ✗✗ |
| 反序列化 | 同上 ✗✗ |
| 枚举 | 两个都挡住 ✓✓ |
五种写法(本节实测):
| 写法 | 懒加载 | 线程安全 | 性能 |
|---|---|---|---|
| 饿汉 | ✗ | ✓ | 最快 ✓ |
| 同步懒汉 | ✓ | ✓ | 最慢 ✗✗(每次进监视器) |
| 双检锁 | ✓ | ✓(必须加 volatile) | 快 ✓ |
| 静态内部类 | ✓ | ✓ | 与饿汉同量级 ✓✓ |
| 枚举 | ✗ | ✓ | 快 ✓ |
★ 实测:并发下是否唯一 ✓(2000 线程各拿一次):
实现方式 实例个数
饿汉 1
同步懒汉 1
双检锁 1
静态内部类 1
枚举 1★ 五种写法在并发下都能保证唯一 ✓ ——但下面两个攻击面才是分水岭 ✓✓
★ 攻击一:反射 ✗
Constructor<DoubleChecked> ctor = DoubleChecked.class.getDeclaredConstructor();
ctor.setAccessible(true); // ★ 私有构造器挡不住它
DoubleChecked a = ctor.newInstance(); // ✗ 造出了第二个实例| 写法 | 结果 |
|---|---|
| 双检锁 / 静态内部类 ✗✗ | 造出第二个实例 ——单例被破坏 |
| 枚举 ✓✓ | 抛 Cannot reflectively create enum objects(JVM 层硬性检查) |
★ 攻击二:反序列化 ✗
| 写法 | 结果 |
|---|---|
| 普通类 ✗✗ | 反序列化得到新对象(除非加 readResolve()) |
| 枚举 ✓✓ | 按名字查找已有常量 → 还是同一个 ✓ |
★ 结论 ✓✓:
只有枚举能同时挡住反射与反序列化 ✓
所以《Effective Java》推荐「单元素枚举是单例的最佳实现」 ✓✓
★ 但枚举也有局限 ✗:
| 局限 | 说明 |
|---|---|
| 不能继承类 | 枚举已隐式继承 Enum |
| 不能懒加载 | 类加载时就初始化所有常量 |
| 序列化形式受限 | 不能自定义 readObject |
★ 更重要的认知 ✓✓:
单例是"全局状态",它会隐藏依赖、污染测试 ✗
本项目的测试隔离问题(T2-011)就与共享状态有关 ✓
现代做法:用容器管理"单实例 Bean"(Spring 默认单例),而不是手写
getInstance()✓✓
★ 简单工厂 ✗(每加类型都要改它):
static Notification create(String channel) {
return switch (channel) {
case "email" -> new EmailNotification();
case "sms" -> new SmsNotification();
default -> throw new IllegalArgumentException("未知渠道:" + channel);
};
}★ 注册式工厂 ✓✓(加类型只需注册一行):
RegistryFactory.register("email", EmailNotification::new);
RegistryFactory.register("dingtalk", DingTalkNotification::new); // ★ 工厂代码不动★ 实测对比 ✓:
| 要做的改动 | 简单工厂 | 注册式工厂 |
|---|---|---|
| 加一个新渠道 | 修改 switch(动老代码) ✗ | 注册一行 ✓✓ |
| 多人同时加渠道 | 改同一个方法(冲突) ✗ | 各加各的 ✓ |
★ 真实框架里的对应 ✓✓:
| 框架 | 注册表 |
|---|---|
| Spring | beanDefinitionMap(BeanFactory.getBean(name))✓ |
| MyBatis | TypeHandlerRegistry / MapperRegistry ✓ |
| JDBC | DriverManager(各驱动自己注册)✓ |
★ 工厂方法 vs 抽象工厂 ✓:
| 模式 | 解决 | 典型 |
|---|---|---|
| 工厂方法 ★★ | 创建步骤固定,某一步因类型而异(本质是"模板方法 + 创建") | 报表生成器 |
| 抽象工厂 ★ | 创建"一族"相关对象 | 跨数据库的 Dialect 工厂 |
✗ 12 参数的构造器:
new BadOrder("ORD-1", "U-1", "P-1", 2, 9900L,
"上海市浦东新区", "张三", "138****0000",
null, true, "尽快发货", "app");★ 问题:参数顺序错一个就出 bug,而且编译器不报错 ✗✗
✓ 建造者:
Order.builder("ORD-1")
.userId("U-1")
.productId("P-1")
.quantity(2)
.address("上海市浦东新区")
.urgent(true)
.build();★ 三项收益 ✓✓:
| 收益 | 说明 |
|---|---|
| 字段名可读 ★★ | 不用数参数位置 |
| 可给默认值 ✓ | quantity 默认 1、remark 默认空 |
| 校验集中且能一次报全 ★★★ | 对比构造器只能报第一个错误 |
★ 第三个收益的实测(本项目代码里的输出):
订单参数有误:[userId 不能为空, productId 不能为空, quantity 必须为正数, 加急订单必须有地址]★ 什么时候用建造者(判断口诀) ✓✓:
| 信号 | 说明 |
|---|---|
| 参数超过 4 个 ✓ | 可读性下降 |
| 有很多可选参数 ★★★ | 最典型的信号 |
| 需要在构建前做校验 ✓ | 且想一次报全部问题 |
★ 反过来:3 个参数的简单对象用构造器就够了 ✗ ——上建造者只会更啰嗦 ✓
| 模式 | 解决的问题 | 典型例子 |
|---|---|---|
| 简单工厂 | 把 if-else 创建逻辑收起来 | 按 type 造对象 |
| 注册式工厂 ★★ | 新增类型不改工厂(开闭) | Spring BeanFactory |
| 工厂方法 | 创建步骤固定、某步因类型而异 | 模板方法 + 创建 |
| 抽象工厂 | 创建"一族"相关对象 | 跨库 Dialect |
| 建造者 ★★ | 参数多、可选项多、要校验 | StringBuilder、OkHttp |
| 单例 | 全局唯一实例 | Spring 的单例 Bean |
★ 一条实践提醒 ✓✓:
创建型模式的价值在"变化点被隔离",不在"多一层" ✓
如果一个对象只有 2 个参数、也永远不会变,直接
new就是最好的设计 ✓✓
public class Config {
private Config() { }
private static class Holder {
static final Config INSTANCE = new Config();
}
public static Config getInstance() { return Holder.INSTANCE; }
}★ 为什么它又好又快 ✓:利用"类加载只执行一次" —— 既懒加载(首次访问才初始化)又无需加锁 ✓✓
Order order = Order.builder(orderNo)
.userId(userId)
.quantity(quantity) // 内部会校验 > 0
.build(); // ★ 校验失败时一次性列出所有问题private static DoubleChecked instance; // ✗ 少了 volatile代码:labs/design-patterns/src/main/java/cn/kb/learn/dp/ 下的 SingletonLesson、FactoryBuilderLesson
运行:
cd labs
.\mvnw.cmd -pl design-patterns compile
java "-Dstdout.encoding=UTF-8" -cp design-patterns/target/classes cn.kb.learn.dp.SingletonLesson
java "-Dstdout.encoding=UTF-8" -cp design-patterns/target/classes cn.kb.learn.dp.FactoryBuilderLesson实测结论(关键三条) ✓:
| # | 结论 |
|---|---|
| ① | 五种单例在并发下都唯一;但反射与反序列化只有枚举挡得住 ✓✓ |
| ② | 同步懒汉最慢(每次进监视器);静态内部类与饿汉同量级 ✓ |
| ③ | 注册式工厂让"加类型"从改老代码变成加新类 ✓✓ |
volatile 去掉:跑并发压测,观察偶发问题(难复现,正是它的可怕之处)⚠️;| # | 错误 | 说明 |
|---|---|---|
| 1 | 双检锁忘了 volatile ✗✗ | 可能拿到构造未完成的对象 ✓ |
| 2 | 以为"私有构造器"能防住反射 ✗✗ | setAccessible(true) 就能绕过 ✓ |
| 3 | 手写单例做全局状态 ✗✗ | 隐藏依赖、污染测试 ✓ |
| 4 | 把单例当"数据库连接"用 ✗✗ | 连接是有状态资源,绝不能单例 ✓ |
| 5 | 3 个参数也上建造者 ✗ | 过度设计 ✓ |
| 6 | 每加一个类型就改工厂的 switch ✗ | 违反开闭 ✗ |
目标:亲手破坏一次单例,理解"为什么推荐枚举"。
步骤:
SingletonLesson,确认五种的实例个数都是 1 ✓;验收方式:能说出"反射为什么能破坏单例、枚举为什么不能"。
扩展任务:给"消息通知"设计一个可扩展的创建方案:
public class Cache {
private static Cache instance;
public static Cache getInstance() {
if (instance == null) { instance = new Cache(); }
return instance;
}
}这段代码有什么问题? 5. (设计)给"订单导出"设计创建方案:支持 CSV / Excel / PDF,新增格式不改老代码,每种格式有各自的可选参数。
setAccessible(true) 绕过私有构造器 ✗★volatile ✓synchronized,每次都要进监视器(性能差) ⚠️《设计模式:结构型》:装饰器与代理 —— 用组合替代继承。