编码 · 设计模式
抽象工厂模式详解与示例
简短回答
抽象工厂是一种创建型设计模式,它提供一个接口,用来创建一系列相关对象,而无需指明它们的具体类。客户端代码拿到一个工厂对象,例如 LightThemeFactory 或 DarkThemeFactory,再向它索取每一种产品(按钮、对话框)。由于整个产品族都出自同一个工厂,这些产品必然彼此匹配,只要传入另一个工厂,就能切换整个产品族。
意图
提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们的具体类。抽象工厂是《设计模式》(四人组,GoF)一书中五种创建型模式之一,书中也称其为 Kit(工具包)。
它要解决的问题
设想一个报表应用,可以向两种目标输出:网页和纯文本终端。每种目标都需要自己版本的几个组件:标题、表格和警告框。网页版的标题不能与终端版的表格混用,否则输出就会出错。
如果报表代码直接调用 new HtmlHeading() 和 new TextTable(),它就绑定在具体类上,目标的选择散落在许多行代码里,也没有任何机制阻止有人把不同产品族混在一起。抽象工厂把一个产品族的所有创建操作放在一个对象后面,于是选择只需在一个地方做出。
结构与参与者
| 参与者 | 职责 | 示例中的对应 |
|---|---|---|
| 抽象工厂(AbstractFactory) | 为每种产品类型声明一个创建方法。 | UiFactory |
| 具体工厂(ConcreteFactory) | 实现某一个产品族的创建方法。 | HtmlFactory, TextFactory |
| 抽象产品(AbstractProduct) | 某一类产品的接口。 | Heading, Alert |
| 具体产品(ConcreteProduct) | 属于某个特定产品族的产品。 | HtmlHeading, TextAlert |
| 客户端(Client) | 只使用抽象工厂和抽象产品接口。 | renderReport |
TypeScript 实现
interface Heading { render(text: string): string; }
interface Alert { render(text: string): string; }
interface UiFactory {
createHeading(): Heading;
createAlert(): Alert;
}
class HtmlFactory implements UiFactory {
createHeading(): Heading { return { render: (t) => `<h1>${t}</h1>` }; }
createAlert(): Alert { return { render: (t) => `<div class="alert">${t}</div>` }; }
}
class TextFactory implements UiFactory {
createHeading(): Heading { return { render: (t) => `${t}\n${"=".repeat(t.length)}` }; }
createAlert(): Alert { return { render: (t) => `[!] ${t}` }; }
}
// The client knows only the interfaces.
function renderReport(ui: UiFactory): string {
const heading = ui.createHeading();
const alert = ui.createAlert();
return [heading.render("Sales"), alert.render("2 orders failed")].join("\n");
}
const target: string = "text"; // e.g. from a command-line flag
const factory: UiFactory = target === "html" ? new HtmlFactory() : new TextFactory();
console.log(renderReport(factory));
// Sales
// =====
// [!] 2 orders failed唯一出现具体类名的地方,是选择工厂的那一行。renderReport 可以配合任何产品族工作,而且不会不小心混用 HTML 和文本的部件,因为它的所有产品都来自同一个工厂。
在真实程序中,产品通常是带有更多方法的类。这里用对象字面量是为了让示例简短,结构是一样的。
何时使用,何时不用
以下情况适合使用抽象工厂:
- 系统必须支持多个相关产品族,并且不同产品族的产品不能混用(UI 主题、操作系统控件、连接对象和命令对象需要配套的数据库驱动)。
- 你想在配置阶段一次性选定产品族,并让其余代码与它无关。
- 你希望测试时通过一个工厂注入一整套假对象。
以下情况应避免使用:
- 只有一个产品族。直接
new,或者一个工厂函数,就更简单。 - 产品类型经常变化。新增一种产品,例如
createTable(),就必须修改工厂接口和每一个具体工厂。这是 GoF 一书列出的主要缺点。
抽象工厂与工厂方法对比
| 方面 | 工厂方法 | 抽象工厂 |
|---|---|---|
| 创建 | 一种产品 | 一族相关产品 |
| 机制 | 子类重写一个创建方法 | 客户端拿到一个带有多个创建方法的工厂对象 |
| 依赖 | 继承 | 组合 |
| 容易添加 | 新的产品变体(新增子类) | 新的产品族(新增工厂类) |
| 难以添加 | 几乎没有;各个创建者相互独立 | 新的产品类型(每个工厂都要改) |
两者经常结合使用:具体工厂的每个创建方法本身就是一个工厂方法。
实际应用
- Java XML API:
DocumentBuilderFactory和TransformerFactory是抽象类。newInstance()会找到一个具体实现,使用它的代码只依赖抽象类型。 - Java 的 JDBC API 的工作方式与此类似:数据库驱动提供配套的
Connection、Statement和ResultSet实现,应用代码只使用这些接口。 - GoF 一书自己的例子是一个支持多种外观风格标准的用户界面工具包,每种标准对应一个工厂,创建与之匹配的滚动条、窗口和按钮。
相关模式
- 工厂方法:具体工厂常常把它的创建方法实现为工厂方法。
- 原型:可以用原型对象来配置工厂,并通过复制它们来创建产品,这样就不必为每个产品族写一个工厂子类。
- 单例:应用通常只需要每个具体工厂的一个实例。
- 建造者:同样隐藏构造过程,但它是一步步构建一个复杂对象,而抽象工厂则立即返回成品。
- 桥接:抽象工厂可以创建并配置桥接的实现一侧。
常见问题
什么是产品族?
一组设计为配合使用的对象,例如同一 UI 主题的按钮、菜单和对话框。把不同产品族的产品混用,会得到不一致甚至出错的结果。
如何给抽象工厂添加新的产品类型?
需要在工厂接口中添加一个创建方法,并在每个具体工厂中实现它。这正是该模式最适合产品类型集合稳定的场景的原因。
抽象工厂和依赖注入是一回事吗?
不是,但它们搭配得很好。依赖注入是从外部提供一个对象;抽象工厂则是一种可以被注入的对象,适用于客户端需要自己创建多个相关对象的情况。
抽象工厂可以是函数而不是类吗?
可以。在 TypeScript 中,工厂可以是一个由创建函数组成的普通对象,只要满足工厂接口即可。这个模式关注的是接口和产品族,而不是类。