元数据卡
- 前置知识:第7章(SOLID 原则)
- 预计时间:50 分钟
- 核心难度:进阶
- 完成标志:能在项目中识别和应用 5 种创建型模式
你的进度
你的机器需要造各种各样的零件:齿轮、弹簧、杠杆、轴承——每种零件的规格越来越多,构造参数越来越长,不同订单需要的配置越来越复杂。
你发现了一个模式:每次都在用同一个锻炉、以不同的配方造东西。工匠之都的老工坊手册里写:“把造东西的流程封装起来,不要让每个工人都自己搭炉子。” 你的任务
new 是最直接的创建对象的方式——但经常不够好。有时你需要保证全局只有一个实例(Singleton),有时你希望让子类决定创建哪个对象(Factory Method),有时你要构建一个包含大量可选参数的复杂对象(Builder),有时你想不通过 new 复制一个对象(Prototype)。创建型模式解决的核心问题:把"创建对象"的行为从"使用对象"的行为中分离出来。
本章分层
- 必读:Singleton、Factory Method、Builder
- 选读:Abstract Factory
- 进阶:Prototype(原型模式)
本章不会要求你掌握
- 对象池(Pool)模式
- 依赖注入容器的内部实现
破局 · 溯源
你写了一个日志服务,整个系统只有一个实例。但你发现其他开发者在各自模块里 new Logger(),每个模块一个不同的日志实例。你的日志级别配了三次——DEUBG、info、Info。日志散得到处都是。
你的反应是"他们不应该自己 new Logger,应该用我准备好的同一个实例"。你的直觉是对的:你需要一个 Singleton。
第一层:Singleton——全局唯一实例
// ch08/EagerSingleton.java
public class LoggerService {
private static final LoggerService INSTANCE = new LoggerService();
private LogLevel level = LogLevel.INFO;
private LoggerService() {} // 私有构造器
public static LoggerService getInstance() {
return INSTANCE;
}
public void log(String message, LogLevel level) {
if (level.ordinal() >= this.level.ordinal()) {
System.out.println("[" + level + "] " + message);
}
}
public void setLevel(LogLevel level) {
this.level = level;
}
}// 使用
LoggerService.getInstance().log("赛事已创建", LogLevel.INFO);
LoggerService.getInstance().setLevel(LogLevel.DEBUG);但 Singleton 有一个隐藏的成本:它把依赖硬编码在了代码里。LoggerService.getInstance() 是一个全局可访问的耦合点——你不能在单元测试里轻易换一个 Mock 实现。
更好的做法:通过依赖注入传入 Logger 实例,而不是直接调用 getInstance()。Singleton 用在"确实需要全局唯一"的场景(日志、配置、连接池),且只在模块内部使用。
延迟初始化版本,只在第一次调用时才创建:
// ch08/LazySingleton.java
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}双重检查锁定(double-checked locking)确保线程安全的避免每次调用都加锁。
在 Java 里,更推荐用枚举实现 Singleton——它天然防止反射攻击和序列化问题:
// ch08/EnumSingleton.java
public enum ConfigManager {
INSTANCE;
private Properties props = new Properties();
public String get(String key) {
return props.getProperty(key);
}
public void load(String path) throws IOException {
try (var in = new FileInputStream(path)) {
props.load(in);
}
}
}第二层:Factory Method——让子类决定创建什么
你希望系统能支持"不同的赛事类型有不同的积分规则"。你是可以选择每次判断赛事类型后手工创建对应的对象——但代码会发散到各个角落。
工厂方法模式:定义一个创建对象的接口,让子类决定实例化哪个类。
// ch08/factory-method/Challenge.java
public abstract class Challenge {
public abstract int calculateScore(int base);
}
// 不同赛事类型
public class ArenaChallenge extends Challenge {
@Override
public int calculateScore(int base) {
return base * 2; // 擂台赛双倍积分
}
}
public class PuzzleChallenge extends Challenge {
@Override
public int calculateScore(int base) {
return base + 20; // 解谜赛固定加分
}
}
// 工厂方法
public abstract class ChallengeFactory {
public abstract Challenge create();
}
public class ArenaChallengeFactory extends ChallengeFactory {
@Override
public Challenge create() {
return new ArenaChallenge();
}
}
public class PuzzleChallengeFactory extends ChallengeFactory {
@Override
public Challenge create() {
return new PuzzleChallenge();
}
}使用:
ChallengeFactory factory = getFactoryForType(type); // 取决于你的配置
Challenge challenge = factory.create();
int score = challenge.calculateScore(100);注意工厂方法把"new 哪个类"推迟到了子类——调用方不直接调用 new ArenaChallenge(),而是通过工厂。这意味着未来新增一个赛事类型,只需要新增 Challenge 和 ChallengeFactory 的子类,不需要修改已有的调用代码。开闭原则落地。
简单工厂是工厂方法的一个简化版本——不是用继承实现,而是一个静态方法:
// ch08/simple-factory/ChallengeFactory.java
public class ChallengeFactory {
public static Challenge create(String type) {
return switch (type) {
case "arena" -> new ArenaChallenge();
case "puzzle" -> new PuzzleChallenge();
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
}
}简单工厂更常见——一个方法管所有创建逻辑。缺点是新加类型要改这个方法(违反 OCP 但简洁)。
第三层:Abstract Factory——一族相关的产品
你面对的问题升级了:一个"挑战"不只是积分规则——"水晶洞窟"主题包含:Arena(积分规则)、Monster(怪物配置)、Decoration(装饰素材)。这组对象在主题下是一起使用的。
// ch08/abstract-factory/CaveThemeFactory.java
public interface ThemeFactory {
Arena createArena();
Monster createMonster();
Decoration createDecoration();
}
// 水晶洞窟主题
public class CrystalCaveFactory implements ThemeFactory {
public Arena createArena() { return new CrystalArena(); }
public Monster createMonster() { return new CrystalGolem(); }
public Decoration createDecoration() { return new CrystalDecoration(); }
}
// 火焰山主题
public class FireMountainFactory implements ThemeFactory {
public Arena createArena() { return new LavaArena(); }
public Monster createMonster() { return new FireDemon(); }
public Decoration createDecoration() { return new LavaDecoration(); }
}使用:
ThemeFactory factory = new CrystalCaveFactory();
Arena arena = factory.createArena();
Monster monster = factory.createMonster();
// arena, monster, decoration 全部来自同一主题,风格统一Abstract Factory 确保"一组产品总是被一起使用"——你不会在火焰山主题里意外配上一个冰霜怪物。换主题只需要换一个工厂实例。
第四层:Builder——分步构建复杂对象
对象的构造器有 7 个参数。大多数参数是可选的。你见过这种构造器调用吗?
new Tournament("比武大会", 16, 20, true, false, 3, null, 60);第八个参数是什么?第六个是 true 代表什么?没有人知道。这是 Telescoping Constructor 反模式。
Builder 模式解决这个问题——把一个多参数的构造过程拆成多个链式调用:
// ch08/builder/Tournament.java
public class Tournament {
private final String name;
private final int maxPlayers;
private final int minLevel;
private final boolean allowTeams;
private final boolean ranked;
private final int rounds;
private final String theme;
private final int timeLimitMinutes;
private Tournament(Builder builder) {
this.name = builder.name;
this.maxPlayers = builder.maxPlayers;
this.minLevel = builder.minLevel;
this.allowTeams = builder.allowTeams;
this.ranked = builder.ranked;
this.rounds = builder.rounds;
this.theme = builder.theme;
this.timeLimitMinutes = builder.timeLimitMinutes;
}
public static class Builder {
// 必填参数
private final String name;
private final int maxPlayers;
// 可选参数(带默认值)
private int minLevel = 1;
private boolean allowTeams = false;
private boolean ranked = true;
private int rounds = 3;
private String theme = "default";
private int timeLimitMinutes = 30;
public Builder(String name, int maxPlayers) {
this.name = name;
this.maxPlayers = maxPlayers;
}
public Builder minLevel(int val) { this.minLevel = val; return this; }
public Builder allowTeams(boolean val) { this.allowTeams = val; return this; }
public Builder ranked(boolean val) { this.ranked = val; return this; }
public Builder rounds(int val) { this.rounds = val; return this; }
public Builder theme(String val) { this.theme = val; return this; }
public Builder timeLimit(int val) { this.timeLimitMinutes = val; return this; }
public Tournament build() {
// 可以在 build 时做校验
if (name == null || name.isBlank())
throw new IllegalStateException("name is required");
return new Tournament(this);
}
}
}使用:
Tournament t = new Tournament.Builder("比武大会", 16)
.minLevel(20)
.ranked(true)
.rounds(5)
.theme("crystal-cave")
.timeLimit(45)
.build();你可以清晰地看出每个参数的含义、顺序自由、默认值自动填充。
Builder 的变体:Lombok 的 @Builder 注解可以自动生成 Builder 类。但手动 Builder 的好处是可以在 build() 里做参数校验和不变式检查。
第五层:Prototype——克隆对象
你有一个配置了各种奖品的 MatchTemplate——模板对象。你希望基于这个模板创建一个新对象,只改其中几个字段,而不是重新建一个几十个参数的构造器。
Prototype 模式:通过复制已有对象来创建新对象。
// ch08/prototype/MatchTemplate.java
public abstract class MatchTemplate implements Cloneable {
protected String name;
protected int baseScore;
protected List<String> rewards;
protected Duration duration;
// 深拷贝
@Override
public MatchTemplate clone() {
try {
MatchTemplate clone = (MatchTemplate) super.clone();
// List 等引用类型需要深拷贝
clone.rewards = new ArrayList<>(this.rewards);
return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError("Cloneable is implemented");
}
}
// setters...
}
// 使用
MatchTemplate defaultMatch = new ArenaTemplate();
defaultMatch.setName("标准擂台");
defaultMatch.setBaseScore(100);
defaultMatch.addReward("金币 x50");
MatchTemplate specialMatch = defaultMatch.clone();
specialMatch.setName("双倍擂台"); // 只改名字
specialMatch.setBaseScore(200); // 只改分数在实际项目中,Prototype 更多是通过"配置复制 + 差异覆盖"的方式存在——YAML 配置文件作为模板,代码加载后按需覆盖字段。直接 clone 在 Java 中用得不多,部分原因是 Cloneable 接口设计有缺陷(不强制 clone() 为 public,深拷贝容易漏)。
常见陷阱
陷阱一:过度使用 Singleton。 "这个类在系统里只需要一个实例"——但你真的确定吗?很多"只需要一个"的设计在后来变成了"需要两个"(多租户、分区配置)。Singletons 在测试中也难替换。优先用依赖注入(Spring @Component、Google Guice)让容器管理单例,而不是写 getInstance()。
陷阱二:Factory 的"我也不知道用哪个工厂"。 调用方在运行时需要选择工厂,结果工厂选择逻辑本身又是一个 switch-case——"给我 arena 的工厂!"。这时候可以用注册表模式(Registry Pattern)或依赖注入容器来管理工厂。
陷阱三:Builder 的 build() 里不做校验。 你写了一大串链式调用,漏了必填参数——结果 build 出来的对象带 null 字段,运行到一半才炸。在 build() 方法里做参数校验。
陷阱四:Prototype 的浅拷贝 bug。 你克隆了一个对象,改了它的 name——但改完发现原来的对象 rewards 列表也变了。浅拷贝只复制引用,不复制引用指向的对象。重写 clone() 时别忘了深拷贝引用类型字段。
通关挑战
- 热身:为你的项目里一个复杂的配置对象(至少 5 个参数)加上 Builder 模式。把原来散乱的
new X(a, b, c, d, e)调用替换成 Builder。 - 挑战:为你的系统设计一组 Abstract Factory——例如"竞技主题"和"探险主题",各产生一组相关的奖品、怪物、场景对象。写两个工厂实现,验证换主题只改一行代码。
- 观察:在 Spring Boot 项目里找
@Service/@Component注解的类——Spring 默认管理的就是 Singleton。有没有什么时候你需要不同的 scope(@Scope("prototype"))?
旅人笔记
创建型模式给了你五种控制对象创建的方式:Singleton 保唯一、Factory Method 延后决策、Abstract Factory 配一族、Builder 拆多步、Prototype 靠复制。它们共同回答一个问题——"什么时候以及如何创建一个对象?"
下一站预告
对象创建完毕,接下来是怎么组装它们。当你需要让不同的类协作,又保持松耦合时——结构型模式出场。下一章:Adapter、Composite、Decorator 等 7 种结构型模式。