🚀
外部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