cv.cm

编码 · 设计模式

抽象工厂模式详解与示例

阅读约 6 分钟

简短回答

抽象工厂是一种创建型设计模式,它提供一个接口,用来创建一系列相关对象,而无需指明它们的具体类。客户端代码拿到一个工厂对象,例如 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 中,工厂可以是一个由创建函数组成的普通对象,只要满足工厂接口即可。这个模式关注的是接口和产品族,而不是类。

全部编码教程