Back

理解 JavaScript 中的工厂模式

理解 JavaScript 中的工厂模式

你已经在十二个不同的文件中写了 new UserService()。现在构造函数签名发生了变化,你不得不在代码库中到处搜索并修复每个调用。这正是工厂模式要解决的问题。

工厂模式将对象创建过程抽象化,为你提供一个统一的控制点来管理对象的构建方式。它不仅仅是一个返回对象的函数——它是一种设计模式,将你需要什么如何构建它解耦。

核心要点

  • 工厂模式集中管理对象创建,使得在整个代码库中更容易处理构造函数的变更。
  • 工厂函数通过闭包提供私有性,而类可以使用 #private 字段提供运行时私有性,并通过原型链共享方法。
  • 将工厂与依赖注入结合使用,可以轻松替换依赖项,从而简化测试。
  • 当需要运行时类型决策或复杂的构造逻辑时使用工厂——对于简单、稳定的对象创建则可以跳过。

工厂模式到底是什么

工厂函数返回一个对象。而工厂模式的范围更广:它是一种架构方法,其中创建逻辑与使用代码分离。

以下是两者的区别:

// Simple factory function
const createUser = (name) => ({ name, createdAt: Date.now() })

// Factory Pattern: abstracts which class to instantiate
function createNotification(type, message) {
  const notifications = {
    email: () => new EmailNotification(message),
    sms: () => new SMSNotification(message),
    push: () => new PushNotification(message)
  }
  
  if (!notifications[type]) {
    throw new Error(`Unknown notification type: ${type}`)
  }
  
  return notifications[type]()
}

当你需要根据运行时条件决定创建哪个对象,或者当构造过程需要调用代码不应该知道的配置时,这个模式就会大放异彩。

JavaScript 工厂函数 vs 类

两种方法都有各自的用武之地。选择取决于你要优化什么。

带有静态工厂方法的类在你需要基于原型的方法共享和清晰的实例化路径时效果很好:

class ApiClient {
  #baseUrl
  #authToken
  
  constructor(baseUrl, authToken) {
    this.#baseUrl = baseUrl
    this.#authToken = authToken
  }
  
  static forProduction(token) {
    return new ApiClient('https://api.example.com', token)
  }
  
  static forDevelopment() {
    return new ApiClient('http://localhost:3000', 'dev-token')
  }
}

工厂函数在你需要通过闭包实现真正的私有性,或者想要避免 this 绑定复杂性时表现出色:

function createCounter(initial = 0) {
  let count = initial
  
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count
  }
}

权衡之处在于:工厂函数为每个对象创建新的方法实例。而类通过原型链共享方法。对于大多数前端应用程序来说,这种差异可以忽略不计——但如果你要创建数千个实例,这就很重要了。

工厂与依赖注入

当工厂与依赖注入结合使用时会变得非常强大。你不是硬编码依赖项,而是将它们传递给工厂:

function createUserService({ apiClient, logger, cache }) {
  return {
    async getUser(id) {
      const cached = cache.get(`user:${id}`)
      if (cached) return cached
      
      logger.debug(`Fetching user ${id}`)
      const user = await apiClient.get(`/users/${id}`)
      cache.set(`user:${id}`, user)
      return user
    }
  }
}

// In your application setup
const userService = createUserService({
  apiClient: productionApiClient,
  logger: consoleLogger,
  cache: memoryCache
})

// In tests
const testUserService = createUserService({
  apiClient: mockApiClient,
  logger: nullLogger,
  cache: noOpCache
})

这种模式使测试变得简单直接——不需要模拟库。

何时不使用工厂

工厂增加了间接性。当你需要灵活性时这很有价值,但当你不需要时就会产生成本。

在以下情况下跳过工厂:

  • 对象创建简单且不太可能改变
  • 你只创建一种类型的对象
  • 增加的抽象层使代码变得模糊而不是清晰

一个仅包装单个 new Date() 调用的工厂不是模式——而是噪音。

现代 JS 模块中的对象创建模式

ES 模块改变了工厂在应用程序架构中的位置。你可以直接导出配置好的工厂函数:

// services/notifications.js
import { EmailService } from './email.js'
import { config } from '../config.js'

export const createNotification = (type, message) => {
  // Factory has access to module-scoped dependencies
  const emailService = new EmailService(config.smtp)
  // ... creation logic
}

模块边界提供了自然的封装。使用者导入工厂时无需知道其依赖项。

结论

当你需要集中管理复杂的构造逻辑、在不改变调用代码的情况下交换实现、为测试注入依赖项,或在运行时决定对象类型时,使用工厂模式。

从直接实例化开始。当分散的 new 调用带来的痛苦变得真实时,再引入工厂。这个模式的存在是为了解决问题——而不是为了满足架构理想。

常见问题

工厂函数只是简单地返回一个对象。工厂模式是一种更广泛的架构方法,它将创建逻辑与使用代码分离,通常根据运行时条件决定创建哪种类型的对象。该模式为整个应用程序的对象实例化提供了一个统一的控制点。

当构造函数签名可能改变、需要在运行时决定对象类型、构造过程涉及复杂配置,或者想要注入依赖项以便于测试时,使用工厂。对于具有稳定构造函数的简单对象,直接实例化更清晰。

工厂函数为每个对象创建新的方法实例,而类通过原型链共享方法。对于大多数应用程序来说,这种差异可以忽略不计,但在创建数千个实例时会变得显著。应根据你的具体性能要求和私有性需求来选择。

工厂与依赖注入结合使用,可以让你在测试期间直接向工厂传递模拟依赖项。这消除了对模拟库或复杂测试设置的需求。你只需传递不同的参数,就可以用测试替身替换真实的 API 客户端、日志记录器或缓存。

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..

OpenReplay