cv.cm

编码 · 设计模式

工厂方法模式详解与示例

阅读约 6 分钟

简短回答

工厂方法是一种创建型设计模式,它定义一个创建对象的方法,但让子类决定实例化哪个具体类。基类的逻辑围绕产品接口来写,并调用 createTransport() 这类工厂方法,而不是直接用 new。这样只需新增一个子类,就能加入新的产品类型,而无需修改使用这些产品的代码。

意图

定义一个用于创建对象的接口,但让子类决定实例化哪个类。工厂方法使一个类把实例化推迟到子类。它是《设计模式》(四人组,GoF)一书中五种创建型模式之一,也是其中唯一作用于类的创建型模式,因为它依赖继承。

它要解决的问题

设想一个通知模块:它格式化消息、发送消息,并记录结果。起初只发邮件,所以代码直接调用 new EmailChannel()。后来产品需要短信,再后来又要推送通知。每增加一个渠道,就得修改同一个类、再加一个 if 分支,而格式化和记录日志这些步骤其实从未变过。

发送流程是稳定的,变化的只是渠道的种类。工厂方法把这个唯一会变的决定,也就是创建哪个类,收拢到一个可重写的方法里。

结构与参与者

参与者职责示例中的对应
产品(Product)工厂方法所创建对象的接口。Channel
具体产品(ConcreteProduct)实现产品接口。EmailChannel, SmsChannel
创建者(Creator)声明工厂方法,并包含使用产品的逻辑。Notifier
具体创建者(ConcreteCreator)重写工厂方法,返回某个具体产品。EmailNotifier, SmsNotifier

TypeScript 实现

interface Channel {
  send(to: string, text: string): string;
}

class EmailChannel implements Channel {
  send(to: string, text: string): string { return `email to ${to}: ${text}`; }
}

class SmsChannel implements Channel {
  send(to: string, text: string): string { return `sms to ${to}: ${text.slice(0, 20)}`; }
}

abstract class Notifier {
  // The factory method: subclasses choose the concrete product.
  protected abstract createChannel(): Channel;
  // Shared logic that works with any Channel.
  notify(to: string, text: string): void {
    const channel = this.createChannel();
    const result = channel.send(to, text.trim());
    console.log("[log]", result);
  }
}

class EmailNotifier extends Notifier {
  protected createChannel(): Channel { return new EmailChannel(); }
}

class SmsNotifier extends Notifier {
  protected createChannel(): Channel { return new SmsChannel(); }
}

const notifiers: Notifier[] = [new EmailNotifier(), new SmsNotifier()];
for (const n of notifiers) n.notify("ada", " Your order has shipped today ");
// [log] email to ada: Your order has shipped today
// [log] sms to ada: Your order has shipp

现在要加推送通知,只需编写 PushChannel 和 PushNotifier。notify 方法以及它的所有调用方都保持不变。

应用该模式之前,创建哪个对象的决定被埋在共用逻辑里:

class Notifier {
  constructor(private kind: "email" | "sms") {}
  notify(to: string, text: string): void {
    // every new channel edits this method
    const result = this.kind === "email"
      ? `email to ${to}: ${text}`
      : `sms to ${to}: ${text.slice(0, 20)}`;
    console.log("[log]", result);
  }
}

更轻量的替代:传入一个工厂函数

工厂方法依赖继承。在 TypeScript 中,往往可以通过组合获得同样的灵活性:把创建函数传给构造函数。这样就不必为每种产品写一个子类:

interface Channel { send(to: string, text: string): string; }
class Notifier {
  constructor(private createChannel: () => Channel) {}
  notify(to: string, text: string): void {
    console.log("[log]", this.createChannel().send(to, text));
  }
}
const push = new Notifier(() => ({ send: (to, text) => `push to ${to}: ${text}` }));
push.notify("ada", "Hello"); // [log] push to ada: Hello

如果每个创建者本来就因为其他原因有各自的类,或者框架要求你继承某个基类,就用子类版本。如果各个变体之间唯一的差别就是创建的对象,就用函数版本。

何时使用,何时不用

以下情况适合使用工厂方法:

  • 一个类包含稳定的算法,却无法事先知道必须创建哪些具体对象。
  • 你在编写框架或库,希望用户通过继承来接入自己的产品类。
  • 你希望把某个类型的创建逻辑集中在一处,而不是到处散落 new 调用。

以下情况应避免使用:

  • 只有一种产品类型,也没有出现第二种的迹象。直接 new 更清晰。
  • 唯一的理由是为了方便测试。通过构造函数传入依赖更简单。
  • 你会建立一套与产品一一对应、却没有其他逻辑的平行创建者层级。

实际应用

  • Java 集合:Iterable.iterator() 只声明一次,每个集合类(ArrayList、HashSet 等)各自重写它,返回自己的迭代器类。GoF 一书就是用工厂方法来描述这种多态迭代。
  • JavaScript 可迭代对象:每个可迭代对象都实现 [Symbol.iterator](),这个方法返回一个适合该集合的新迭代器。
  • UI 框架通常允许子类重写创建组件、单元格或视图的方法,而基类负责控制布局和生命周期。

相关模式

模式区别
抽象工厂通过一个带有多个创建方法的对象,创建一整个系列的相关产品。它的各个方法通常用工厂方法来实现。
模板方法工厂方法常常在模板方法里被调用,就像上面的 notify 调用 createChannel 一样。
原型通过复制现有实例来创建对象,而不是继承创建者,因此不需要平行的创建者层级。
建造者关注的是一步步构造一个复杂对象,而不是选择创建哪个类。

常见问题

工厂方法与抽象工厂有什么区别?

工厂方法是单个方法,由子类重写来创建一种产品。抽象工厂是一个带有多个创建方法的对象,用来产出必须配套使用的一系列相关产品。

简单的工厂函数就是工厂方法吗?

不完全是。像 createShape(kind) 这样内部用 switch 的函数,通常称为简单工厂。GoF 的工厂方法依赖子类重写创建方法,不过两者都对客户端隐藏了 new。

工厂方法符合开闭原则吗?

符合。新增产品时,只需添加一个新的产品类和一个新的创建者子类,而不必改动创建者已有的逻辑。

工厂方法必须是抽象的吗?

不必。基类可以提供一个返回标准产品的默认实现,子类只在需要不同产品时才重写它。

全部编码教程