02|简单工厂:把产品选择集中到一个入口
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.simplefactory。
系列导航:Java 25 设计模式学习指南
博客导出功能先支持纯文本,随后增加 Markdown。如果每个控制器都根据字符串判断应该 new 哪个格式化器,那么增加一种格式时,要逐个寻找这些分支。更麻烦的是,不同入口可能使用不同的拼写或默认值,同一配置在两个页面得到不同结果。
这里先把变化限制为一个问题:给定格式名称,应该创建哪种产品?格式化算法仍由产品对象完成。把创建位置收拢后,调用方只依赖一个 Formatter 契约;它不需要知道具体格式化类的构造细节。
从问题提炼设计意图
简单工厂把“选择并创建具体产品”的规则集中到一个工厂中。它是本系列补充的第 24 种学习模式,不属于 GoF 原书的 23 种模式。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
Formatter |
产品接口,描述标题格式化能力 |
PlainFormatter、MarkdownFormatter |
具体产品,各自实现一种文本格式 |
FormatterFactory |
简单工厂,集中解释格式名称 |
Demo |
调用方,选择格式并通过接口使用产品 |
classDiagram
Formatter <|.. PlainFormatter
Formatter <|.. MarkdownFormatter
FormatterFactory ..> Formatter : creates
Demo ..> FormatterFactory : selects
代码思路:变化应该落在哪个对象上
第一步先抽取真正相同的能力:两种产品都接受标题并返回文本,因此接口只需要 format。不要把 Markdown 专用选项塞进所有产品共有的接口,否则“共同接口”会变成一堆只有部分实现理解的参数。
第二步让 create 只负责选择和创建。这里使用 switch 表达有限、稳定的两种格式,未知名称抛出 IllegalArgumentException。故意不设置“默认返回纯文本”,因为配置错误应该在创建边界暴露;null 则由 requireNonNull 拒绝。
第三步由调用方持有 Formatter,再调用 format。具体类的名字仍出现在工厂里,这是创建边界必须承担的知识。模式的收益是减少散落的创建决策,而不是让具体类凭空消失。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.simplefactory,源码目录为 src/main/java/com/hanserwei/patterns/simplefactory/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
Formatter.java:
package com.hanserwei.patterns.simplefactory;
/** 文章标题格式化契约. */
public interface Formatter {
/** 将非空标题转换为目标文本表示. */
String format(String title);
}
FormatterFactory.java:
package com.hanserwei.patterns.simplefactory;
import java.util.Objects;
/** 集中管理格式名称到具体产品的映射. */
public final class FormatterFactory {
/** 禁止实例化无状态工厂. */
private FormatterFactory() {}
/** 按名称创建产品;未知名称抛出 IllegalArgumentException. */
public static Formatter create(String type) {
Objects.requireNonNull(type, "type");
return switch (type) {
case "plain" -> new PlainFormatter();
case "markdown" -> new MarkdownFormatter();
default -> throw new IllegalArgumentException("Unknown format: " + type);
};
}
}
MarkdownFormatter.java:
package com.hanserwei.patterns.simplefactory;
import java.util.Objects;
/** 生成 Markdown 一级标题的产品. */
public final class MarkdownFormatter implements Formatter {
/** 返回目标格式的标题;拒绝 null. */
@Override
public String format(String title) {
Objects.requireNonNull(title, "title");
return "# " + title;
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.simplefactory;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
System.out.println(FormatterFactory.create("markdown").format("Java 25"));
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runSimplefactory
./gradlew test --tests 'com.hanserwei.patterns.simplefactory.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
# Java 25
用测试确认模式的行为
两个格式名称得到各自的格式化结果;pdf 被明确拒绝。测试关注创建结果和失败契约,而非仅检查返回对象不为 null。
对应测试位于 src/test/java/com/hanserwei/patterns/simplefactory/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 导入导出工具按配置选择格式化器。
- 协议种类少、变化集中时,为本地客户端选择实现。
- 业务对象构造参数复杂,需要统一默认值和校验。
适用边界与容易踩的坑
新增产品仍要修改这个工厂的分支;简单工厂不会自动满足所有扩展场景的开闭原则。对于两三种稳定产品,这种集中修改可能比增加注册机制更清楚。若格式由外部插件不断扩展,再考虑注册表或服务发现。
静态方法只是本例的实现方式,简单工厂也可以是普通对象。产品同样不必每次创建:缓存与生命周期可以另行设计,但不能在调用方不知情时把有状态产品改成共享实例。本例每次创建新产品,产品没有可变字段。
输出“# 标题”只展示对象选择,不是完整 Markdown 转义库。如果标题来自复杂用户输入,换行、特殊语法和渲染边界应在具体产品中定义。
与相近模式比较
简单工厂通过参数分支选产品;工厂方法通过创建者子类覆写创建步骤。看到一个名为 Factory 的类,还不足以判定它是工厂方法模式。
动手练习
增加 QuoteFormatter,把标题输出为引用文本。先增加产品和测试,再修改工厂映射。验收时列出改动文件,并回答:为什么增加产品仍要修改工厂?在仅有三个稳定格式的场景中,这是否真的构成问题?
系列导航:Java 25 设计模式学习指南