Back

JavaScriptにおけるファクトリーパターンの理解

JavaScriptにおけるファクトリーパターンの理解

12個の異なるファイルにnew UserService()と書いたとします。そしてコンストラクタのシグネチャが変更され、コードベース全体を探し回って各呼び出しを修正することになります。これこそがファクトリーパターンが解決する問題です。

ファクトリーパターンはオブジェクト生成を抽象化し、オブジェクトの構築方法を単一のポイントで制御できるようにします。これは単にオブジェクトを返す関数ではなく、何が必要かとどのように構築されるかを分離する設計パターンです。

重要なポイント

  • ファクトリーパターンはオブジェクト生成を一元化し、コードベース全体でコンストラクタの変更管理を容易にします。
  • ファクトリー関数はクロージャによってプライバシーを提供し、クラスは#privateフィールドで実行時のプライバシーを提供し、プロトタイプチェーンを介してメソッドを共有できます。
  • ファクトリーと依存性注入を組み合わせることで、依存関係の簡単な置き換えが可能になり、テストが簡素化されます。
  • 実行時の型決定や複雑な構築ロジックが必要な場合にファクトリーを使用し、シンプルで安定したオブジェクト生成にはスキップしましょう。

ファクトリーパターンとは実際に何か

ファクトリー関数はオブジェクトを返します。ファクトリーパターンはより広範囲で、生成ロジックが使用コードとは別に存在するアーキテクチャ的アプローチです。

以下がその違いです:

// シンプルなファクトリー関数
const createUser = (name) => ({ name, createdAt: Date.now() })

// ファクトリーパターン: どのクラスをインスタンス化するかを抽象化
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のファクトリー関数とクラス

どちらのアプローチにも適した場面があります。選択は何を最適化するかによって決まります。

静的ファクトリーメソッドを持つクラスは、プロトタイプベースのメソッド共有と明確なインスタンス化パスが必要な場合に適しています:

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
    }
  }
}

// アプリケーションのセットアップ時
const userService = createUserService({
  apiClient: productionApiClient,
  logger: consoleLogger,
  cache: memoryCache
})

// テスト時
const testUserService = createUserService({
  apiClient: mockApiClient,
  logger: nullLogger,
  cache: noOpCache
})

このパターンはテストを簡単にします—モックライブラリは不要です。

ファクトリーを使うべきでない場合

ファクトリーは間接性を追加します。これは柔軟性が必要な場合には価値がありますが、不要な場合にはコストになります。

以下の場合はファクトリーをスキップしましょう:

  • オブジェクト生成がシンプルで変更される可能性が低い
  • 1種類のオブジェクトしか作成しない
  • 追加された抽象化が明確化ではなく不明瞭化をもたらす

単一のnew Date()呼び出しをラップするファクトリーはパターンではなく、ノイズです。

モダンなJSモジュールにおけるオブジェクト生成パターン

ESモジュールは、ファクトリーがアプリケーションアーキテクチャにどのように適合するかを変えます。設定済みのファクトリー関数を直接エクスポートできます:

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

export const createNotification = (type, message) => {
  // ファクトリーはモジュールスコープの依存関係にアクセス可能
  const emailService = new EmailService(config.smtp)
  // ... 生成ロジック
}

モジュール境界が自然なカプセル化を提供します。利用者は依存関係を知ることなくファクトリーをインポートします。

まとめ

複雑な構築ロジックを一元化する必要がある場合、呼び出し側のコードを変更せずに実装を交換する必要がある場合、テストのために依存関係を注入する必要がある場合、または実行時にオブジェクトの型を決定する必要がある場合に、ファクトリーパターンを使用してください。

直接インスタンス化から始めましょう。分散した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