🚀

外部API切り替えを容易にするAdapterパターン設計

に公開

概要

複数の外部APIを使い分ける必要がある場合、Adapterパターンを活用することで、API固有の処理を抽象化し、切り替えやすい設計を実現できます。この記事では、実際のプロジェクトで使用した設計パターンを元に、その実装方法と利点について解説します。

設計の背景

外部APIを利用する際によくある課題:

  • API仕様の違い: レスポンス形式、エラーハンドリング、認証方式の違い
  • APIの可用性: 料金制限、レスポンス速度、サービス停止リスク
  • 将来性: 新しいAPIサービスへの切り替え需要

これらの課題を解決するため、Adapterパターンを採用しました。

設計構造

共通インターフェース(IApiAdapter)

// 例: 商品情報APIの統一インターフェース
interface IProductApiAdapter {
  getApiType(): ApiType
  fetchProductData(productId: string): Promise<NormalizedProductResult>
  validateResponse(response: unknown): boolean
  getApiConfiguration(): ApiConfiguration
  handleApiError(error: unknown): string
  calculateStatus(response: unknown): StatusResult
}

具体的なAdapter実装

// ShopifyAPI用のAdapter
class ShopifyAdapter implements IProductApiAdapter {
  getApiType(): ApiType {
    return 'SHOPIFY'
  }

  async fetchProductData(productId: string): Promise<NormalizedProductResult> {
    const config = this.getApiConfiguration()

    // Shopify固有のAPI呼び出し
    const response = await fetch(`${config.apiUrl}/products/${productId}`, {
      headers: config.requiredHeaders
    })

    // レスポンスを正規化
    return this.normalizeShopifyResponse(response)
  }

  private normalizeShopifyResponse(response: ShopifyResponse): NormalizedProductResult {
    // Shopify固有のレスポンス形式を統一形式に変換
    return {
      id: response.product.id,
      title: response.product.title,
      price: response.product.variants[0].price,
      // ... その他の統一フィールド
    }
  }
}

// WooCommerce API用のAdapter
class WooCommerceAdapter implements IProductApiAdapter {
  getApiType(): ApiType {
    return 'WOOCOMMERCE'
  }

  async fetchProductData(productId: string): Promise<NormalizedProductResult> {
    const config = this.getApiConfiguration()

    // WooCommerce固有のAPI呼び出し
    const response = await fetch(`${config.apiUrl}/wp-json/wc/v3/products/${productId}`, {
      headers: config.requiredHeaders
    })

    // レスポンスを正規化
    return this.normalizeWooCommerceResponse(response)
  }

  private normalizeWooCommerceResponse(response: WooCommerceResponse): NormalizedProductResult {
    // WooCommerce固有のレスポンス形式を統一形式に変換
    return {
      id: response.id,
      title: response.name,
      price: response.price,
      // ... その他の統一フィールド
    }
  }
}

Factory Pattern で Adapter を取得

// AdapterFactory
class ProductApiFactory {
  private static adapters: Map<ApiType, IProductApiAdapter> = new Map([
    ['SHOPIFY', new ShopifyAdapter()],
    ['WOOCOMMERCE', new WooCommerceAdapter()],
  ])

  static getAdapter(apiType: ApiType): IProductApiAdapter {
    const adapter = this.adapters.get(apiType)
    if (!adapter) {
      throw new Error(`Unsupported API type: ${apiType}`)
    }
    return adapter
  }

  static getDefaultApiType(): ApiType {
    return 'SHOPIFY' // デフォルト設定
  }
}

使用例

// 呼び出し側のコード
const fetchProductInfo = async (productId: string, preferredApi?: ApiType) => {
  const apiType = preferredApi || ProductApiFactory.getDefaultApiType()
  const adapter = ProductApiFactory.getAdapter(apiType)

  try {
    const result = await adapter.fetchProductData(productId)

    if (!result.success) {
      throw new Error(result.error)
    }

    return result.data
  } catch (error) {
    // エラーハンドリング
    throw new ProductApiError(error.message, apiType)
  }
}

設計のメリット

単一責任の原則

各AdapterはひとつのAPI仕様に特化し、その責任のみを持つ

開放/閉鎖の原則

新しいAPIを追加する際は、既存コードを変更せずに新しいAdapterを追加するだけ

依存性の逆転

呼び出し側は具体的なAPI実装に依存せず、抽象化されたインターフェースに依存

テスタビリティの向上

MockAdapterを作成してテストが容易

実装時の注意点

レスポンス正規化の重要性

  • 各APIの異なるレスポンス形式を統一的な形式に変換
  • 必須フィールドの不足に対する適切なデフォルト値設定

エラーハンドリングの統一

  • API固有のエラーコードを統一的なエラー形式に変換
  • 原因の特定が可能な詳細なエラー情報の保持

設定の外部化

  • API KEY、URL、レート制限などの設定を環境変数で管理
  • 各APIの制限に応じた適切な設定

拡張性の考慮

フォールバック機能

const fetchWithFallback = async (productId: string) => {
  const apiPriority = ['SHOPIFY', 'WOOCOMMERCE']

  for (const apiType of apiPriority) {
    try {
      const adapter = ProductApiFactory.getAdapter(apiType)
      const result = await adapter.fetchProductData(productId)
      return result
    } catch (error) {
      console.warn(`${apiType} API failed, trying next...`)
    }
  }

  throw new Error('All APIs failed')
}

レート制限対応

interface ApiConfiguration {
  apiUrl: string
  requiredHeaders: Record<string, string>
  rateLimit?: {
    requestsPerSecond?: number
    requestsPerMinute?: number
  }
}

まとめ

Adapterパターンを使用することで:

  • 保守性: API仕様変更の影響を局所化
  • 拡張性: 新しいAPIの追加が容易
  • テスタビリティ: 各API実装の独立したテストが可能
  • 可読性: API固有の処理が明確に分離

このパターンは、外部APIに依存するアプリケーションの設計において、長期的な保守性と拡張性を提供する有効な手法です。

Discussion