Skip to content

元数据卡

  • 前置知识:第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——全局唯一实例

java
// 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;
    }
}
java
// 使用
LoggerService.getInstance().log("赛事已创建", LogLevel.INFO);
LoggerService.getInstance().setLevel(LogLevel.DEBUG);

但 Singleton 有一个隐藏的成本:它把依赖硬编码在了代码里。LoggerService.getInstance() 是一个全局可访问的耦合点——你不能在单元测试里轻易换一个 Mock 实现。

更好的做法:通过依赖注入传入 Logger 实例,而不是直接调用 getInstance()。Singleton 用在"确实需要全局唯一"的场景(日志、配置、连接池),且只在模块内部使用。

延迟初始化版本,只在第一次调用时才创建:

java
// 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——它天然防止反射攻击和序列化问题:

java
// 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——让子类决定创建什么

你希望系统能支持"不同的赛事类型有不同的积分规则"。你是可以选择每次判断赛事类型后手工创建对应的对象——但代码会发散到各个角落。

工厂方法模式:定义一个创建对象的接口,让子类决定实例化哪个类。

java
// 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();
    }
}

使用:

java
ChallengeFactory factory = getFactoryForType(type);  // 取决于你的配置
Challenge challenge = factory.create();
int score = challenge.calculateScore(100);

注意工厂方法把"new 哪个类"推迟到了子类——调用方不直接调用 new ArenaChallenge(),而是通过工厂。这意味着未来新增一个赛事类型,只需要新增 ChallengeChallengeFactory 的子类,不需要修改已有的调用代码。开闭原则落地。

简单工厂是工厂方法的一个简化版本——不是用继承实现,而是一个静态方法:

java
// 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(装饰素材)。这组对象在主题下是一起使用的

java
// 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(); }
}

使用:

java
ThemeFactory factory = new CrystalCaveFactory();
Arena arena = factory.createArena();
Monster monster = factory.createMonster();
// arena, monster, decoration 全部来自同一主题,风格统一

Abstract Factory 确保"一组产品总是被一起使用"——你不会在火焰山主题里意外配上一个冰霜怪物。换主题只需要换一个工厂实例。

第四层:Builder——分步构建复杂对象

对象的构造器有 7 个参数。大多数参数是可选的。你见过这种构造器调用吗?

java
new Tournament("比武大会", 16, 20, true, false, 3, null, 60);

第八个参数是什么?第六个是 true 代表什么?没有人知道。这是 Telescoping Constructor 反模式。

Builder 模式解决这个问题——把一个多参数的构造过程拆成多个链式调用:

java
// 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);
        }
    }
}

使用:

java
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 模式:通过复制已有对象来创建新对象。

java
// 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 种结构型模式。

Built with VitePress | Software Systems Atlas